feat: translate admin panel to French
Translates all user-visible text in the admin panel (login, nav, products, services, schedule, bookings, inquiries, and the shared friendlyError helper) to plain, direct French for Mira's internal use. Database CHECK-constraint enum values (product category, booking/inquiry status) and column names are left untouched in code; French display labels are added via lookup maps instead. Corresponding test assertions updated to match.
This commit is contained in:
@@ -9,7 +9,7 @@ describe('friendlyError', () => {
|
||||
message:
|
||||
'update or delete on table "products" violates foreign key constraint "inquiries_product_id_fkey" on table "inquiries"',
|
||||
})
|
||||
).toBe("This can't be deleted because it's referenced by an existing booking or inquiry.")
|
||||
).toBe('Impossible de supprimer cet élément : il est référencé par une réservation ou une demande existante.')
|
||||
})
|
||||
|
||||
it('falls back to the raw message for any other error', () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ const FOREIGN_KEY_VIOLATION = '23503'
|
||||
|
||||
export function friendlyError(error: { code?: string; message: string }): string {
|
||||
if (error.code === FOREIGN_KEY_VIOLATION) {
|
||||
return "This can't be deleted because it's referenced by an existing booking or inquiry."
|
||||
return "Impossible de supprimer cet élément : il est référencé par une réservation ou une demande existante."
|
||||
}
|
||||
return error.message
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('AdminApp routing', () => {
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
expect(screen.getByRole('heading', { name: /admin login/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: /connexion administrateur/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the products page (with layout) at /admin/products when authenticated', async () => {
|
||||
@@ -36,8 +36,8 @@ describe('AdminApp routing', () => {
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
expect(await screen.findByRole('heading', { name: /^products$/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /bookings/i })).toBeInTheDocument()
|
||||
expect(await screen.findByRole('heading', { name: /^produits$/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /réservations/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('redirects /admin to /admin/products when authenticated', async () => {
|
||||
@@ -48,7 +48,7 @@ describe('AdminApp routing', () => {
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
expect(await screen.findByRole('heading', { name: /^products$/i })).toBeInTheDocument()
|
||||
expect(await screen.findByRole('heading', { name: /^produits$/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('redirects an unmatched /admin/* path to /admin/products when authenticated', async () => {
|
||||
@@ -59,6 +59,6 @@ describe('AdminApp routing', () => {
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
expect(await screen.findByRole('heading', { name: /^products$/i })).toBeInTheDocument()
|
||||
expect(await screen.findByRole('heading', { name: /^produits$/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('AdminBookings', () => {
|
||||
render(<AdminBookings />)
|
||||
await screen.findByText('Jane Doe')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /cancel/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /annuler/i }))
|
||||
|
||||
expect(window.confirm).toHaveBeenCalled()
|
||||
await waitFor(() => expect(updateMock).toHaveBeenCalledWith({ status: 'cancelled' }))
|
||||
@@ -59,7 +59,7 @@ describe('AdminBookings', () => {
|
||||
render(<AdminBookings />)
|
||||
await screen.findByText('Jane Doe')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /cancel/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /annuler/i }))
|
||||
|
||||
expect(window.confirm).toHaveBeenCalled()
|
||||
expect(updateMock).not.toHaveBeenCalled()
|
||||
@@ -69,7 +69,7 @@ describe('AdminBookings', () => {
|
||||
render(<AdminBookings />)
|
||||
await screen.findByText('Jane Doe')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /mark completed/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /marquer comme terminée/i }))
|
||||
|
||||
expect(window.confirm).toHaveBeenCalled()
|
||||
await waitFor(() => expect(updateMock).toHaveBeenCalledWith({ status: 'completed' }))
|
||||
@@ -80,7 +80,7 @@ describe('AdminBookings', () => {
|
||||
render(<AdminBookings />)
|
||||
await screen.findByText('Jane Doe')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /mark completed/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /marquer comme terminée/i }))
|
||||
|
||||
expect(window.confirm).toHaveBeenCalled()
|
||||
expect(updateMock).not.toHaveBeenCalled()
|
||||
|
||||
@@ -14,6 +14,12 @@ interface AdminBooking {
|
||||
services: { name: string } | null
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<AdminBooking['status'], string> = {
|
||||
confirmed: 'Confirmée',
|
||||
cancelled: 'Annulée',
|
||||
completed: 'Terminée',
|
||||
}
|
||||
|
||||
function AdminBookings() {
|
||||
const [bookings, setBookings] = useState<AdminBooking[]>([])
|
||||
const [error, setError] = useState('')
|
||||
@@ -56,16 +62,16 @@ function AdminBookings() {
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1>Bookings</h1>
|
||||
<h1>Réservations</h1>
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Heure</th>
|
||||
<th>Service</th>
|
||||
<th>Contact</th>
|
||||
<th>Status</th>
|
||||
<th>Statut</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -81,29 +87,29 @@ function AdminBookings() {
|
||||
<div>{booking.contact_email}</div>
|
||||
{booking.contact_notes && <div>{booking.contact_notes}</div>}
|
||||
</td>
|
||||
<td>{booking.status}</td>
|
||||
<td>{STATUS_LABELS[booking.status]}</td>
|
||||
<td>
|
||||
{booking.status === 'confirmed' && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (window.confirm('Cancel this booking? This will free up its slot for other bookings.')) {
|
||||
if (window.confirm('Annuler cette réservation ? Cela libérera son créneau pour d\'autres réservations.')) {
|
||||
void setStatus(booking.id, 'cancelled')
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (window.confirm('Mark this booking completed?')) {
|
||||
if (window.confirm('Marquer cette réservation comme terminée ?')) {
|
||||
void setStatus(booking.id, 'completed')
|
||||
}
|
||||
}}
|
||||
>
|
||||
Mark completed
|
||||
Marquer comme terminée
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('AdminInquiries', () => {
|
||||
render(<AdminInquiries />)
|
||||
await screen.findByText('Nadia')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /mark as read/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /marquer comme lue/i }))
|
||||
|
||||
await waitFor(() => expect(updateMock).toHaveBeenCalledWith({ status: 'read' }))
|
||||
expect(eqUpdateMock).toHaveBeenCalledWith('id', 'inquiry-1')
|
||||
|
||||
@@ -11,6 +11,11 @@ interface AdminInquiry {
|
||||
products: { name: string } | null
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<AdminInquiry['status'], string> = {
|
||||
new: 'Nouvelle',
|
||||
read: 'Lue',
|
||||
}
|
||||
|
||||
function AdminInquiries() {
|
||||
const [inquiries, setInquiries] = useState<AdminInquiry[]>([])
|
||||
const [error, setError] = useState('')
|
||||
@@ -53,15 +58,15 @@ function AdminInquiries() {
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1>Inquiries</h1>
|
||||
<h1>Demandes</h1>
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Produit</th>
|
||||
<th>Contact</th>
|
||||
<th>Notes</th>
|
||||
<th>Status</th>
|
||||
<th>Statut</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -74,10 +79,10 @@ function AdminInquiries() {
|
||||
<div>{inquiry.contact_email}</div>
|
||||
</td>
|
||||
<td>{inquiry.contact_notes}</td>
|
||||
<td>{inquiry.status}</td>
|
||||
<td>{STATUS_LABELS[inquiry.status]}</td>
|
||||
<td>
|
||||
{inquiry.status === 'new' && (
|
||||
<button type="button" onClick={() => markRead(inquiry.id)}>Mark as read</button>
|
||||
<button type="button" onClick={() => markRead(inquiry.id)}>Marquer comme lue</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -37,17 +37,17 @@ describe('AdminLayout', () => {
|
||||
|
||||
it('renders nav links for every admin section and the outlet content', () => {
|
||||
renderLayout()
|
||||
expect(screen.getByRole('link', { name: /products/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /produits/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /services/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /schedule/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /bookings/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /inquiries/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /horaire/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /réservations/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /demandes/i })).toBeInTheDocument()
|
||||
expect(screen.getByText('Products content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('signs out and navigates to the login page', async () => {
|
||||
renderLayout()
|
||||
await userEvent.click(screen.getByRole('button', { name: /sign out/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /déconnexion/i }))
|
||||
expect(signOutMock).toHaveBeenCalled()
|
||||
expect(navigateMock).toHaveBeenCalledWith('/admin/login')
|
||||
})
|
||||
@@ -55,7 +55,7 @@ describe('AdminLayout', () => {
|
||||
it('does not navigate if sign out fails', async () => {
|
||||
signOutMock.mockResolvedValue({ error: new Error('Sign out failed') })
|
||||
renderLayout()
|
||||
await userEvent.click(screen.getByRole('button', { name: /sign out/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /déconnexion/i }))
|
||||
expect(signOutMock).toHaveBeenCalled()
|
||||
expect(navigateMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -3,11 +3,11 @@ import { supabase } from '../../lib/supabase'
|
||||
import styles from './AdminLayout.module.css'
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ to: '/admin/products', label: 'Products' },
|
||||
{ to: '/admin/products', label: 'Produits' },
|
||||
{ to: '/admin/services', label: 'Services' },
|
||||
{ to: '/admin/schedule', label: 'Schedule' },
|
||||
{ to: '/admin/bookings', label: 'Bookings' },
|
||||
{ to: '/admin/inquiries', label: 'Inquiries' },
|
||||
{ to: '/admin/schedule', label: 'Horaire' },
|
||||
{ to: '/admin/bookings', label: 'Réservations' },
|
||||
{ to: '/admin/inquiries', label: 'Demandes' },
|
||||
]
|
||||
|
||||
function AdminLayout() {
|
||||
@@ -34,7 +34,7 @@ function AdminLayout() {
|
||||
))}
|
||||
</nav>
|
||||
<button type="button" className={styles.signOut} onClick={handleSignOut}>
|
||||
Sign out
|
||||
Déconnexion
|
||||
</button>
|
||||
</aside>
|
||||
<main className={styles.content}>
|
||||
|
||||
@@ -27,9 +27,9 @@ describe('AdminLogin', () => {
|
||||
signInMock.mockResolvedValue({ error: null })
|
||||
render(<MemoryRouter><AdminLogin /></MemoryRouter>)
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), 'mira@example.com')
|
||||
await userEvent.type(screen.getByLabelText(/password/i), 'correct-password')
|
||||
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
|
||||
await userEvent.type(screen.getByLabelText(/e-mail/i), 'mira@example.com')
|
||||
await userEvent.type(screen.getByLabelText(/mot de passe/i), 'correct-password')
|
||||
await userEvent.click(screen.getByRole('button', { name: /se connecter/i }))
|
||||
|
||||
expect(signInMock).toHaveBeenCalledWith({ email: 'mira@example.com', password: 'correct-password' })
|
||||
expect(navigateMock).toHaveBeenCalledWith('/admin/products')
|
||||
@@ -39,11 +39,11 @@ describe('AdminLogin', () => {
|
||||
signInMock.mockResolvedValue({ error: { message: 'Invalid login credentials' } })
|
||||
render(<MemoryRouter><AdminLogin /></MemoryRouter>)
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), 'mira@example.com')
|
||||
await userEvent.type(screen.getByLabelText(/password/i), 'wrong-password')
|
||||
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
|
||||
await userEvent.type(screen.getByLabelText(/e-mail/i), 'mira@example.com')
|
||||
await userEvent.type(screen.getByLabelText(/mot de passe/i), 'wrong-password')
|
||||
await userEvent.click(screen.getByRole('button', { name: /se connecter/i }))
|
||||
|
||||
expect(await screen.findByText(/incorrect email or password/i)).toBeInTheDocument()
|
||||
expect(await screen.findByText(/adresse e-mail ou mot de passe incorrect/i)).toBeInTheDocument()
|
||||
expect(navigateMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ function AdminLogin() {
|
||||
const { error: signInError } = await supabase.auth.signInWithPassword({ email, password })
|
||||
setSubmitting(false)
|
||||
if (signInError) {
|
||||
setError('Incorrect email or password.')
|
||||
setError('Adresse e-mail ou mot de passe incorrect.')
|
||||
return
|
||||
}
|
||||
navigate('/admin/products')
|
||||
@@ -26,9 +26,9 @@ function AdminLogin() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<h1>Admin Login</h1>
|
||||
<h1>Connexion administrateur</h1>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="admin-email">Email</label>
|
||||
<label htmlFor="admin-email">E-mail</label>
|
||||
<input
|
||||
id="admin-email"
|
||||
type="email"
|
||||
@@ -38,7 +38,7 @@ function AdminLogin() {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="admin-password">Password</label>
|
||||
<label htmlFor="admin-password">Mot de passe</label>
|
||||
<input
|
||||
id="admin-password"
|
||||
type="password"
|
||||
@@ -49,7 +49,7 @@ function AdminLogin() {
|
||||
</div>
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
<button type="submit" disabled={submitting}>
|
||||
{submitting ? 'Signing in…' : 'Sign in'}
|
||||
{submitting ? 'Connexion…' : 'Se connecter'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -58,10 +58,10 @@ describe('AdminProducts', () => {
|
||||
await screen.findByText('Silk Edge Control')
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/^id$/i), 'braid-oil')
|
||||
await userEvent.type(screen.getByLabelText(/^name$/i), 'Nourishing Braid Oil')
|
||||
await userEvent.type(screen.getByLabelText(/^price$/i), '16')
|
||||
await userEvent.type(screen.getByLabelText(/^nom$/i), 'Nourishing Braid Oil')
|
||||
await userEvent.type(screen.getByLabelText(/^prix$/i), '16')
|
||||
await userEvent.type(screen.getByLabelText(/^description$/i), 'Lightweight scalp oil.')
|
||||
await userEvent.click(screen.getByRole('button', { name: /save product/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /enregistrer le produit/i }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(upsertMock).toHaveBeenCalledWith(
|
||||
@@ -75,7 +75,7 @@ describe('AdminProducts', () => {
|
||||
render(<AdminProducts />)
|
||||
await screen.findByText('Silk Edge Control')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /delete/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /supprimer/i }))
|
||||
|
||||
expect(deleteMock).toHaveBeenCalled()
|
||||
expect(eqMock).toHaveBeenCalledWith('id', 'edge-control')
|
||||
|
||||
@@ -16,6 +16,13 @@ interface AdminProduct {
|
||||
|
||||
const CATEGORIES: AdminProduct['category'][] = ['hair-care', 'extensions', 'tools', 'accessories']
|
||||
|
||||
const CATEGORY_LABELS: Record<AdminProduct['category'], string> = {
|
||||
'hair-care': 'Soins capillaires',
|
||||
extensions: 'Extensions',
|
||||
tools: 'Outils',
|
||||
accessories: 'Accessoires',
|
||||
}
|
||||
|
||||
function emptyForm(): AdminProduct {
|
||||
return { id: '', name: '', category: 'hair-care', price: 0, image: '', description: '', in_stock: true }
|
||||
}
|
||||
@@ -58,7 +65,7 @@ function AdminProducts() {
|
||||
try {
|
||||
image = await uploadSalonImage(file, `products/${form.id}-${file.name}`)
|
||||
} catch {
|
||||
setError('Image upload failed.')
|
||||
setError("Échec du téléversement de l'image.")
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
@@ -76,7 +83,7 @@ function AdminProducts() {
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!window.confirm('Delete this product?')) return
|
||||
if (!window.confirm('Supprimer ce produit ?')) return
|
||||
const { error: deleteError } = await supabase.from('products').delete().eq('id', id)
|
||||
if (deleteError) {
|
||||
setError(friendlyError(deleteError))
|
||||
@@ -89,14 +96,14 @@ function AdminProducts() {
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1>Products</h1>
|
||||
<h1>Produits</h1>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Price</th>
|
||||
<th>In stock</th>
|
||||
<th>Nom</th>
|
||||
<th>Catégorie</th>
|
||||
<th>Prix</th>
|
||||
<th>En stock</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -104,12 +111,12 @@ function AdminProducts() {
|
||||
{products.map((product) => (
|
||||
<tr key={product.id}>
|
||||
<td>{product.name}</td>
|
||||
<td>{product.category}</td>
|
||||
<td>${product.price}</td>
|
||||
<td>{product.in_stock ? 'Yes' : 'No'}</td>
|
||||
<td>{CATEGORY_LABELS[product.category]}</td>
|
||||
<td>{product.price} $</td>
|
||||
<td>{product.in_stock ? 'Oui' : 'Non'}</td>
|
||||
<td>
|
||||
<button type="button" onClick={() => setForm(product)}>Edit</button>
|
||||
<button type="button" onClick={() => handleDelete(product.id)}>Delete</button>
|
||||
<button type="button" onClick={() => setForm(product)}>Modifier</button>
|
||||
<button type="button" onClick={() => handleDelete(product.id)}>Supprimer</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -117,7 +124,7 @@ function AdminProducts() {
|
||||
</table>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSubmit}>
|
||||
<h2>{isEditing ? 'Edit product' : 'New product'}</h2>
|
||||
<h2>{isEditing ? 'Modifier le produit' : 'Nouveau produit'}</h2>
|
||||
<label htmlFor="product-id">
|
||||
Id
|
||||
<input
|
||||
@@ -129,7 +136,7 @@ function AdminProducts() {
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="product-name">
|
||||
Name
|
||||
Nom
|
||||
<input
|
||||
id="product-name"
|
||||
value={form.name}
|
||||
@@ -138,19 +145,19 @@ function AdminProducts() {
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="product-category">
|
||||
Category
|
||||
Catégorie
|
||||
<select
|
||||
id="product-category"
|
||||
value={form.category}
|
||||
onChange={(e) => setForm((f) => ({ ...f, category: e.target.value as AdminProduct['category'] }))}
|
||||
>
|
||||
{CATEGORIES.map((category) => (
|
||||
<option key={category} value={category}>{category}</option>
|
||||
<option key={category} value={category}>{CATEGORY_LABELS[category]}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label htmlFor="product-price">
|
||||
Price
|
||||
Prix
|
||||
<input
|
||||
id="product-price"
|
||||
type="number"
|
||||
@@ -185,10 +192,10 @@ function AdminProducts() {
|
||||
checked={form.in_stock}
|
||||
onChange={(e) => setForm((f) => ({ ...f, in_stock: e.target.checked }))}
|
||||
/>
|
||||
In stock
|
||||
En stock
|
||||
</label>
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
<button type="submit" disabled={saving}>Save product</button>
|
||||
<button type="submit" disabled={saving}>Enregistrer le produit</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('AdminSchedule', () => {
|
||||
render(<AdminSchedule />)
|
||||
await screen.findByDisplayValue('09:00:00')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /save schedule/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /enregistrer l'horaire/i }))
|
||||
|
||||
await waitFor(() => expect(templateUpdateMock).toHaveBeenCalled())
|
||||
expect(templateEqUpdateMock).toHaveBeenCalledWith('id', 'default')
|
||||
@@ -77,9 +77,9 @@ describe('AdminSchedule', () => {
|
||||
render(<AdminSchedule />)
|
||||
await screen.findByDisplayValue('09:00:00')
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/exception date/i), '2026-11-01')
|
||||
await userEvent.type(screen.getByLabelText(/date d'exception/i), '2026-11-01')
|
||||
await userEvent.type(screen.getByLabelText(/^note$/i), 'Closed for training')
|
||||
await userEvent.click(screen.getByRole('button', { name: /add exception/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /ajouter une exception/i }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(exceptionsInsertMock).toHaveBeenCalledWith(
|
||||
|
||||
@@ -16,7 +16,7 @@ interface ScheduleException {
|
||||
note: string | null
|
||||
}
|
||||
|
||||
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
const DAY_LABELS = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam']
|
||||
|
||||
function AdminSchedule() {
|
||||
const [template, setTemplate] = useState<ScheduleTemplate | null>(null)
|
||||
@@ -115,11 +115,11 @@ function AdminSchedule() {
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1>Schedule</h1>
|
||||
<h1>Horaire</h1>
|
||||
|
||||
{template && (
|
||||
<form className={styles.section} onSubmit={handleSaveTemplate}>
|
||||
<h2>Weekly hours</h2>
|
||||
<h2>Heures hebdomadaires</h2>
|
||||
<div className={styles.days}>
|
||||
{DAY_LABELS.map((label, day) => (
|
||||
<label key={day}>
|
||||
@@ -133,7 +133,7 @@ function AdminSchedule() {
|
||||
))}
|
||||
</div>
|
||||
<label htmlFor="schedule-start">
|
||||
Start time
|
||||
Heure de début
|
||||
<input
|
||||
id="schedule-start"
|
||||
type="time"
|
||||
@@ -143,7 +143,7 @@ function AdminSchedule() {
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="schedule-end">
|
||||
End time
|
||||
Heure de fin
|
||||
<input
|
||||
id="schedule-end"
|
||||
type="time"
|
||||
@@ -153,7 +153,7 @@ function AdminSchedule() {
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="schedule-slot-minutes">
|
||||
Slot length (minutes)
|
||||
Durée des créneaux (minutes)
|
||||
<input
|
||||
id="schedule-slot-minutes"
|
||||
type="number"
|
||||
@@ -161,7 +161,7 @@ function AdminSchedule() {
|
||||
onChange={(e) => setTemplate({ ...template, slot_minutes: Number(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={saving}>Save schedule</button>
|
||||
<button type="submit" disabled={saving}>Enregistrer l'horaire</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -171,7 +171,7 @@ function AdminSchedule() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Closed</th>
|
||||
<th>Fermé</th>
|
||||
<th>Note</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -180,10 +180,10 @@ function AdminSchedule() {
|
||||
{exceptions.map((exception) => (
|
||||
<tr key={exception.date}>
|
||||
<td>{exception.date}</td>
|
||||
<td>{exception.closed ? 'Yes' : 'No'}</td>
|
||||
<td>{exception.closed ? 'Oui' : 'Non'}</td>
|
||||
<td>{exception.note}</td>
|
||||
<td>
|
||||
<button type="button" onClick={() => handleRemoveException(exception.date)}>Remove</button>
|
||||
<button type="button" onClick={() => handleRemoveException(exception.date)}>Retirer</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -192,7 +192,7 @@ function AdminSchedule() {
|
||||
|
||||
<form onSubmit={handleAddException} className={styles.form}>
|
||||
<label htmlFor="exception-date">
|
||||
Exception date
|
||||
Date d'exception
|
||||
<input
|
||||
id="exception-date"
|
||||
type="date"
|
||||
@@ -207,7 +207,7 @@ function AdminSchedule() {
|
||||
checked={newException.closed}
|
||||
onChange={(e) => setNewException((f) => ({ ...f, closed: e.target.checked }))}
|
||||
/>
|
||||
Closed
|
||||
Fermé
|
||||
</label>
|
||||
<label htmlFor="exception-note">
|
||||
Note
|
||||
@@ -217,7 +217,7 @@ function AdminSchedule() {
|
||||
onChange={(e) => setNewException((f) => ({ ...f, note: e.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Add exception</button>
|
||||
<button type="submit">Ajouter une exception</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -55,11 +55,11 @@ describe('AdminServices', () => {
|
||||
await screen.findByText('Knotless Braids')
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/^id$/i), 'cornrows')
|
||||
await userEvent.type(screen.getByLabelText(/^name$/i), 'Cornrows')
|
||||
await userEvent.type(screen.getByLabelText(/duration/i), '120')
|
||||
await userEvent.type(screen.getByLabelText(/price from/i), '90')
|
||||
await userEvent.type(screen.getByLabelText(/^nom$/i), 'Cornrows')
|
||||
await userEvent.type(screen.getByLabelText(/durée en minutes/i), '120')
|
||||
await userEvent.type(screen.getByLabelText(/prix à partir de/i), '90')
|
||||
await userEvent.type(screen.getByLabelText(/^description$/i), 'Classic style.')
|
||||
await userEvent.click(screen.getByRole('button', { name: /save service/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /enregistrer le service/i }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(upsertMock).toHaveBeenCalledWith(
|
||||
@@ -73,7 +73,7 @@ describe('AdminServices', () => {
|
||||
render(<AdminServices />)
|
||||
await screen.findByText('Knotless Braids')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /delete/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /supprimer/i }))
|
||||
|
||||
expect(deleteMock).toHaveBeenCalled()
|
||||
expect(eqMock).toHaveBeenCalledWith('id', 'knotless')
|
||||
|
||||
@@ -55,7 +55,7 @@ function AdminServices() {
|
||||
try {
|
||||
image = await uploadSalonImage(file, `styles/${form.id}-${file.name}`)
|
||||
} catch {
|
||||
setError('Image upload failed.')
|
||||
setError("Échec du téléversement de l'image.")
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
@@ -73,7 +73,7 @@ function AdminServices() {
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!window.confirm('Delete this service?')) return
|
||||
if (!window.confirm('Supprimer ce service ?')) return
|
||||
const { error: deleteError } = await supabase.from('services').delete().eq('id', id)
|
||||
if (deleteError) {
|
||||
setError(friendlyError(deleteError))
|
||||
@@ -90,9 +90,9 @@ function AdminServices() {
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Duration (min)</th>
|
||||
<th>Price from</th>
|
||||
<th>Nom</th>
|
||||
<th>Durée (min)</th>
|
||||
<th>Prix à partir de</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -101,10 +101,10 @@ function AdminServices() {
|
||||
<tr key={service.id}>
|
||||
<td>{service.name}</td>
|
||||
<td>{service.duration_minutes}</td>
|
||||
<td>${service.price_from}</td>
|
||||
<td>{service.price_from} $</td>
|
||||
<td>
|
||||
<button type="button" onClick={() => setForm(service)}>Edit</button>
|
||||
<button type="button" onClick={() => handleDelete(service.id)}>Delete</button>
|
||||
<button type="button" onClick={() => setForm(service)}>Modifier</button>
|
||||
<button type="button" onClick={() => handleDelete(service.id)}>Supprimer</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -112,7 +112,7 @@ function AdminServices() {
|
||||
</table>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSubmit}>
|
||||
<h2>{isEditing ? 'Edit service' : 'New service'}</h2>
|
||||
<h2>{isEditing ? 'Modifier le service' : 'Nouveau service'}</h2>
|
||||
<label htmlFor="service-id">
|
||||
Id
|
||||
<input
|
||||
@@ -124,7 +124,7 @@ function AdminServices() {
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="service-name">
|
||||
Name
|
||||
Nom
|
||||
<input
|
||||
id="service-name"
|
||||
value={form.name}
|
||||
@@ -133,7 +133,7 @@ function AdminServices() {
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="service-duration">
|
||||
Duration minutes
|
||||
Durée en minutes
|
||||
<input
|
||||
id="service-duration"
|
||||
type="number"
|
||||
@@ -143,7 +143,7 @@ function AdminServices() {
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="service-price-from">
|
||||
Price from
|
||||
Prix à partir de
|
||||
<input
|
||||
id="service-price-from"
|
||||
type="number"
|
||||
@@ -172,7 +172,7 @@ function AdminServices() {
|
||||
/>
|
||||
</label>
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
<button type="submit" disabled={saving}>Save service</button>
|
||||
<button type="submit" disabled={saving}>Enregistrer le service</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user