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:
belviskhoremk
2026-08-12 18:51:53 +02:00
parent b84db38096
commit 3eb361f57f
17 changed files with 123 additions and 105 deletions

View File

@@ -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', () => {

View File

@@ -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
}

View File

@@ -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()
})
})

View File

@@ -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()

View File

@@ -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>
</>
)}

View File

@@ -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')

View File

@@ -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>

View File

@@ -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()
})

View File

@@ -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}>

View File

@@ -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()
})
})

View File

@@ -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>

View File

@@ -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')

View File

@@ -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>
)

View File

@@ -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(

View File

@@ -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&apos;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&apos;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>

View File

@@ -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')

View File

@@ -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>
)