diff --git a/client/index.html b/client/index.html index e558bdf..f9cfe11 100644 --- a/client/index.html +++ b/client/index.html @@ -3,12 +3,20 @@ + + + + + + + + QueueMed — Salle d'attente virtuelle diff --git a/client/public/favicon.svg b/client/public/favicon.svg new file mode 100644 index 0000000..fce6a66 --- /dev/null +++ b/client/public/favicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/client/public/icon-192x192.svg b/client/public/icon-192x192.svg new file mode 100644 index 0000000..9c82f55 --- /dev/null +++ b/client/public/icon-192x192.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/client/public/icon-512x512.svg b/client/public/icon-512x512.svg new file mode 100644 index 0000000..7955f12 --- /dev/null +++ b/client/public/icon-512x512.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index 6d74f9a..aff3448 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -19,11 +19,14 @@ const buttonVariants = cva( ghost: "hover:bg-accent dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", + gradient: + "bg-gradient-to-r from-emerald-500 to-cyan-500 text-white shadow-md hover:shadow-lg hover:from-emerald-600 hover:to-cyan-600", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + xl: "h-12 rounded-xl px-8 text-base has-[>svg]:px-6", icon: "size-9", "icon-sm": "size-8", "icon-lg": "size-10", diff --git a/client/src/i18n.ts b/client/src/i18n.ts index f9b500c..a7ed7cd 100644 --- a/client/src/i18n.ts +++ b/client/src/i18n.ts @@ -23,4 +23,12 @@ void i18n }, }); +const syncHtmlLang = (lng: string) => { + if (typeof document !== "undefined") { + document.documentElement.lang = lng.startsWith("en") ? "en" : "fr"; + } +}; +syncHtmlLang(i18n.resolvedLanguage ?? i18n.language ?? "fr"); +i18n.on("languageChanged", syncHtmlLang); + export default i18n; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 1eea6e8..bd2395f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -35,7 +35,146 @@ "heroSubtitle": "QueueMed digitises your queue. Patients scan a QR code and follow their turn in real time — no app required.", "heroCtaPrimary": "Start free", "heroCtaSecondary": "Sign in", - "trustedBy": "Over 200 practices trust us" + "trustedBy": "Over 200 practices trust us", + "nav": { + "features": "Features", + "how": "How it works", + "pricing": "Pricing", + "help": "Help", + "login": "Sign in", + "freeTrial": "Free trial" + }, + "heroBadge": "Next-generation virtual waiting room", + "heroH1Part1": "Your patients", + "heroH1Part2": "don't wait anymore,", + "heroH1Part3": "they", + "heroH1Accent": "live", + "heroDescription": "QueueMed turns your medical practice into a seamless experience. QR code, real-time tracking, notifications, display screen — no app required.", + "heroStartTrial": "Start free trial (30 days)", + "heroSeeHow": "See how it works", + "heroCheck1": "No credit card", + "heroCheck2": "Setup in 2 minutes", + "heroCheck3": "Data hosted in France", + "mockCurrent": "Current patient", + "mockRoom": "Room 2 — Dr. Martin", + "mockUpcoming": "Up next", + "mockAnonymous": "Anonymous patient", + "mockMin": "min", + "featuresKicker": "Features", + "featuresTitlePart1": "Everything your practice", + "featuresTitleAccent": "needs", + "featuresSubtitle": "A platform built by and for doctors. Elegant, fast, compliant.", + "features": { + "qrCode": { + "title": "Rotating QR code", + "description": "Patients scan a QR at the entrance — anti-cheat rotating token, no app to install." + }, + "realtime": { + "title": "Real-time position", + "description": "Each patient sees their position and estimated wait time, updated live via WebSocket." + }, + "alerts": { + "title": "Smart alerts", + "description": "Push notification when their turn is near — patients can step out of the waiting room." + }, + "displayScreen": { + "title": "Waiting room screen", + "description": "Full-screen display on tablet with ticker, large called number and live queue." + }, + "stats": { + "title": "Precise analytics", + "description": "Traffic by hour, day, average duration. AI recommendations to optimise your practice." + }, + "gdpr": { + "title": "GDPR & sovereign", + "description": "Data hosted in France. No patient tracking. Bank-grade security (TLS, JWT, bcrypt)." + } + }, + "howKicker": "How it works", + "howTitleAccent": "3 steps", + "howTitleRest": "and you're live", + "steps": { + "step1": { + "title": "Set up your practice", + "desc": "2 minutes to create your queue. Print the QR code and place it at reception." + }, + "step2": { + "title": "Patients scan", + "desc": "They open their camera, scan, and join the queue with one tap." + }, + "step3": { + "title": "You call the next one", + "desc": "One click from your dashboard, the patient is notified, the screen updates." + } + }, + "pricingKicker": "Pricing", + "pricingTitlePart1": "Simple and", + "pricingTitleAccent": "transparent", + "pricingSubtitle": "30-day free trial, no commitment, no credit card.", + "pricingPopular": "Popular", + "pricing": { + "trial": { + "name": "Trial", + "price": "Free", + "period": "30 days", + "description": "All features, no credit card.", + "feature1": "1 practice", + "feature2": "Unlimited patients", + "feature3": "Basic analytics", + "feature4": "Email support", + "cta": "Start trial" + }, + "basic": { + "name": "Basic", + "price": "€29", + "period": "/ month", + "description": "For a solo practice.", + "feature1": "1 practice", + "feature2": "Unlimited patients", + "feature3": "Display screen", + "feature4": "Advanced analytics", + "feature5": "Priority support", + "cta": "Subscribe" + }, + "pro": { + "name": "Pro", + "price": "€79", + "period": "/ month", + "description": "For medical centres.", + "feature1": "Unlimited practices", + "feature2": "Multi-practitioner", + "feature3": "AI recommendations", + "feature4": "Advanced CSV export", + "feature5": "Phone support", + "cta": "Subscribe" + } + }, + "testimonialsKicker": "Testimonials", + "testimonialsTitlePart1": "Trusted by", + "testimonialsTitleAccent": "200+ doctors", + "testimonials": { + "t1": { + "name": "Dr. Marie Dubois", + "role": "GP, Lyon", + "quote": "My patients love it. No more crowded waiting room, no more stress. I save an hour every day, easily." + }, + "t2": { + "name": "Dr. Karim Benali", + "role": "Paediatrician, Marseille", + "quote": "Setup in 5 minutes. The rotating QR prevents abuse and the display screen is perfect for my waiting room." + }, + "t3": { + "name": "Dr. Sophie Lefèvre", + "role": "Dentist, Bordeaux", + "quote": "The analytics let me optimise my appointment slots. Clear ROI from the first month." + } + }, + "ctaTitle": "Ready to transform your practice?", + "ctaSubtitle": "30-day free trial. No credit card. Setup in 2 minutes.", + "ctaButton": "Get started now", + "footerTagline": "Virtual waiting room", + "footerHelp": "Help", + "footerContact": "Contact" }, "login": { "metaTitle": "Sign in — QueueMed", @@ -106,7 +245,50 @@ "kpiPatients": "Patients today", "kpiAvgWait": "Avg wait", "noClinic": "You don't have a practice yet.", - "createClinic": "Create a practice" + "createClinic": "Create a practice", + "metaDescription": "Overview of your practices, KPIs and quick links in QueueMed.", + "fallbackDoctor": "Doctor", + "hello": "Hello", + "dayStarts": "Your day starts here.", + "trialDaysLeft_one": "Free trial — {{count}} day left", + "trialDaysLeft_other": "Free trial — {{count}} days left", + "trialExpired": "Trial expired", + "subscribe": "Subscribe", + "subscriptionExpired": "Subscription expired", + "renew": "Renew", + "kpiActiveClinics": "Active practices", + "kpiPatients7d": "Patients (7d)", + "kpiAvgWaitShort": "Avg wait", + "kpiPlan": "Plan", + "minutesShort": "min", + "yourClinics": "Your practices", + "manage": "Manage", + "welcomeTitle": "Welcome to QueueMed!", + "welcomeSubtitle": "Set up your first practice in 2 minutes with our wizard.", + "startSetup": "Start setup", + "createManually": "Create manually", + "statusOpen": "Open", + "statusClosed": "Closed", + "minPerPatient": "min/patient", + "quickAccess": "Quick access", + "quick": { + "analytics": { + "label": "Analytics", + "desc": "Stats & AI" + }, + "subscription": { + "label": "Subscription", + "desc": "Manage your plan" + }, + "display": { + "label": "Display", + "desc": "Waiting room screen" + }, + "help": { + "label": "Help", + "desc": "Help center & FAQ" + } + } }, "queue": { "metaTitle": "Queue management — QueueMed", @@ -120,7 +302,49 @@ "waiting": "Waiting", "called": "Called", "inConsultation": "In consultation", - "addPrintedTicket": "Add printed ticket" + "addPrintedTicket": "Add printed ticket", + "metaDescription": "Manage your practice queue in real time.", + "openShort": "Open", + "closeShort": "Close", + "clinicNotFound": "Practice not found.", + "displayScreen": "Display screen", + "headerCounts": "{{waiting}} waiting · {{called}} called", + "actions": "Actions", + "printTicket": "Print a ticket", + "resetQueue": "Reset queue", + "resetConfirm": "Are you sure? The entire queue will be cleared.", + "resetYes": "Yes, reset", + "qrCode": "QR Code", + "qrAlt": "Practice QR code", + "qrExpires": "Expires", + "qrRenew": "Renew", + "qrPoster": "Poster", + "statsTitle": "Statistics", + "statsAvgConsult": "Avg consult.", + "statsAvgConsultValue": "~{{minutes}} min", + "queueListTitle": "Queue", + "patientCount": "{{count}} patient(s)", + "openToWelcome": "Open the queue to start welcoming patients.", + "patientFallback": "Patient #{{number}}", + "printed": "Printed", + "posShort": "Pos.", + "minShort": "min", + "callThisPatient": "Call this patient", + "endConsultation": "End consultation", + "markAbsentTitle": "Mark as absent", + "statusWaiting": "Waiting", + "statusCalled": "Called", + "statusInConsultation": "In consult.", + "statusDone": "Done", + "statusAbsent": "Absent", + "statusCanceled": "Canceled", + "toastTicketCalled": "Ticket #{{number}} called", + "toastPatientAbsent": "Patient marked absent", + "toastConsultDone": "Consultation completed", + "toastPatientCalled": "Patient called", + "toastQueueReset": "Queue reset", + "toastTicketCreated": "Ticket #{{number}} created", + "toastQrRegenerated": "QR regenerated" }, "patient": { "metaTitle": "Your spot — QueueMed", @@ -132,17 +356,63 @@ "phone": "Phone", "whatsappOptional": "WhatsApp (optional)", "joining": "Joining…", - "youAreCalled": "It's your turn!", + "youAreCalled": "It's your turn", "pleaseGoTo": "Please go to reception", "leaveQueue": "Leave queue", - "thanksForVisit": "Thanks for your visit" + "thanksForVisit": "Thanks for your visit.", + "metaDescription": "Track your position in the waiting queue in real time.", + "minutesFull": "minutes", + "calledDesc": "Please go to the consultation room immediately.", + "inConsult": "In consultation", + "inConsultDesc": "You are currently with your doctor.", + "consultDone": "Consultation completed", + "seeYouSoon": "See you soon!", + "ticketClosed": "Ticket closed", + "markedAbsentDesc": "You were marked absent. Rescan the QR at reception to rejoin.", + "ticketCanceledDesc": "Your ticket has been canceled.", + "ticketNotFound": "Ticket not found", + "ticketNotFoundDesc": "This link is invalid or has expired. Please rescan the QR code at reception.", + "yourTicket": "Your ticket", + "anonymousPatient": "Anonymous patient", + "position": "Position", + "outOf": "of {{count}}", + "wait": "Wait", + "currentPatient": "Current patient", + "keepPageOpen": "Keep this page open. You'll be notified when your turn approaches.", + "cancelConfirm": "Cancel your ticket? You'll have to rescan the QR to come back.", + "cancelMyTicket": "Cancel my ticket", + "joinedAt": "Joined at {{time}}", + "refresh": "Refresh", + "notifTitle": "It's your turn!", + "notifBody": "Please go to the consultation room.", + "toastApproaching": "You're up next — get ready!", + "toastTicketCanceled": "Ticket canceled" }, "display": { "metaTitle": "Display screen — QueueMed", "nowCalling": "Now calling", "ticket": "Ticket", "waiting": "waiting", - "queueClosed": "Queue closed" + "queueClosed": "Queue closed", + "metaDescription": "Real-time display of the practice waiting queue.", + "clinicNotFound": "Practice not found", + "clinicNotFoundDesc": "Check the URL or contact the doctor.", + "brandTagline": "QueueMed — Live queue", + "live": "Live", + "reconnecting": "Reconnecting...", + "patientCalled": "Patient called", + "consultationRoom": "Consultation room", + "noPatientCalled": "No patient called", + "upcoming": "Upcoming", + "waitingCount": "{{count}} waiting", + "statusOpen": "OPEN", + "statusClosed": "CLOSED", + "noWaiting": "No patients waiting", + "anonymousPatient": "Anonymous patient", + "minShort": "min", + "position": "Position", + "nextLabel": "NEXT", + "ticker": "✨ Welcome to {{clinic}} — Scan the QR code at reception to join the online queue — Track your position in real time on your phone — You'll be notified when your turn approaches" }, "analytics": { "metaTitle": "Analytics — QueueMed", @@ -155,7 +425,43 @@ "byHour": "By hour", "byDay": "By day", "exportCsv": "Export CSV", - "recommendations": "AI Recommendations" + "recommendations": "AI Recommendations", + "metaDescription": "Patient flow statistics, wait times and AI recommendations for your medical practice.", + "headerSubtitle": "Patient flow, wait times and AI recommendations.", + "recommendationsSubtitle": "Optimisations identified for the selected period.", + "period": "Period", + "clinic": "Practice", + "allClinics": "All", + "daysLabel": "{{count}} days", + "hourSuffix": "h", + "minShort": "min", + "kpiJoined": "Patients joined", + "kpiServed": "Served", + "kpiAbsent": "No-shows", + "kpiAvgWait": "Avg wait", + "kpiAvgConsultation": "Avg cons.", + "flowJoined": "Joined", + "flowServed": "Served", + "flowAbsent": "No-shows", + "chartByHour": "Patient flow by hour", + "chartByDay": "Patient flow by day", + "chartFlow": "Patient flow", + "chartAvgWait": "Average wait time", + "peakHour": "Peak hour:", + "peakDay": "Busiest day:", + "minutesOnAverage": "minutes on average", + "consultationLabel": "Consultation", + "totalLabel": "Total", + "daySun": "Sun", + "dayMon": "Mon", + "dayTue": "Tue", + "dayWed": "Wed", + "dayThu": "Thu", + "dayFri": "Fri", + "daySat": "Sat", + "toastNoClinic": "No practice selected", + "toastExportFailed": "Export failed", + "toastExportSuccess": "CSV exported" }, "clinicSettings": { "metaTitle": "Practice settings — QueueMed", @@ -163,7 +469,38 @@ "general": "General", "openingHours": "Opening hours", "whatsapp": "WhatsApp", - "save": "Save" + "save": "Save", + "metaDescription": "Customise the patient experience and queue management for your practice.", + "subtitle": "Customise the patient experience and queue management", + "openingHoursHelp": "Shown to patients on the queue page.", + "saveButton": "Save settings", + "selectClinic": "Select a practice", + "welcomeMessage": "Welcome message", + "welcomeMessageHelp": "Shown to patients when they join the queue. Leave empty to hide the message.", + "welcomeMessagePlaceholder": "E.g. Welcome to Dr Smith's practice. Please wait, we'll call you as soon as possible.", + "patientLanguage": "Patient interface language", + "patientLanguageHelp": "Language shown on the patient screen and in WhatsApp messages.", + "queueSettings": "Queue settings", + "avgConsultation": "Avg consultation duration", + "maxQueueSize": "Max queue size", + "autoAbsentTimer": "Auto no-show timer", + "patients": "patients", + "minShort": "min", + "open": "Open", + "closed": "Closed", + "openingTime": "Opening time", + "closingTime": "Closing time", + "timeSeparator": "to", + "autoAbsentDisabled": "Disabled — the doctor manually marks no-shows", + "autoAbsentEnabled": "Patient is marked as no-show after {{minutes}} min without response", + "dayMon": "Monday", + "dayTue": "Tuesday", + "dayWed": "Wednesday", + "dayThu": "Thursday", + "dayFri": "Friday", + "daySat": "Saturday", + "daySun": "Sunday", + "toastSaved": "Settings saved" }, "whatsapp": { "metaTitle": "WhatsApp — QueueMed", @@ -172,7 +509,49 @@ "disconnect": "Disconnect", "scanQr": "Scan this QR code with WhatsApp", "connected": "Connected", - "disconnected": "Disconnected" + "disconnected": "Disconnected", + "metaDescription": "Connect WhatsApp to your practice to send automatic notifications to your patients.", + "headerTitle": "WhatsApp notifications", + "headerSubtitle": "Connect WhatsApp to send automatic alerts to your patients", + "statusDisconnected": "Disconnected", + "statusConnecting": "Connecting…", + "statusQrReady": "Waiting for scan", + "statusConnected": "Connected", + "disclaimerNote": "Note:", + "disclaimerBody": "This feature uses WhatsApp Web (unofficial protocol). Keep sending below 500 messages/day to avoid any risk of restriction. A personal or business WhatsApp number is required.", + "clinic": "Practice", + "connectionStatus": "Connection status", + "qrAltText": "WhatsApp QR code", + "howToScan": "How to scan", + "scanStep1": "1. Open WhatsApp on your phone", + "scanStep2": "2. Tap ⋮ → Linked devices", + "scanStep3": "3. Tap Link a device", + "scanStep4": "4. Scan this QR code", + "refreshStatus": "Refresh status", + "connectedTitle": "WhatsApp connected", + "connectedBody": "Automatic notifications are active for this practice.", + "notConnectedTitle": "Not connected", + "notConnectedBody": "Click \"Connect\" to generate a QR code to scan.", + "newQr": "New QR code", + "cancel": "Cancel", + "testMessage": "Test message", + "testMessageHelp": "Send a test message to confirm the connection is working.", + "testPhonePlaceholder": "International number (e.g. 33612345678)", + "testPhoneLabel": "Phone number", + "send": "Send", + "phoneFormatHint": "Enter the number without the + (e.g. 33612345678 for +33 6 12 34 56 78)", + "howItWorks": "How it works", + "step1Title": "Sign-up", + "step1Desc": "The patient enters their WhatsApp number when signing up via QR code", + "step2Title": "Almost-up alert", + "step2Desc": "When 2 patients remain ahead, they receive an alert message", + "step3Title": "It's their turn", + "step3Desc": "When the doctor calls them, they receive a message immediately", + "toastQrGenerated": "QR code generated — scan with WhatsApp", + "toastConnected": "WhatsApp connected!", + "toastDisconnected": "WhatsApp session disconnected", + "toastTestSent": "Test message sent!", + "toastTestFailed": "Failed: {{error}}" }, "onboarding": { "metaTitle": "Welcome — QueueMed", @@ -180,7 +559,59 @@ "step1": "Create your practice", "step2": "Print your QR code", "step3": "Open the queue", - "finish": "Finish" + "finish": "Finish", + "metaDescription": "Set up your first QueueMed practice in 2 minutes.", + "headerPart1": "Initial", + "headerAccent": "setup", + "headerSubtitle": "Set up your first practice in 2 minutes.", + "steps": { + "s1": { + "title": "Your practice", + "description": "Provide basic information and queue parameters." + }, + "s2": { + "title": "Your QR code", + "description": "Print or preview the poster to display at reception." + }, + "s3": { + "title": "All set!", + "description": "Here are the next steps to get started." + } + }, + "fieldName": "Practice name", + "fieldAddress": "Address", + "fieldPhone": "Phone", + "optional": "(optional)", + "placeholderName": "e.g. Dr. Smith Practice", + "placeholderAddress": "12 Main Street, London", + "placeholderPhone": "+44 20 1234 5678", + "queueSettings": "Queue settings", + "avgConsultation": "Average consultation duration", + "avgConsultationHelp": "Used to estimate patient wait times.", + "maxQueueSize": "Maximum queue size", + "maxQueueHelp": "Beyond this, new patients won't be able to join.", + "minutesShort": "min", + "patientsShort": "pat.", + "qrIntroPart1": "Here is the QR code for", + "qrIntroPart2": ". Print it and place it at the practice entrance so your patients can join the queue.", + "qrAlt": "QR Code", + "qrUnavailable": "QR unavailable", + "viewPoster": "View / print poster", + "continue": "Continue", + "doneTitle": "Practice configured!", + "donePart1": "Practice", + "donePart2": "is ready. Here are your next steps to get started.", + "next1": "Print the QR code and display it at reception", + "next2": "Set up the display screen on your tablet or monitor", + "next3": "Open the queue from the dashboard at the start of each day", + "creating": "Creating...", + "createClinic": "Create practice", + "back": "Back", + "viewQueue": "View queue", + "dashboard": "Dashboard", + "skip": "Skip for now", + "toastCreated": "Practice created successfully!", + "errorNameRequired": "Practice name is required." }, "subscription": { "metaTitle": "Subscription — QueueMed", @@ -189,11 +620,357 @@ "active": "Active", "expired": "Expired", "daysLeft": "{{days}} days left", - "upgrade": "Upgrade plan" + "upgrade": "Upgrade plan", + "metaDescription": "Manage your QueueMed subscription and trial period.", + "subtitle": "Manage your plan and trial period.", + "daysCount_one": "{{count}} day", + "daysCount_other": "{{count}} days", + "currentPlan": "Current plan", + "currentPlanLabel": "Current plan", + "expiredMessage": "Your subscription has expired. Renew to keep using QueueMed.", + "freeTrial": "Free trial", + "nextRenewal": "Next renewal", + "in": "in", + "untilDate": "(until {{date}})", + "subscribeNow": "Subscribe now", + "dayN": "Day {{day}}", + "choosePlan": "Choose your plan", + "popular": "Popular", + "current": "Current", + "automaticTrial": "Automatic trial", + "subscribe": "Subscribe", + "commitmentTitle": "Our commitment", + "commitmentBody": "Cancel any time. Data hosted in France. GDPR compliant. Free assisted migration and setup.", + "toastRedirect": "Redirecting to {{plan}} checkout…", + "toastRedirectDescription": "Stripe integration will be enabled soon.", + "plans": { + "trial": { + "name": "Trial", + "period": "30 days", + "description": "Try QueueMed with no commitment.", + "features": { + "0": "1 practice", + "1": "Unlimited patients", + "2": "Basic statistics", + "3": "Email support" + } + }, + "basic": { + "name": "Basic", + "period": "/ month", + "description": "For a single practice.", + "features": { + "0": "1 practice", + "1": "Unlimited patients", + "2": "Display screen", + "3": "Advanced statistics", + "4": "Priority support" + } + }, + "pro": { + "name": "Pro", + "period": "/ month", + "description": "Medical centres and multi-practitioner teams.", + "features": { + "0": "Unlimited practices", + "1": "Multi-practitioner", + "2": "AI recommendations", + "3": "CSV export", + "4": "Phone support" + } + } + } }, "help": { "metaTitle": "Help — QueueMed", "title": "Help center", - "subtitle": "Find quick answers to your questions" + "subtitle": "Find quick answers to your questions", + "metaDescription": "QueueMed help centre: find quick answers to your questions.", + "headerCenter": "Help", + "headerHelp": "center", + "headerSubtitle": "Find quick answers to your questions about QueueMed.", + "searchPlaceholder": "Search a question...", + "noResults": "No question matches your search.", + "contactTitle": "Can't find your answer?", + "contactBody": "Our team is available to help you set up and use QueueMed in your practice.", + "dashboardButton": "Dashboard", + "contactButton": "Contact support", + "categories": { + "all": "All", + "gettingStarted": "Getting started", + "queueManagement": "Queue management", + "patientExperience": "Patient experience", + "displayScreen": "Display screen", + "subscription": "Subscription", + "technical": "Technical" + }, + "quickLinks": { + "gettingStarted": "Getting started", + "patients": "Patients", + "display": "Display", + "subscription": "Subscription" + }, + "faq": { + "createClinic": { + "q": "How do I create my first practice?", + "a": "On your first sign-in, follow the setup wizard by clicking 'Start setup' from the dashboard. Enter the name, optional address and queue settings (average consultation duration, maximum size). A unique QR code is generated automatically." + }, + "printPoster": { + "q": "How do I print my QR code poster?", + "a": "From a practice's management page, click 'QR poster'. The page shows an A4 poster ready to print. Use coloured paper if possible, laminate the poster and place it at eye level at the entrance of the practice." + }, + "setupTime": { + "q": "How long does it take to set up QueueMed?", + "a": "About 2 minutes: create your account, set up your first practice and print the QR code. You can welcome your first patients in less than 5 minutes total." + }, + "openCloseQueue": { + "q": "How do I open and close the queue?", + "a": "On the 'Queue management' page, select your practice and click 'Open queue'. Patients can then join. At the end of the day, click 'Close queue' then 'Reset' to start fresh the next day." + }, + "callNext": { + "q": "How do I call the next patient?", + "a": "Click 'Call next' in the management interface. The number is automatically shown on the waiting-room display screen and the patient receives a push notification on their phone." + }, + "noShow": { + "q": "What if a patient doesn't show up?", + "a": "Click 'Absent' next to the patient's name. They are removed from the queue and the other patients' positions update automatically. The patient will need to rescan the QR code to rejoin." + }, + "reorder": { + "q": "Can I reorder patients?", + "a": "Yes. In the queue list, drag and drop patients to change their order. Positions and waiting times recalculate live, and each patient receives the update on their phone." + }, + "printedTicket": { + "q": "How do I print a ticket for a patient without a smartphone?", + "a": "In the management interface, click 'Add patient' then tick 'No smartphone'. A printable ticket opens in a new tab with the number and position. Hand it to the patient — they can follow their turn on the display screen." + }, + "patientJoin": { + "q": "How does a patient join the queue?", + "a": "The patient opens their smartphone camera and scans the QR code displayed at reception. A link opens automatically — they tap it to join the queue. No app to install." + }, + "patientLeave": { + "q": "Can the patient leave the physical waiting room?", + "a": "Yes, that's the main benefit of QueueMed. The patient keeps the page open on their phone and can step away. They receive a push notification when their turn approaches. Recommend they stay within 5 minutes of the practice." + }, + "qrRotation": { + "q": "Why does the QR code sometimes stop working?", + "a": "The QR code rotates automatically at regular intervals (anti-cheat system) to prevent fraudulent sharing of the link outside the practice. If a patient gets an error, they just need to rescan the QR code at reception." + }, + "notification": { + "q": "Does the patient actually receive the notification?", + "a": "On first access, the browser asks them for notification permission. If they accept, they'll receive a push notification + vibration when their turn is called. Otherwise, the page stays up to date in real time as long as it's open." + }, + "displaySetup": { + "q": "How do I configure the display screen?", + "a": "On your practice's page, copy the 'Display screen link' (/display/:clinicId). Open this link on your waiting-room tablet or monitor, then enable full-screen mode (F11 on PC). The screen updates automatically via WebSocket." + }, + "displayHardware": { + "q": "What hardware should I use for the display?", + "a": "Any tablet, monitor or TV connected to the internet with a modern browser (Chrome, Safari, Edge). A simple 80€ Android tablet does the job perfectly." + }, + "internetOutage": { + "q": "What happens if the internet goes down?", + "a": "The display screen shows an orange 'Reconnecting...' indicator. Patients already in the queue keep their position. As soon as the connection is restored, syncing resumes automatically." + }, + "trialDuration": { + "q": "How long is the free trial?", + "a": "The free trial lasts 30 days from your first sign-in. All features are available without restriction during this period, and you can create multiple practices and welcome an unlimited number of patients." + }, + "afterTrial": { + "q": "What happens after the free trial?", + "a": "Access to management features (open queue, call patients, create practices) is blocked until you subscribe to a paid plan. Your data is preserved and patients can still see their position in active queues." + }, + "cancelSub": { + "q": "Can I cancel my subscription?", + "a": "Yes, you can cancel any time from the 'Subscription' page in your dashboard. Access to paid features remains active until the end of the period already paid for." + }, + "clinicCount": { + "q": "How many practices can I manage?", + "a": "The Solo plan includes 1 practice. The Pro plan allows up to 5 practices. The Practice plan includes unlimited practices and gives access to advanced statistics with AI recommendations." + }, + "devices": { + "q": "What devices does QueueMed work on?", + "a": "QueueMed works on any device with a modern browser: iOS and Android smartphones, tablets, Windows / Mac / Linux computers. No app to install. Recommended: an up-to-date Chrome or Safari." + }, + "dataSecurity": { + "q": "Is my patient data secure?", + "a": "Yes. Names and ticket numbers are encrypted in transit (HTTPS) and stored on servers hosted in France. No medical data is collected. Patients are identified only by an optional name." + }, + "exportStats": { + "q": "Can I export my statistics?", + "a": "Yes. From the 'Analytics' page, click 'Export to CSV' to download the full consultation history. The file includes times, waiting durations and consultation durations for each patient." + }, + "offline": { + "q": "Does QueueMed work offline?", + "a": "No, an internet connection is required for real-time synchronisation between the doctor, the display screen and patients. In case of an outage, the app resumes automatically once the connection returns." + } + } + }, + "clinics": { + "metaTitle": "My practices — QueueMed", + "metaDescription": "Manage your practices, their QR codes and settings.", + "title": "My practices", + "subtitle": "Manage your practices, their QR codes and settings.", + "newClinic": "New practice", + "emptyTitle": "No practice yet", + "emptySubtitle": "Create your first practice to get started.", + "createClinic": "Create a practice", + "statusOpen": "Open", + "statusClosed": "Closed", + "statCons": "Cons.", + "statMax": "Max", + "statQrRot": "QR rot.", + "minutesShort": "min", + "manageQueue": "Manage queue", + "open": "Open", + "close": "Close", + "qr": "QR", + "screen": "Screen", + "editAction": "Edit", + "dialogEditTitle": "Edit practice", + "dialogCreateTitle": "New practice", + "dialogDescription": "Set the information and waiting-room parameters.", + "fieldName": "Practice name", + "fieldAddress": "Address", + "fieldPhone": "Phone", + "fieldColor": "Color", + "fieldAvgConsultation": "Average consultation duration", + "fieldMaxQueue": "Max queue size", + "fieldQrRotation": "QR rotation (anti-cheat)", + "placeholderName": "e.g. Dr. Smith Practice", + "placeholderAddress": "e.g. 12 Main Street, London", + "placeholderPhone": "+44 20 1234 5678", + "qrDisabled": "Disabled", + "cancel": "Cancel", + "save": "Save", + "create": "Create", + "qrDialogTitle": "Practice QR code", + "qrDialogDescription": "Display this QR at reception. Patients scan it to join the queue.", + "posterA4": "A4 poster", + "closeButton": "Close", + "deleteDialogTitle": "Delete this practice?", + "deleteDialogDescription": "This action cannot be undone. The entire queue and history will be lost.", + "deletePermanently": "Delete permanently", + "qrAlt": "QR code", + "expiresOn": "Expires on", + "regenerate": "Regenerate", + "toastCreated": "Practice created!", + "toastUpdated": "Practice updated", + "toastDeleted": "Practice deleted", + "toastQrRegenerated": "New QR code generated", + "errorNameRequired": "Practice name is required (≥ 2 characters)." + }, + "ticket": { + "metaTitle": "Ticket — QueueMed", + "metaDescription": "Printable waiting queue ticket.", + "notFound": "Ticket not found", + "notFoundDesc": "This ticket doesn't exist or has been deleted.", + "printTicket": "Print ticket", + "tipLabel": "Tip", + "tipText": "print on A6 paper or fold in half. Hand this ticket to the patient; they can follow their turn on the display screen.", + "subtitle": "Waiting queue ticket", + "yourNumber": "Your number", + "position": "Position", + "wait": "Wait", + "minShort": "min", + "howItWorks": "How does it work?", + "howItWorksDesc": "Watch the display screen in the room. When your number appears, please go to the consultation room immediately.", + "issuedAt": "Issued on {{date}} at {{time}}" + }, + "history": { + "metaTitle": "Consultation history — QueueMed", + "metaDescription": "View the full history and statistics of consultations for your medical practice.", + "title": "Consultation history", + "subtitle": "View the full history of your patients", + "selectClinic": "Select a practice", + "totalConsultations": "Total consultations", + "avgDuration": "Average duration", + "perConsultation": "per consultation", + "presenceRate": "Attendance rate", + "patientsPresent": "patients present", + "topReason": "Top reason", + "consultationsCount": "{{count}} consultations", + "reasonsBreakdown": "Reason breakdown", + "statsRange": "Stats period", + "range7": "7 days", + "range30": "30 days", + "range90": "90 days", + "range365": "1 year", + "from": "From", + "to": "To", + "reason": "Reason", + "allReasons": "All reasons", + "clear": "Clear", + "noResults": "No consultations found", + "tryEditingFilters": "Try editing your filters", + "colTicket": "Ticket", + "colPatient": "Patient", + "colReason": "Reason", + "colDate": "Date", + "colWait": "Wait", + "colDuration": "Duration", + "colStatus": "Status", + "anonymous": "Anonymous", + "minShort": "min", + "pageInfo": "Page {{page}} of {{totalPages}} — {{total}} results", + "previousPage": "Previous page", + "nextPage": "Next page", + "reasonConsultation": "Consultation", + "reasonUrgence": "Urgent", + "reasonCertificatScolaire": "School certificate", + "reasonCertificatSportif": "Sports certificate", + "reasonArretTravail": "Sick leave", + "reasonAdministratif": "Administrative", + "reasonAutre": "Other", + "statusDone": "Completed", + "statusAbsent": "No-show", + "statusCanceled": "Cancelled" + }, + "subscriptionBlocked": { + "metaTitle": "Subscription expired — QueueMed", + "metaDescription": "Your QueueMed trial has ended. Choose a plan to continue.", + "title": "Subscription expired", + "description": "Your free trial has ended. Choose a subscription to continue using the waiting room.", + "cta": "Choose a subscription", + "features": { + "0": "Unlimited queue", + "1": "Anti-cheat QR code", + "2": "Real-time tracking", + "3": "Advanced analytics" + } + }, + "qrPoster": { + "metaTitle": "QR poster — QueueMed", + "metaDescription": "Print your QueueMed QR code poster for the waiting room.", + "notFoundTitle": "Practice not found", + "notFoundBody": "This practice doesn't exist or doesn't belong to you.", + "backToClinics": "Back to practices", + "backToManagement": "Back to management", + "refresh": "Refresh", + "printPoster": "Print poster", + "tipsTitle": "Printing tips:", + "tipsBody": "use A4 paper, in colour if possible. Laminate the poster and place it at eye level at the entrance of the practice.", + "tagline": "Virtual waiting room", + "scanToJoin": "Scan to join the queue", + "followInRealTime": "Track your position in real time on your phone.", + "qrAlt": "Queue QR code", + "qrUnavailable": "QR code unavailable", + "noAppTitle": "No app to install", + "noAppBody": "Works in your browser. Free for patients.", + "noSmartphoneNote": "No smartphone? Ask for a printed ticket at reception.", + "poweredBy": "Powered by QueueMed", + "steps": { + "scan": { + "title": "Scan", + "desc": "Point your camera at the QR code" + }, + "join": { + "title": "Join", + "desc": "Tap the link and enter the queue" + }, + "wait": { + "title": "Wait", + "desc": "You'll be alerted when your turn is near" + } + } } } diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index 1e3be4b..362a98f 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -35,7 +35,146 @@ "heroSubtitle": "QueueMed digitalise votre file d'attente. Vos patients scannent un QR code et suivent leur tour en temps réel, sans application à installer.", "heroCtaPrimary": "Démarrer gratuitement", "heroCtaSecondary": "Se connecter", - "trustedBy": "Plus de 200 cabinets nous font confiance" + "trustedBy": "Plus de 200 cabinets nous font confiance", + "nav": { + "features": "Fonctionnalités", + "how": "Fonctionnement", + "pricing": "Tarifs", + "help": "Aide", + "login": "Connexion", + "freeTrial": "Essai gratuit" + }, + "heroBadge": "Salle d'attente virtuelle nouvelle génération", + "heroH1Part1": "Vos patients", + "heroH1Part2": "n'attendent plus,", + "heroH1Part3": "ils", + "heroH1Accent": "vivent", + "heroDescription": "QueueMed transforme votre cabinet médical en une expérience fluide. QR code, suivi en temps réel, notifications, écran d'affichage — sans application à installer.", + "heroStartTrial": "Démarrer l'essai gratuit (30j)", + "heroSeeHow": "Voir comment ça marche", + "heroCheck1": "Aucune carte bancaire", + "heroCheck2": "Setup en 2 minutes", + "heroCheck3": "Données en France", + "mockCurrent": "Patient en cours", + "mockRoom": "Salle 2 — Dr. Martin", + "mockUpcoming": "Prochains", + "mockAnonymous": "Patient anonyme", + "mockMin": "min", + "featuresKicker": "Fonctionnalités", + "featuresTitlePart1": "Tout ce dont votre cabinet", + "featuresTitleAccent": "a besoin", + "featuresSubtitle": "Une plateforme pensée par et pour les médecins. Élégante, rapide, conforme.", + "features": { + "qrCode": { + "title": "QR code rotatif", + "description": "Vos patients scannent un QR à l'entrée — token tournant anti-triche, aucune appli à installer." + }, + "realtime": { + "title": "Position en temps réel", + "description": "Chaque patient voit sa position et son temps d'attente estimé, mis à jour en direct via WebSocket." + }, + "alerts": { + "title": "Alertes intelligentes", + "description": "Notification push quand le tour approche — vos patients peuvent quitter la salle d'attente." + }, + "displayScreen": { + "title": "Écran de salle", + "description": "Affichage plein écran sur tablette avec ticker, numéro appelé géant, file en direct." + }, + "stats": { + "title": "Statistiques précises", + "description": "Affluence par heure, jour, durée moyenne. Recommandations IA pour optimiser votre cabinet." + }, + "gdpr": { + "title": "RGPD & souverain", + "description": "Données hébergées en France. Aucun tracking patient. Sécurité bancaire (TLS, JWT, bcrypt)." + } + }, + "howKicker": "Comment ça marche", + "howTitleAccent": "3 étapes", + "howTitleRest": "et c'est lancé", + "steps": { + "step1": { + "title": "Configurez votre cabinet", + "desc": "2 minutes pour créer votre file. Imprimez le QR code et placez-le à l'accueil." + }, + "step2": { + "title": "Vos patients scannent", + "desc": "Ils ouvrent l'appareil photo, scannent et rejoignent la file en un clic." + }, + "step3": { + "title": "Vous appelez le suivant", + "desc": "Un clic depuis votre tableau, le patient est notifié, l'écran s'actualise." + } + }, + "pricingKicker": "Tarifs", + "pricingTitlePart1": "Simples et", + "pricingTitleAccent": "transparents", + "pricingSubtitle": "30 jours d'essai gratuit, sans engagement, sans carte bancaire.", + "pricingPopular": "Populaire", + "pricing": { + "trial": { + "name": "Essai", + "price": "Gratuit", + "period": "30 jours", + "description": "Toutes les fonctionnalités, sans carte bancaire.", + "feature1": "1 cabinet", + "feature2": "Patients illimités", + "feature3": "Statistiques de base", + "feature4": "Support email", + "cta": "Démarrer l'essai" + }, + "basic": { + "name": "Basic", + "price": "29€", + "period": "/ mois", + "description": "Pour un cabinet individuel.", + "feature1": "1 cabinet", + "feature2": "Patients illimités", + "feature3": "Écran d'affichage", + "feature4": "Statistiques avancées", + "feature5": "Support prioritaire", + "cta": "S'abonner" + }, + "pro": { + "name": "Pro", + "price": "79€", + "period": "/ mois", + "description": "Pour les centres médicaux.", + "feature1": "Cabinets illimités", + "feature2": "Multi-praticiens", + "feature3": "Recommandations IA", + "feature4": "Export CSV avancé", + "feature5": "Support téléphonique", + "cta": "S'abonner" + } + }, + "testimonialsKicker": "Témoignages", + "testimonialsTitlePart1": "Approuvé par", + "testimonialsTitleAccent": "200+ médecins", + "testimonials": { + "t1": { + "name": "Dr. Marie Dubois", + "role": "Médecin généraliste, Lyon", + "quote": "Mes patients adorent. Plus de salle d'attente bondée, plus de stress. Je gagne 1h par jour facilement." + }, + "t2": { + "name": "Dr. Karim Benali", + "role": "Pédiatre, Marseille", + "quote": "Setup en 5 minutes. Le QR rotatif évite les abus et l'écran d'affichage est parfait pour ma salle." + }, + "t3": { + "name": "Dr. Sophie Lefèvre", + "role": "Dentiste, Bordeaux", + "quote": "Les statistiques m'ont permis d'optimiser mes plages horaires. ROI évident dès le premier mois." + } + }, + "ctaTitle": "Prêt à transformer votre cabinet ?", + "ctaSubtitle": "30 jours d'essai gratuit. Aucune carte bancaire. Setup en 2 minutes.", + "ctaButton": "Démarrer maintenant", + "footerTagline": "Salle d'attente virtuelle", + "footerHelp": "Aide", + "footerContact": "Contact" }, "login": { "metaTitle": "Connexion — QueueMed", @@ -106,7 +245,50 @@ "kpiPatients": "Patients aujourd'hui", "kpiAvgWait": "Attente moyenne", "noClinic": "Vous n'avez pas encore de cabinet.", - "createClinic": "Créer un cabinet" + "createClinic": "Créer un cabinet", + "metaDescription": "Vue d'ensemble de vos cabinets, KPIs et accès rapides QueueMed.", + "fallbackDoctor": "Docteur", + "hello": "Bonjour", + "dayStarts": "Votre journée commence ici.", + "trialDaysLeft_one": "Essai gratuit — {{count}} jour restant", + "trialDaysLeft_other": "Essai gratuit — {{count}} jours restants", + "trialExpired": "Essai expiré", + "subscribe": "S'abonner", + "subscriptionExpired": "Abonnement expiré", + "renew": "Renouveler", + "kpiActiveClinics": "Cabinets actifs", + "kpiPatients7d": "Patients (7j)", + "kpiAvgWaitShort": "Attente moy.", + "kpiPlan": "Plan", + "minutesShort": "min", + "yourClinics": "Vos cabinets", + "manage": "Gérer", + "welcomeTitle": "Bienvenue sur QueueMed !", + "welcomeSubtitle": "Configurez votre premier cabinet en 2 minutes avec notre assistant.", + "startSetup": "Démarrer la configuration", + "createManually": "Créer manuellement", + "statusOpen": "Ouvert", + "statusClosed": "Fermé", + "minPerPatient": "min/patient", + "quickAccess": "Accès rapide", + "quick": { + "analytics": { + "label": "Analytics", + "desc": "Statistiques & IA" + }, + "subscription": { + "label": "Abonnement", + "desc": "Gérer votre plan" + }, + "display": { + "label": "Affichage", + "desc": "Écran salle d'attente" + }, + "help": { + "label": "Aide", + "desc": "Centre d'aide & FAQ" + } + } }, "queue": { "metaTitle": "Gestion file d'attente — QueueMed", @@ -120,7 +302,49 @@ "waiting": "En attente", "called": "Appelé", "inConsultation": "En consultation", - "addPrintedTicket": "Ajouter un ticket imprimé" + "addPrintedTicket": "Ajouter un ticket imprimé", + "metaDescription": "Gérez la file d'attente de votre cabinet en temps réel.", + "openShort": "Ouvrir", + "closeShort": "Fermer", + "clinicNotFound": "Cabinet introuvable.", + "displayScreen": "Écran d'affichage", + "headerCounts": "{{waiting}} en attente · {{called}} appelé(s)", + "actions": "Actions", + "printTicket": "Imprimer un ticket", + "resetQueue": "Réinitialiser la file", + "resetConfirm": "Êtes-vous sûr ? Toute la file sera effacée.", + "resetYes": "Oui, réinitialiser", + "qrCode": "QR Code", + "qrAlt": "QR code du cabinet", + "qrExpires": "Expire", + "qrRenew": "Renouveler", + "qrPoster": "Affiche", + "statsTitle": "Statistiques", + "statsAvgConsult": "Cons. moy.", + "statsAvgConsultValue": "~{{minutes}} min", + "queueListTitle": "File d'attente", + "patientCount": "{{count}} patient(s)", + "openToWelcome": "Ouvrez la file pour commencer à accueillir des patients.", + "patientFallback": "Patient #{{number}}", + "printed": "Imprimé", + "posShort": "Pos.", + "minShort": "min", + "callThisPatient": "Appeler ce patient", + "endConsultation": "Terminer la consultation", + "markAbsentTitle": "Marquer absent", + "statusWaiting": "En attente", + "statusCalled": "Appelé", + "statusInConsultation": "En consult.", + "statusDone": "Terminé", + "statusAbsent": "Absent", + "statusCanceled": "Annulé", + "toastTicketCalled": "Ticket #{{number}} appelé", + "toastPatientAbsent": "Patient marqué absent", + "toastConsultDone": "Consultation terminée", + "toastPatientCalled": "Patient appelé", + "toastQueueReset": "File réinitialisée", + "toastTicketCreated": "Ticket #{{number}} créé", + "toastQrRegenerated": "QR régénéré" }, "patient": { "metaTitle": "Ma place — QueueMed", @@ -132,17 +356,63 @@ "phone": "Téléphone", "whatsappOptional": "WhatsApp (optionnel)", "joining": "Inscription en cours…", - "youAreCalled": "C'est à vous !", + "youAreCalled": "C'est votre tour", "pleaseGoTo": "Présentez-vous à l'accueil", "leaveQueue": "Quitter la file", - "thanksForVisit": "Merci de votre visite" + "thanksForVisit": "Merci de votre visite.", + "metaDescription": "Suivez votre position dans la file d'attente en temps réel.", + "minutesFull": "minutes", + "calledDesc": "Présentez-vous immédiatement à la salle de consultation.", + "inConsult": "En consultation", + "inConsultDesc": "Vous êtes actuellement avec votre médecin.", + "consultDone": "Consultation terminée", + "seeYouSoon": "À bientôt !", + "ticketClosed": "Ticket clos", + "markedAbsentDesc": "Vous avez été marqué absent. Rescannez le QR à l'accueil pour rejoindre à nouveau.", + "ticketCanceledDesc": "Votre ticket a été annulé.", + "ticketNotFound": "Ticket introuvable", + "ticketNotFoundDesc": "Ce lien est invalide ou a expiré. Veuillez rescanner le QR code à l'accueil.", + "yourTicket": "Votre ticket", + "anonymousPatient": "Patient anonyme", + "position": "Position", + "outOf": "sur {{count}}", + "wait": "Attente", + "currentPatient": "Patient en cours", + "keepPageOpen": "Gardez cette page ouverte. Vous serez notifié quand votre tour approche.", + "cancelConfirm": "Annuler votre ticket ? Vous devrez rescanner le QR pour revenir.", + "cancelMyTicket": "Annuler mon ticket", + "joinedAt": "Rejoint à {{time}}", + "refresh": "Actualiser", + "notifTitle": "C'est votre tour !", + "notifBody": "Présentez-vous en salle de consultation.", + "toastApproaching": "Vous êtes le prochain — préparez-vous !", + "toastTicketCanceled": "Ticket annulé" }, "display": { "metaTitle": "Écran d'affichage — QueueMed", "nowCalling": "On appelle", "ticket": "Ticket", "waiting": "en attente", - "queueClosed": "File fermée" + "queueClosed": "File fermée", + "metaDescription": "Affichage en temps réel de la file d'attente du cabinet.", + "clinicNotFound": "Cabinet introuvable", + "clinicNotFoundDesc": "Vérifiez l'URL ou contactez le médecin.", + "brandTagline": "QueueMed — File en direct", + "live": "En direct", + "reconnecting": "Reconnexion...", + "patientCalled": "Patient appelé", + "consultationRoom": "Salle de consultation", + "noPatientCalled": "Aucun patient appelé", + "upcoming": "Prochains", + "waitingCount": "{{count}} en attente", + "statusOpen": "OUVERT", + "statusClosed": "FERMÉ", + "noWaiting": "Aucun patient en attente", + "anonymousPatient": "Patient anonyme", + "minShort": "min", + "position": "Position", + "nextLabel": "SUIVANT", + "ticker": "✨ Bienvenue au {{clinic}} — Scannez le QR code à l'accueil pour rejoindre la file en ligne — Suivez votre position en temps réel sur votre téléphone — Vous serez notifié quand votre tour approche" }, "analytics": { "metaTitle": "Analytics — QueueMed", @@ -155,7 +425,43 @@ "byHour": "Par heure", "byDay": "Par jour", "exportCsv": "Exporter CSV", - "recommendations": "Recommandations IA" + "recommendations": "Recommandations IA", + "metaDescription": "Statistiques d'affluence, temps d'attente et recommandations IA pour votre cabinet médical.", + "headerSubtitle": "Affluence, temps d'attente et recommandations IA.", + "recommendationsSubtitle": "Optimisations identifiées sur la période sélectionnée.", + "period": "Période", + "clinic": "Cabinet", + "allClinics": "Tous", + "daysLabel": "{{count}} jours", + "hourSuffix": "h", + "minShort": "min", + "kpiJoined": "Patients joints", + "kpiServed": "Servis", + "kpiAbsent": "Absents", + "kpiAvgWait": "Attente moy.", + "kpiAvgConsultation": "Cons. moy.", + "flowJoined": "Joints", + "flowServed": "Servis", + "flowAbsent": "Absents", + "chartByHour": "Affluence par heure", + "chartByDay": "Affluence par jour", + "chartFlow": "Flux patients", + "chartAvgWait": "Temps d'attente moyen", + "peakHour": "Pic d'affluence :", + "peakDay": "Jour le plus chargé :", + "minutesOnAverage": "minutes en moyenne", + "consultationLabel": "Consultation", + "totalLabel": "Total", + "daySun": "Dim", + "dayMon": "Lun", + "dayTue": "Mar", + "dayWed": "Mer", + "dayThu": "Jeu", + "dayFri": "Ven", + "daySat": "Sam", + "toastNoClinic": "Aucun cabinet sélectionné", + "toastExportFailed": "Export échoué", + "toastExportSuccess": "CSV exporté" }, "clinicSettings": { "metaTitle": "Paramètres cabinet — QueueMed", @@ -163,7 +469,38 @@ "general": "Général", "openingHours": "Horaires d'ouverture", "whatsapp": "WhatsApp", - "save": "Enregistrer" + "save": "Enregistrer", + "metaDescription": "Personnalisez l'expérience patient et la gestion de la file d'attente de votre cabinet.", + "subtitle": "Personnalisez l'expérience patient et la gestion de la file", + "openingHoursHelp": "Affichés aux patients sur la page de la file d'attente.", + "saveButton": "Sauvegarder les paramètres", + "selectClinic": "Sélectionner un cabinet", + "welcomeMessage": "Message de bienvenue", + "welcomeMessageHelp": "Affiché aux patients lorsqu'ils rejoignent la file d'attente. Laissez vide pour ne pas afficher de message.", + "welcomeMessagePlaceholder": "Ex: Bienvenue au cabinet du Dr Martin. Merci de patienter, nous vous appellerons dès que possible.", + "patientLanguage": "Langue de l'interface patient", + "patientLanguageHelp": "Langue affichée sur l'écran du patient et dans les messages WhatsApp.", + "queueSettings": "Paramètres de la file d'attente", + "avgConsultation": "Durée moy. consultation", + "maxQueueSize": "Taille max. file", + "autoAbsentTimer": "Timer absent auto", + "patients": "patients", + "minShort": "min", + "open": "Ouvert", + "closed": "Fermé", + "openingTime": "Heure d'ouverture", + "closingTime": "Heure de fermeture", + "timeSeparator": "à", + "autoAbsentDisabled": "Désactivé — le médecin marque manuellement les absents", + "autoAbsentEnabled": "Le patient est marqué absent après {{minutes}} min sans réponse", + "dayMon": "Lundi", + "dayTue": "Mardi", + "dayWed": "Mercredi", + "dayThu": "Jeudi", + "dayFri": "Vendredi", + "daySat": "Samedi", + "daySun": "Dimanche", + "toastSaved": "Paramètres sauvegardés" }, "whatsapp": { "metaTitle": "WhatsApp — QueueMed", @@ -172,7 +509,49 @@ "disconnect": "Déconnecter", "scanQr": "Scannez ce QR code avec WhatsApp", "connected": "Connecté", - "disconnected": "Déconnecté" + "disconnected": "Déconnecté", + "metaDescription": "Connectez WhatsApp à votre cabinet pour envoyer des notifications automatiques à vos patients.", + "headerTitle": "Notifications WhatsApp", + "headerSubtitle": "Connectez WhatsApp pour envoyer des alertes automatiques à vos patients", + "statusDisconnected": "Déconnecté", + "statusConnecting": "Connexion en cours…", + "statusQrReady": "En attente du scan", + "statusConnected": "Connecté", + "disclaimerNote": "Note :", + "disclaimerBody": "Cette fonctionnalité utilise WhatsApp Web (protocole non officiel). Limitez l'envoi à moins de 500 messages/jour pour éviter tout risque de restriction. Un numéro WhatsApp personnel ou professionnel est requis.", + "clinic": "Cabinet", + "connectionStatus": "Statut de la connexion", + "qrAltText": "QR Code WhatsApp", + "howToScan": "Comment scanner", + "scanStep1": "1. Ouvrez WhatsApp sur votre téléphone", + "scanStep2": "2. Appuyez sur ⋮ → Appareils liés", + "scanStep3": "3. Appuyez sur Lier un appareil", + "scanStep4": "4. Scannez ce QR code", + "refreshStatus": "Actualiser le statut", + "connectedTitle": "WhatsApp connecté", + "connectedBody": "Les notifications automatiques sont actives pour ce cabinet.", + "notConnectedTitle": "Non connecté", + "notConnectedBody": "Cliquez sur « Connecter » pour générer un QR code à scanner.", + "newQr": "Nouveau QR code", + "cancel": "Annuler", + "testMessage": "Message de test", + "testMessageHelp": "Envoyez un message de test pour vérifier que la connexion fonctionne.", + "testPhonePlaceholder": "Numéro international (ex: 33612345678)", + "testPhoneLabel": "Numéro de téléphone", + "send": "Envoyer", + "phoneFormatHint": "Entrez le numéro sans le + (ex: 33612345678 pour +33 6 12 34 56 78)", + "howItWorks": "Comment ça fonctionne", + "step1Title": "Inscription", + "step1Desc": "Le patient entre son numéro WhatsApp lors de l'inscription via QR code", + "step2Title": "Alerte bientôt", + "step2Desc": "Quand il reste 2 patients avant lui, il reçoit un message d'alerte", + "step3Title": "C'est son tour", + "step3Desc": "Quand le médecin l'appelle, il reçoit immédiatement un message", + "toastQrGenerated": "QR code généré — scannez avec WhatsApp", + "toastConnected": "WhatsApp connecté !", + "toastDisconnected": "Session WhatsApp déconnectée", + "toastTestSent": "Message test envoyé !", + "toastTestFailed": "Échec : {{error}}" }, "onboarding": { "metaTitle": "Bienvenue — QueueMed", @@ -180,7 +559,59 @@ "step1": "Créez votre cabinet", "step2": "Imprimez votre QR code", "step3": "Ouvrez la file d'attente", - "finish": "Terminer" + "finish": "Terminer", + "metaDescription": "Configurez votre premier cabinet QueueMed en 2 minutes.", + "headerPart1": "Configuration", + "headerAccent": "initiale", + "headerSubtitle": "Configurez votre premier cabinet en 2 minutes.", + "steps": { + "s1": { + "title": "Votre cabinet", + "description": "Renseignez les informations de base et les paramètres de la file." + }, + "s2": { + "title": "Votre QR code", + "description": "Imprimez ou prévisualisez l'affiche à apposer à l'accueil." + }, + "s3": { + "title": "Tout est prêt !", + "description": "Voici les prochaines étapes pour démarrer." + } + }, + "fieldName": "Nom du cabinet", + "fieldAddress": "Adresse", + "fieldPhone": "Téléphone", + "optional": "(optionnel)", + "placeholderName": "Ex: Cabinet Dr. Martin", + "placeholderAddress": "12 rue de la Paix, Paris", + "placeholderPhone": "01 23 45 67 89", + "queueSettings": "Paramètres de la file", + "avgConsultation": "Durée moyenne de consultation", + "avgConsultationHelp": "Utilisé pour estimer le temps d'attente des patients.", + "maxQueueSize": "Taille maximale de la file", + "maxQueueHelp": "Au-delà, les nouveaux patients ne pourront plus rejoindre.", + "minutesShort": "min", + "patientsShort": "pat.", + "qrIntroPart1": "Voici le QR code de", + "qrIntroPart2": ". Imprimez-le et placez-le à l'entrée du cabinet pour que vos patients puissent rejoindre la file.", + "qrAlt": "QR Code", + "qrUnavailable": "QR indisponible", + "viewPoster": "Voir / imprimer l'affiche", + "continue": "Continuer", + "doneTitle": "Cabinet configuré !", + "donePart1": "Le cabinet", + "donePart2": "est prêt. Voici vos prochaines étapes pour bien démarrer.", + "next1": "Imprimez le QR code et affichez-le à l'accueil", + "next2": "Configurez l'écran d'affichage sur votre tablette ou moniteur", + "next3": "Ouvrez la file depuis le tableau de bord en début de journée", + "creating": "Création...", + "createClinic": "Créer le cabinet", + "back": "Retour", + "viewQueue": "Voir la file", + "dashboard": "Tableau de bord", + "skip": "Passer pour l'instant", + "toastCreated": "Cabinet créé avec succès !", + "errorNameRequired": "Le nom du cabinet est requis." }, "subscription": { "metaTitle": "Abonnement — QueueMed", @@ -189,11 +620,357 @@ "active": "Actif", "expired": "Expiré", "daysLeft": "{{days}} jours restants", - "upgrade": "Passer au plan payant" + "upgrade": "Passer au plan payant", + "metaDescription": "Gérez votre abonnement QueueMed et votre période d'essai.", + "subtitle": "Gérez votre plan et votre période d'essai.", + "daysCount_one": "{{count}} jour", + "daysCount_other": "{{count}} jours", + "currentPlan": "Plan actuel", + "currentPlanLabel": "Plan actuel", + "expiredMessage": "Votre abonnement est expiré. Renouvelez pour continuer à utiliser QueueMed.", + "freeTrial": "Essai gratuit", + "nextRenewal": "Prochain renouvellement", + "in": "dans", + "untilDate": "(jusqu'au {{date}})", + "subscribeNow": "S'abonner maintenant", + "dayN": "Jour {{day}}", + "choosePlan": "Choisissez votre plan", + "popular": "Populaire", + "current": "Actuel", + "automaticTrial": "Essai automatique", + "subscribe": "S'abonner", + "commitmentTitle": "Notre engagement", + "commitmentBody": "Annulation à tout moment. Données hébergées en France. Conformité RGPD. Migration et configuration assistées gratuites.", + "toastRedirect": "Redirection vers le paiement {{plan}}…", + "toastRedirectDescription": "L'intégration Stripe sera activée prochainement.", + "plans": { + "trial": { + "name": "Essai", + "period": "30 jours", + "description": "Découvrez QueueMed sans engagement.", + "features": { + "0": "1 cabinet", + "1": "Patients illimités", + "2": "Statistiques de base", + "3": "Support email" + } + }, + "basic": { + "name": "Basic", + "period": "/ mois", + "description": "Pour un cabinet individuel.", + "features": { + "0": "1 cabinet", + "1": "Patients illimités", + "2": "Écran d'affichage", + "3": "Statistiques avancées", + "4": "Support prioritaire" + } + }, + "pro": { + "name": "Pro", + "period": "/ mois", + "description": "Centres médicaux et multi-praticiens.", + "features": { + "0": "Cabinets illimités", + "1": "Multi-praticiens", + "2": "Recommandations IA", + "3": "Export CSV", + "4": "Support téléphonique" + } + } + } }, "help": { "metaTitle": "Aide — QueueMed", "title": "Centre d'aide", - "subtitle": "Trouvez vite la réponse à vos questions" + "subtitle": "Trouvez vite la réponse à vos questions", + "metaDescription": "Centre d'aide QueueMed : trouvez rapidement les réponses à vos questions.", + "headerCenter": "Centre", + "headerHelp": "d'aide", + "headerSubtitle": "Trouvez rapidement les réponses à vos questions sur QueueMed.", + "searchPlaceholder": "Rechercher une question...", + "noResults": "Aucune question ne correspond à votre recherche.", + "contactTitle": "Vous ne trouvez pas votre réponse ?", + "contactBody": "Notre équipe est disponible pour vous aider à configurer et utiliser QueueMed dans votre cabinet.", + "dashboardButton": "Tableau de bord", + "contactButton": "Contacter le support", + "categories": { + "all": "Tous", + "gettingStarted": "Démarrage", + "queueManagement": "Gestion de la file", + "patientExperience": "Expérience patient", + "displayScreen": "Écran d'affichage", + "subscription": "Abonnement", + "technical": "Technique" + }, + "quickLinks": { + "gettingStarted": "Démarrage", + "patients": "Patients", + "display": "Écran", + "subscription": "Abonnement" + }, + "faq": { + "createClinic": { + "q": "Comment créer mon premier cabinet ?", + "a": "Lors de votre première connexion, suivez l'assistant de configuration en cliquant sur 'Démarrer la configuration' depuis le tableau de bord. Renseignez le nom, l'adresse optionnelle et les paramètres de la file (durée de consultation moyenne, taille maximale). Un QR code unique est généré automatiquement." + }, + "printPoster": { + "q": "Comment imprimer mon affiche QR code ?", + "a": "Depuis la page de gestion d'un cabinet, cliquez sur 'Affiche QR'. La page affiche un poster A4 prêt à imprimer. Utilisez du papier couleur si possible, plastifiez l'affiche et placez-la à hauteur des yeux à l'entrée du cabinet." + }, + "setupTime": { + "q": "Combien de temps faut-il pour configurer QueueMed ?", + "a": "Environ 2 minutes : créez votre compte, configurez votre premier cabinet et imprimez le QR code. Vous pouvez accueillir vos premiers patients en moins de 5 minutes au total." + }, + "openCloseQueue": { + "q": "Comment ouvrir et fermer la file d'attente ?", + "a": "Dans la page 'Gestion de la file', sélectionnez votre cabinet et cliquez sur 'Ouvrir la file'. Les patients pourront alors rejoindre. En fin de journée, cliquez sur 'Fermer la file' puis 'Réinitialiser' pour repartir à zéro le lendemain." + }, + "callNext": { + "q": "Comment appeler le prochain patient ?", + "a": "Cliquez sur 'Appeler le suivant' dans l'interface de gestion. Le numéro s'affiche automatiquement sur l'écran d'affichage en salle d'attente et le patient reçoit une notification push sur son téléphone." + }, + "noShow": { + "q": "Que faire si un patient ne se présente pas ?", + "a": "Cliquez sur 'Absent' à côté du nom du patient. Il est retiré de la file et les positions des autres patients se mettent à jour automatiquement. Le patient devra rescanner le QR code pour rejoindre à nouveau." + }, + "reorder": { + "q": "Puis-je réorganiser l'ordre des patients ?", + "a": "Oui. Dans la liste de la file, glissez-déposez les patients pour modifier leur ordre. Les positions et temps d'attente se recalculent en direct, et chaque patient reçoit la mise à jour sur son téléphone." + }, + "printedTicket": { + "q": "Comment imprimer un ticket pour un patient sans smartphone ?", + "a": "Dans l'interface de gestion, cliquez sur 'Ajouter un patient' puis cochez 'Sans smartphone'. Un ticket imprimable s'ouvre dans un nouvel onglet avec le numéro et la position. Donnez-le au patient — il suivra son tour à l'écran d'affichage." + }, + "patientJoin": { + "q": "Comment un patient rejoint-il la file ?", + "a": "Le patient ouvre l'appareil photo de son smartphone et scanne le QR code affiché à l'accueil. Un lien s'ouvre automatiquement — il appuie dessus pour rejoindre la file. Aucune application à installer." + }, + "patientLeave": { + "q": "Le patient peut-il quitter la salle d'attente physique ?", + "a": "Oui, c'est l'avantage principal de QueueMed. Le patient garde la page ouverte sur son téléphone et peut s'éloigner. Il reçoit une notification push lorsque son tour approche. Recommandez-lui de rester à moins de 5 minutes du cabinet." + }, + "qrRotation": { + "q": "Pourquoi le QR code ne fonctionne plus parfois ?", + "a": "Le QR code se renouvelle automatiquement à intervalles réguliers (système anti-triche) pour éviter le partage frauduleux du lien hors du cabinet. Si un patient obtient une erreur, il lui suffit de rescanner le QR code à l'accueil." + }, + "notification": { + "q": "Le patient reçoit-il bien la notification ?", + "a": "Lors du premier accès, le navigateur lui demande l'autorisation des notifications. S'il accepte, il recevra une notification push + vibration quand son tour est appelé. Sinon, la page reste à jour en temps réel tant qu'elle est ouverte." + }, + "displaySetup": { + "q": "Comment configurer l'écran d'affichage ?", + "a": "Dans la fiche de votre cabinet, copiez le 'Lien écran d'affichage' (/display/:clinicId). Ouvrez ce lien sur votre tablette ou moniteur de salle d'attente, puis activez le mode plein écran (F11 sur PC). L'écran se met à jour automatiquement via WebSocket." + }, + "displayHardware": { + "q": "Quel matériel utiliser pour l'écran ?", + "a": "N'importe quelle tablette, moniteur ou TV connectée à internet et dotée d'un navigateur moderne (Chrome, Safari, Edge). Une simple tablette Android à 80 € fait parfaitement l'affaire." + }, + "internetOutage": { + "q": "Que se passe-t-il en cas de coupure internet ?", + "a": "L'écran d'affichage affiche un indicateur 'Reconnexion...' en orange. Les patients déjà dans la file conservent leur position. Dès que la connexion est rétablie, la synchronisation reprend automatiquement." + }, + "trialDuration": { + "q": "Combien de temps dure l'essai gratuit ?", + "a": "L'essai gratuit dure 30 jours à compter de votre première connexion. Toutes les fonctionnalités sont disponibles sans restriction pendant cette période, et vous pouvez créer plusieurs cabinets et accueillir un nombre illimité de patients." + }, + "afterTrial": { + "q": "Que se passe-t-il après l'essai gratuit ?", + "a": "L'accès aux fonctionnalités de gestion (ouvrir la file, appeler des patients, créer des cabinets) est bloqué jusqu'à la souscription d'un plan payant. Vos données sont conservées et les patients peuvent toujours voir leur position dans les files actives." + }, + "cancelSub": { + "q": "Puis-je annuler mon abonnement ?", + "a": "Oui, vous pouvez annuler à tout moment depuis la page 'Abonnement' de votre tableau de bord. L'accès aux fonctionnalités payantes reste actif jusqu'à la fin de la période déjà payée." + }, + "clinicCount": { + "q": "Combien de cabinets puis-je gérer ?", + "a": "Le plan Solo inclut 1 cabinet. Le plan Pro permet de créer jusqu'à 5 cabinets. Le plan Cabinet inclut un nombre illimité de cabinets et donne accès à des statistiques avancées avec recommandations IA." + }, + "devices": { + "q": "Sur quels appareils fonctionne QueueMed ?", + "a": "QueueMed fonctionne sur tous les appareils dotés d'un navigateur moderne : smartphones iOS et Android, tablettes, ordinateurs Windows / Mac / Linux. Aucune application à installer. Recommandé : Chrome ou Safari à jour." + }, + "dataSecurity": { + "q": "Mes données patients sont-elles sécurisées ?", + "a": "Oui. Les noms et numéros de ticket sont chiffrés en transit (HTTPS) et stockés sur des serveurs hébergés en France. Aucune donnée médicale n'est collectée. Les patients sont identifiés uniquement par un nom optionnel." + }, + "exportStats": { + "q": "Puis-je exporter mes statistiques ?", + "a": "Oui. Depuis la page 'Analytics', cliquez sur 'Exporter en CSV' pour télécharger l'historique complet des consultations. Le fichier inclut les heures, durées d'attente et durées de consultation pour chaque patient." + }, + "offline": { + "q": "QueueMed fonctionne-t-il hors ligne ?", + "a": "Non, une connexion internet est nécessaire pour la synchronisation en temps réel entre le médecin, l'écran d'affichage et les patients. En cas de coupure, l'application reprend automatiquement dès le retour de la connexion." + } + } + }, + "clinics": { + "metaTitle": "Mes cabinets — QueueMed", + "metaDescription": "Gérez vos cabinets, leurs QR codes et leurs paramètres.", + "title": "Mes cabinets", + "subtitle": "Gérez vos cabinets, leurs QR codes et leurs paramètres.", + "newClinic": "Nouveau cabinet", + "emptyTitle": "Aucun cabinet pour le moment", + "emptySubtitle": "Créez votre premier cabinet pour démarrer.", + "createClinic": "Créer un cabinet", + "statusOpen": "Ouvert", + "statusClosed": "Fermé", + "statCons": "Cons.", + "statMax": "Max", + "statQrRot": "QR rot.", + "minutesShort": "min", + "manageQueue": "Gérer la file", + "open": "Ouvrir", + "close": "Fermer", + "qr": "QR", + "screen": "Écran", + "editAction": "Éditer", + "dialogEditTitle": "Modifier le cabinet", + "dialogCreateTitle": "Nouveau cabinet", + "dialogDescription": "Configurez les informations et paramètres de la salle d'attente.", + "fieldName": "Nom du cabinet", + "fieldAddress": "Adresse", + "fieldPhone": "Téléphone", + "fieldColor": "Couleur", + "fieldAvgConsultation": "Durée moyenne consultation", + "fieldMaxQueue": "Taille max file", + "fieldQrRotation": "Rotation QR (anti-triche)", + "placeholderName": "Ex: Cabinet Dr. Martin", + "placeholderAddress": "Ex: 12 rue de la Paix, Paris", + "placeholderPhone": "01 23 45 67 89", + "qrDisabled": "Désactivé", + "cancel": "Annuler", + "save": "Enregistrer", + "create": "Créer", + "qrDialogTitle": "QR Code du cabinet", + "qrDialogDescription": "Affichez ce QR à l'accueil. Vos patients le scannent pour rejoindre la file.", + "posterA4": "Affiche A4", + "closeButton": "Fermer", + "deleteDialogTitle": "Supprimer ce cabinet ?", + "deleteDialogDescription": "Cette action est irréversible. Toute la file et l'historique seront perdus.", + "deletePermanently": "Supprimer définitivement", + "qrAlt": "QR code", + "expiresOn": "Expire le", + "regenerate": "Régénérer", + "toastCreated": "Cabinet créé !", + "toastUpdated": "Cabinet mis à jour", + "toastDeleted": "Cabinet supprimé", + "toastQrRegenerated": "Nouveau QR code généré", + "errorNameRequired": "Le nom du cabinet est requis (≥ 2 caractères)." + }, + "ticket": { + "metaTitle": "Ticket — QueueMed", + "metaDescription": "Ticket imprimable de la file d'attente.", + "notFound": "Ticket introuvable", + "notFoundDesc": "Ce ticket n'existe pas ou a été supprimé.", + "printTicket": "Imprimer le ticket", + "tipLabel": "Conseil", + "tipText": "imprimez sur du papier A6 ou pliez en deux. Donnez ce ticket au patient ; il pourra suivre son tour à l'écran d'affichage.", + "subtitle": "Ticket de file d'attente", + "yourNumber": "Votre numéro", + "position": "Position", + "wait": "Attente", + "minShort": "min", + "howItWorks": "Comment ça marche ?", + "howItWorksDesc": "Surveillez l'écran d'affichage en salle. Lorsque votre numéro s'affiche, présentez-vous immédiatement à la salle de consultation.", + "issuedAt": "Émis le {{date}} à {{time}}" + }, + "history": { + "metaTitle": "Historique des consultations — QueueMed", + "metaDescription": "Consultez l'historique complet et les statistiques des consultations de votre cabinet médical.", + "title": "Historique des consultations", + "subtitle": "Consultez l'historique complet de vos patients", + "selectClinic": "Sélectionner un cabinet", + "totalConsultations": "Total consultations", + "avgDuration": "Durée moyenne", + "perConsultation": "par consultation", + "presenceRate": "Taux de présence", + "patientsPresent": "patients présents", + "topReason": "Top motif", + "consultationsCount": "{{count}} consultations", + "reasonsBreakdown": "Répartition des motifs", + "statsRange": "Période des statistiques", + "range7": "7 jours", + "range30": "30 jours", + "range90": "90 jours", + "range365": "1 an", + "from": "Du", + "to": "Au", + "reason": "Motif", + "allReasons": "Tous les motifs", + "clear": "Effacer", + "noResults": "Aucune consultation trouvée", + "tryEditingFilters": "Essayez de modifier vos filtres", + "colTicket": "Ticket", + "colPatient": "Patient", + "colReason": "Motif", + "colDate": "Date", + "colWait": "Attente", + "colDuration": "Durée", + "colStatus": "Statut", + "anonymous": "Anonyme", + "minShort": "min", + "pageInfo": "Page {{page}} sur {{totalPages}} — {{total}} résultats", + "previousPage": "Page précédente", + "nextPage": "Page suivante", + "reasonConsultation": "Consultation", + "reasonUrgence": "Urgence", + "reasonCertificatScolaire": "Certificat scolaire", + "reasonCertificatSportif": "Certificat sportif", + "reasonArretTravail": "Arrêt de travail", + "reasonAdministratif": "Administratif", + "reasonAutre": "Autre", + "statusDone": "Terminé", + "statusAbsent": "Absent", + "statusCanceled": "Annulé" + }, + "subscriptionBlocked": { + "metaTitle": "Abonnement expiré — QueueMed", + "metaDescription": "Votre période d'essai QueueMed est terminée. Choisissez un abonnement pour continuer.", + "title": "Abonnement expiré", + "description": "Votre période d'essai gratuit est terminée. Choisissez un abonnement pour continuer à utiliser Salle d'attente.", + "cta": "Choisir un abonnement", + "features": { + "0": "File d'attente illimitée", + "1": "QR code anti-triche", + "2": "Suivi temps réel", + "3": "Analytics avancés" + } + }, + "qrPoster": { + "metaTitle": "Affiche QR — QueueMed", + "metaDescription": "Imprimez votre affiche QR code QueueMed pour la salle d'attente.", + "notFoundTitle": "Cabinet introuvable", + "notFoundBody": "Ce cabinet n'existe pas ou ne vous appartient pas.", + "backToClinics": "Retour aux cabinets", + "backToManagement": "Retour à la gestion", + "refresh": "Rafraîchir", + "printPoster": "Imprimer l'affiche", + "tipsTitle": "Conseils d'impression :", + "tipsBody": "utilisez du papier A4, en couleur si possible. Plastifiez l'affiche et placez-la à hauteur des yeux à l'entrée du cabinet.", + "tagline": "Salle d'attente virtuelle", + "scanToJoin": "Scannez pour rejoindre la file", + "followInRealTime": "Suivez votre position en temps réel sur votre téléphone.", + "qrAlt": "QR Code file d'attente", + "qrUnavailable": "QR Code non disponible", + "noAppTitle": "Aucune application à installer", + "noAppBody": "Fonctionne dans votre navigateur. Gratuit pour les patients.", + "noSmartphoneNote": "Pas de smartphone ? Demandez un ticket imprimé à l'accueil.", + "poweredBy": "Propulsé par QueueMed", + "steps": { + "scan": { + "title": "Scannez", + "desc": "Pointez votre appareil photo vers le QR code" + }, + "join": { + "title": "Rejoignez", + "desc": "Appuyez sur le lien et entrez dans la file" + }, + "wait": { + "title": "Patientez", + "desc": "Vous serez alerté quand votre tour approche" + } + } } } diff --git a/client/src/pages/Analytics.tsx b/client/src/pages/Analytics.tsx index cc39a5f..c2f4098 100644 --- a/client/src/pages/Analytics.tsx +++ b/client/src/pages/Analytics.tsx @@ -1,23 +1,35 @@ import { useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; import { BarChart3, Users, Clock, Activity, Sparkles, Download, Loader2, TrendingUp, Calendar, } from "lucide-react"; import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, - LineChart, Line, AreaChart, Area, Cell, PieChart, Pie, + AreaChart, Area, Cell, PieChart, Pie, } from "recharts"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; -const DAY_NAMES = ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"]; const PIE_COLORS = ["#10b981", "#06b6d4", "#0d9488", "#22d3ee", "#34d399", "#0891b2", "#14b8a6"]; export default function Analytics() { + const { t } = useTranslation(); const [days, setDays] = useState(30); const [clinicId, setClinicId] = useState(undefined); + const DAY_NAMES = [ + t("analytics.daySun"), + t("analytics.dayMon"), + t("analytics.dayTue"), + t("analytics.dayWed"), + t("analytics.dayThu"), + t("analytics.dayFri"), + t("analytics.daySat"), + ]; + const clinicsQuery = trpc.clinic.list.useQuery(); const summaryQuery = trpc.analytics.summary.useQuery({ days, clinicId }); @@ -31,12 +43,12 @@ export default function Analytics() { const handleExport = async () => { if (!clinicId && !clinics[0]) { - toast.error("Aucun cabinet sélectionné"); + toast.error(t("analytics.toastNoClinic")); return; } const result = await exportCsv.refetch(); if (!result.data) { - toast.error("Export échoué"); + toast.error(t("analytics.toastExportFailed")); return; } const blob = new Blob([result.data.csv], { type: "text/csv;charset=utf-8;" }); @@ -48,35 +60,39 @@ export default function Analytics() { a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - toast.success("CSV exporté"); + toast.success(t("analytics.toastExportSuccess")); }; - const hourData = (summary?.byHour ?? []).map((count, hour) => ({ hour: `${hour}h`, count })); + const hourData = (summary?.byHour ?? []).map((count, hour) => ({ hour: `${hour}${t("analytics.hourSuffix")}`, count })); const dayData = (summary?.byDay ?? []).map((count, dow) => ({ day: DAY_NAMES[dow], count })); const flowData = [ - { name: "Joints", value: summary?.totalJoined ?? 0 }, - { name: "Servis", value: summary?.totalServed ?? 0 }, - { name: "Absents", value: summary?.totalAbsent ?? 0 }, + { name: t("analytics.flowJoined"), value: summary?.totalJoined ?? 0 }, + { name: t("analytics.flowServed"), value: summary?.totalServed ?? 0 }, + { name: t("analytics.flowAbsent"), value: summary?.totalAbsent ?? 0 }, ]; return (
+ + {t("analytics.metaTitle")} + +
-

Analytics

-

Affluence, temps d'attente et recommandations IA.

+

{t("analytics.title")}

+

{t("analytics.headerSubtitle")}

{/* Filters */}
- Période + {t("analytics.period")} {[7, 30, 90, 365].map((d) => ( ))}
- Cabinet + {t("analytics.clinic")}