diff --git a/languages/de.json b/languages/de.json index 593c6af..249bc3e 100644 --- a/languages/de.json +++ b/languages/de.json @@ -1525,7 +1525,15 @@ "calc.history_desc": "Ausgeführte Rechnungen werden hier für Hausaufgaben & Nachvollziehbarkeit archiviert.", "lang_modal.auto_detect_banner_title": "⚡ Automatische Übernahme von Sprachdateien & Flaggen", "lang_modal.auto_detect_banner_desc": "Sobald eine Sprachdatei (.json) im Ordner languages/ aktualisiert wird, übernimmt SOCDOF diese direkt. Eigene Flaggenbilder können im Unterordner flags/ hinterlegt werden; standardmäßig wird das Länder-Emoji oder die Flagge mit Fragezeichen verwendet.", - "lang_modal.languages_available": "verfügbar" + "lang_modal.languages_available": "verfügbar", + "therapy.integrations": "Verknüpfte Arbeitsabläufe", + "therapy.integrationsDesc": "Klienten verwenden die gemeinsame Kontaktliste. Termine verwenden den gemeinsamen Kalender. Die Abrechnung öffnet den bestehenden Rechnungsablauf.", + "therapy.openCalendar": "Kalender öffnen", + "therapy.openBilling": "Abrechnung öffnen", + "therapy.contactPicker": "Bestehenden Kontakt verwenden", + "therapy.searchContacts": "Kontakte suchen...", + "therapy.noContacts": "Keine Kontakte vorhanden.", + "therapy.appointmentPrefix": "Therapietermin" }, "therapy_practice": { "title": "Praxis & Therapie" diff --git a/languages/en.json b/languages/en.json index 728d715..d17e41b 100644 --- a/languages/en.json +++ b/languages/en.json @@ -1525,7 +1525,15 @@ "calc.history_desc": "Executed calculations are archived here for homework and reference.", "lang_modal.auto_detect_banner_title": "⚡ Automatic sync of language files & flags", "lang_modal.auto_detect_banner_desc": "As soon as a language file (.json) in the languages/ folder is updated, SOCDOF immediately loads it. Custom flag images can be placed in the flags/ subfolder; by default, the country emoji or fallback flag with a question mark is displayed.", - "lang_modal.languages_available": "available" + "lang_modal.languages_available": "available", + "therapy.integrations": "Connected workflows", + "therapy.integrationsDesc": "Clients use the shared Contacts list. Appointments use the shared Calendar. Billing opens the existing invoice workflow.", + "therapy.openCalendar": "Open calendar", + "therapy.openBilling": "Open billing", + "therapy.contactPicker": "Use an existing contact", + "therapy.searchContacts": "Search contacts...", + "therapy.noContacts": "No contacts available.", + "therapy.appointmentPrefix": "Therapy appointment" }, "therapy_practice": { "title": "Practice & Therapy" diff --git a/languages/es.json b/languages/es.json index a0dbef9..1618b66 100644 --- a/languages/es.json +++ b/languages/es.json @@ -1524,7 +1524,15 @@ "calc.history_desc": "Los cálculos se archivan aquí para tareas y seguimiento.", "lang_modal.auto_detect_banner_title": "⚡ Sincronización automática de archivos de idioma y banderas", "lang_modal.auto_detect_banner_desc": "Tan pronto como se actualice un archivo de idioma (.json) en la carpeta languages/, SOCDOF lo carga directamente. Las imágenes de banderas personalizadas se pueden colocar en la subcarpeta flags/; por defecto, se utiliza el emoji del país o la bandera de reserva con signo de interrogación.", - "lang_modal.languages_available": "disponibles" + "lang_modal.languages_available": "disponibles", + "therapy.integrations": "Flujos conectados", + "therapy.integrationsDesc": "Los clientes usan la lista de contactos compartida. Las citas usan el calendario compartido. La facturación abre el flujo de facturas existente.", + "therapy.openCalendar": "Abrir calendario", + "therapy.openBilling": "Abrir facturación", + "therapy.contactPicker": "Usar un contacto existente", + "therapy.searchContacts": "Buscar contactos...", + "therapy.noContacts": "No hay contactos disponibles.", + "therapy.appointmentPrefix": "Cita de terapia" }, "therapy_practice": { "title": "Consulta y terapia" diff --git a/languages/fr.json b/languages/fr.json index 39b6092..c81b629 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -1524,7 +1524,15 @@ "calc.history_desc": "Les calculs sont archivés ici pour les devoirs et le suivi.", "lang_modal.auto_detect_banner_title": "⚡ Synchronisation automatique des fichiers de langue et drapeaux", "lang_modal.auto_detect_banner_desc": "Dès qu'un fichier de langue (.json) du dossier languages/ est mis à jour, SOCDOF le charge directement. Les images de drapeaux peuvent être placées dans le sous-dossier flags/ ; par défaut, l'émoji du pays ou le drapeau de secours avec un point d'interrogation est utilisé.", - "lang_modal.languages_available": "disponibles" + "lang_modal.languages_available": "disponibles", + "therapy.integrations": "Flux de travail connectés", + "therapy.integrationsDesc": "Les clients utilisent la liste de contacts commune. Les rendez-vous utilisent le calendrier commun. La facturation ouvre le processus de facture existant.", + "therapy.openCalendar": "Ouvrir le calendrier", + "therapy.openBilling": "Ouvrir la facturation", + "therapy.contactPicker": "Utiliser un contact existant", + "therapy.searchContacts": "Rechercher des contacts...", + "therapy.noContacts": "Aucun contact disponible.", + "therapy.appointmentPrefix": "Rendez-vous thérapeutique" }, "therapy_practice": { "title": "Cabinet & Thérapie" diff --git a/src/components/CalendarModule.tsx b/src/components/CalendarModule.tsx index cae4712..870e0b7 100644 --- a/src/components/CalendarModule.tsx +++ b/src/components/CalendarModule.tsx @@ -213,6 +213,13 @@ export const CalendarModule: React.FC = ({ setMiniCalendarMonth(new Date(focusedDate.getFullYear(), focusedDate.getMonth(), 1)); }, [focusedDate]); + // Refresh when another module creates or removes a shared local calendar event. + useEffect(() => { + const handleCalendarUpdated = () => refreshUnifiedEvents(); + window.addEventListener('socdof:calendar-updated', handleCalendarUpdated); + return () => window.removeEventListener('socdof:calendar-updated', handleCalendarUpdated); + }, [refreshUnifiedEvents]); + // Subscribe to Auth and Sync changes useEffect(() => { const unsubAuth = subscribeToGoogleAuth((user, token) => { @@ -781,6 +788,7 @@ export const CalendarModule: React.FC = ({ }; const existing = getStoredCustomCalendarEvents(); saveStoredCustomCalendarEvents([...existing, newLocalEvent]); + window.dispatchEvent(new Event('socdof:calendar-updated')); sounds.playSuccess(); setStatusNotification({ text: `Lokaler Termin "${newEventTitle}" gespeichert.`, diff --git a/src/components/DesktopWindowWorkspace.tsx b/src/components/DesktopWindowWorkspace.tsx index d961ddf..6243aae 100644 --- a/src/components/DesktopWindowWorkspace.tsx +++ b/src/components/DesktopWindowWorkspace.tsx @@ -55,6 +55,7 @@ import { Check, Headphones, User, + Briefcase, Plus, StickyNote, Zap @@ -1648,7 +1649,7 @@ export const DesktopWindowWorkspace: React.FC = ({ purchases: { title: t('module.purchases', currentLang, 'Einkauf'), subtitle: t('desc.purchases', currentLang, 'Lieferantenbestellungen'), icon: ShoppingCart, color: 'bg-gradient-to-br from-orange-500 to-amber-600' }, calendar: { title: t('module.calendar', currentLang, 'Kalender'), subtitle: t('desc.calendar', currentLang, 'Google Live Sync & Termine'), icon: Calendar, color: 'bg-gradient-to-br from-blue-500 to-sky-600' }, calculator: { title: t('module.calculator', currentLang, 'Taschenrechner'), subtitle: t('desc.calculator', currentLang, 'Einfach & Wissenschaftlich'), icon: Calculator, color: 'bg-gradient-to-br from-emerald-500 to-teal-700' }, - therapy_practice: { title: t('module.therapy_practice', currentLang, 'Praxis'), subtitle: t('desc.therapy_practice', currentLang, 'Therapie & Beratung'), icon: User, color: 'bg-gradient-to-br from-slate-600 to-indigo-700' }, + therapy_practice: { title: t('module.therapy_practice', currentLang, 'Praxis'), subtitle: t('desc.therapy_practice', currentLang, 'Therapie & Beratung'), icon: Briefcase, color: 'bg-gradient-to-br from-slate-600 to-indigo-700' }, widgets: { title: t('module.widgets', currentLang, 'Widgets'), subtitle: t('desc.widgets', currentLang, 'Desktop-Widgets & Notizen'), icon: WidgetsIcon, color: 'bg-gradient-to-br from-violet-500 to-purple-600' }, appstore: { title: t('module.appstore', currentLang, 'App Store'), subtitle: t('desc.appstore', currentLang, 'Module verwalten'), icon: Package, color: 'bg-gradient-to-br from-fuchsia-500 to-pink-600' }, docs: { title: t('module.docs', currentLang, 'Handbuch'), subtitle: t('desc.docs', currentLang, 'Dokumentation & Hilfe'), icon: BookOpen, color: 'bg-gradient-to-br from-sky-500 to-blue-600' }, @@ -2924,7 +2925,12 @@ export const DesktopWindowWorkspace: React.FC = ({ )} {win.module === 'therapy_practice' && ( - + openWindow('calendar', t('module.calendar', currentLang, 'Kalender'))} + onOpenInvoice={(contactId) => openWindow('invoices', contactId ? `Rechnung für Kontakt` : undefined, { isCreateOpen: true, contactId })} + /> )} {win.module === 'calculator' && ( diff --git a/src/components/TherapyPracticeModule.tsx b/src/components/TherapyPracticeModule.tsx index bf42731..b6dc49d 100644 --- a/src/components/TherapyPracticeModule.tsx +++ b/src/components/TherapyPracticeModule.tsx @@ -1,6 +1,10 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { CalendarDays, Car, Check, Clock3, FileText, Plus, Search, ShieldCheck, Trash2, UserRound, X } from 'lucide-react'; +import { CalendarDays, Car, Check, Clock3, FileText, Plus, Search, Trash2, UserRound, X } from 'lucide-react'; +import { Contact, Invoice, CalendarAppEvent } from '../types'; import { useLanguage, t } from '../lib/i18n'; +import { getStoredCustomCalendarEvents, saveStoredCustomCalendarEvents } from '../lib/googleCalendar'; +import { db } from '../lib/db'; +import { getCurrentUser, AUTH_CHANGE_EVENT_NAME } from '../lib/auth'; type Client = { id: string; name: string; birthDate: string; contact: string; notes: string; createdAt: string }; type Session = { id: string; clientId: string; date: string; duration: number; template: string; intervention: string; progress: string }; @@ -9,31 +13,112 @@ type Trip = { id: string; date: string; departure: string; destination: string; type Billing = { id: string; clientId: string; date: string; service: string; amount: number; status: 'draft' | 'ready' }; const STORAGE_KEY = 'socdof_therapy_practice_v1'; +const BACKUP_LIMIT = 10; const emptyData = { clients: [] as Client[], sessions: [] as Session[], appointments: [] as Appointment[], trips: [] as Trip[], billing: [] as Billing[] }; -function loadData() { +function loadLegacyData() { try { const raw = localStorage.getItem(STORAGE_KEY); - return raw ? { ...emptyData, ...JSON.parse(raw) } : emptyData; - } catch { return emptyData; } + return raw ? { ...emptyData, ...JSON.parse(raw) } : null; + } catch { return null; } } -export const TherapyPracticeModule: React.FC = () => { +interface TherapyPracticeModuleProps { + contacts: Contact[]; + invoices: Invoice[]; + onOpenCalendar: () => void; + onOpenInvoice: (contactId?: number) => void; +} + +export const TherapyPracticeModule: React.FC = ({ + contacts, + invoices, + onOpenCalendar, + onOpenInvoice +}) => { const currentLang = useLanguage(); - const [data, setData] = useState(loadData); + const [userId, setUserId] = useState(() => getCurrentUser()?.id ?? 'anonymous'); + const [data, setData] = useState(() => loadLegacyData() ?? emptyData); + const hydratedUserRef = React.useRef(null); + const persistedUserRef = React.useRef(null); const [tab, setTab] = useState<'overview' | 'clients' | 'sessions' | 'appointments' | 'mileage' | 'billing'>('overview'); const [query, setQuery] = useState(''); - const [modal, setModal] = useState(null); + const [modal, setModal] = useState(null); const [selectedClient, setSelectedClient] = useState(''); + useEffect(() => { persistedUserRef.current = null; }, [userId]); + + useEffect(() => { + const handleAuthChanged = () => setUserId(getCurrentUser()?.id ?? 'anonymous'); + window.addEventListener(AUTH_CHANGE_EVENT_NAME, handleAuthChanged); + return () => window.removeEventListener(AUTH_CHANGE_EVENT_NAME, handleAuthChanged); + }, []); - useEffect(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); }, [data]); + useEffect(() => { + let cancelled = false; + hydratedUserRef.current = null; + setData(emptyData); + setSelectedClient(''); + void (async () => { + const record = await db.therapy_practice.get(userId); + if (cancelled) return; + if (record?.data) { + setData({ ...emptyData, ...(record.data as typeof emptyData) }); + } else { + const legacy = loadLegacyData(); + if (legacy) { + await db.therapy_practice.put({ + key: userId, + userId, + data: legacy, + updatedAt: new Date().toISOString() + }); + await db.therapy_backups.put({ + id: crypto.randomUUID(), + userId, + data: legacy, + createdAt: new Date().toISOString() + }); + try { localStorage.removeItem(STORAGE_KEY); } catch {} + } + const initialData = legacy ?? emptyData; + setData(initialData); + const now = new Date().toISOString(); + await db.therapy_practice.put({ key: userId, userId, data: initialData, updatedAt: now }); + await db.therapy_backups.put({ id: crypto.randomUUID(), userId, data: initialData, createdAt: now }); + } + hydratedUserRef.current = userId; + })().catch(() => { + hydratedUserRef.current = userId; + }); + return () => { cancelled = true; }; + }, [userId]); + + useEffect(() => { + if (hydratedUserRef.current !== userId || persistedUserRef.current === userId) return; + persistedUserRef.current = userId; + const persist = async () => { + const updatedAt = new Date().toISOString(); + await db.therapy_practice.put({ key: userId, userId, data, updatedAt }); + await db.therapy_backups.put({ + id: crypto.randomUUID(), + userId, + data, + createdAt: updatedAt + }); + const backups = await db.therapy_backups.where('userId').equals(userId).sortBy('createdAt'); + if (backups.length > BACKUP_LIMIT) { + await db.therapy_backups.bulkDelete(backups.slice(0, backups.length - BACKUP_LIMIT).map(b => b.id)); + } + }; + void persist(); + }, [data, userId]); const clients = useMemo(() => data.clients.filter(c => c.name.toLowerCase().includes(query.toLowerCase())), [data.clients, query]); const today = new Date().toISOString().slice(0, 10); const openAppointments = data.appointments.filter(a => a.date >= today && a.status === 'scheduled').length; const mileageTotal = data.trips.reduce((s, t) => s + Math.max(0, t.endKm - t.startKm), 0); - const billingTotal = data.billing.reduce((s, b) => s + b.amount, 0); + const billingTotal = invoices.reduce((s, invoice) => s + (invoice.total || 0), 0); const add = (kind: NonNullable, value: any) => { const id = crypto.randomUUID(); @@ -43,8 +128,38 @@ export const TherapyPracticeModule: React.FC = () => { setModal(null); }; + const createAppointmentCalendarEvent = (value: any) => { + const client = data.clients.find(c => c.id === value.clientId); + const event: CalendarAppEvent = { + id: `therapy_${crypto.randomUUID()}`, + title: `${t('therapy.appointmentPrefix', currentLang)}: ${client?.name || t('therapy.patient', currentLang)}`, + description: value.notes || '', + startDate: value.date, + endDate: value.date, + isAllDay: true, + category: 'customer', + source: 'local', + createdAt: new Date().toISOString() + }; + saveStoredCustomCalendarEvents([...getStoredCustomCalendarEvents(), event]); + window.dispatchEvent(new Event('socdof:calendar-updated')); + return event.id; + }; + + const saveAppointment = (value: any) => { + const calendarEventId = createAppointmentCalendarEvent(value); + add('appointment', { ...value, calendarEventId }); + }; + const remove = (kind: 'clients' | 'sessions' | 'appointments' | 'trips' | 'billing', id: string) => { if (!window.confirm(t('therapy.confirmDelete', currentLang))) return; + if (kind === 'appointments') { + const appointment = data.appointments.find(a => a.id === id); + if (appointment?.calendarEventId) { + saveStoredCustomCalendarEvents(getStoredCustomCalendarEvents().filter(e => e.id !== appointment.calendarEventId)); + window.dispatchEvent(new Event('socdof:calendar-updated')); + } + } setData(prev => ({ ...prev, [kind]: prev[kind].filter((x: any) => x.id !== id) })); }; @@ -58,7 +173,6 @@ export const TherapyPracticeModule: React.FC = () => {

{t('therapy.title', currentLang)}

{t('therapy.subtitle', currentLang)}

-
{t('therapy.encrypted', currentLang)}
{([ @@ -91,9 +205,12 @@ export const TherapyPracticeModule: React.FC = () => {
{['intake','standard','crisis','finalReport'].map(k => )}
-

{t('therapy.privacy', currentLang)}

-

{t('therapy.privacy', currentLang)}

-
{t('therapy.encrypted', currentLang)}
+

{t('therapy.integrations', currentLang)}

+

{t('therapy.integrationsDesc', currentLang)}

+
+ + +
@@ -108,6 +225,7 @@ export const TherapyPracticeModule: React.FC = () => { } {tab === 'appointments' && setModal('appointment')}> +
{data.appointments.length === 0 ? : data.appointments.map(a => } title={clientName(a.clientId)} subtitle={`${a.date} · ${t(`therapy.${a.status}`, currentLang)}`} onDelete={() => remove('appointments', a.id)} />)}
} @@ -116,12 +234,12 @@ export const TherapyPracticeModule: React.FC = () => {
{mileageTotal.toFixed(1)} km
} - {tab === 'billing' && setModal('billing')}> - {data.billing.length === 0 ? : data.billing.map(b => } title={clientName(b.clientId)} subtitle={`${b.date} · ${b.service} · € ${b.amount.toFixed(2)}`} onDelete={() => remove('billing', b.id)} />)} + {tab === 'billing' && onOpenInvoice()}> + {invoices.length === 0 ? : invoices.slice(0, 20).map(invoice => } title={invoice.contact_name || invoice.contact_company || invoice.number} subtitle={`${invoice.date} · ${invoice.number} · € ${(invoice.total || 0).toFixed(2)} · ${invoice.status}`} onDelete={undefined} />)} } - {modal && setModal(null)} onSave={add} />} + {modal && setModal(null)} onSave={(kind, value) => kind === 'appointment' ? saveAppointment(value) : add(kind, value)} />} ); }; @@ -133,15 +251,32 @@ const ListShell = ({ title, action, onAdd, search, value, onSearch, children }: return

{title}

{search &&
onSearch(e.target.value)} placeholder={t('therapy.search', currentLang)} className="pl-9 pr-3 py-2 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-xs outline-none" />
}
{children}
; }; -function Modal({ kind, clients, onClose, onSave }: any) { +function Modal({ kind, clients, contacts, selectedClient, setSelectedClient, onClose, onSave }: any) { const currentLang = useLanguage(); const [form, setForm] = useState({ date: new Date().toISOString().slice(0,10), duration: 50, rate: 0, startKm: 0, endKm: 0, status: 'scheduled', template: 'standard' }); + const [contactSearch, setContactSearch] = useState(''); const set = (k: string, v: any) => setForm((p: any) => ({ ...p, [k]: v })); + const selectContact = (id: string) => { + const contact = contacts.find((c: Contact) => String(c.id) === id); + set('contactId', contact?.id); + set('name', contact?.name || ''); + set('contact', contact?.phone || contact?.email || ''); + setContactSearch(contact?.name || ''); + }; + const contactPicker = kind === 'client' ?
+ +
setContactSearch(e.target.value)} placeholder={t('therapy.searchContacts', currentLang)} className="w-full pl-9 pr-3 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-sm outline-none" />
+
+ {contacts.filter((c: Contact) => (c.name || '').toLowerCase().includes(contactSearch.toLowerCase())).slice(0, 8).map((c: Contact) => )} + {contacts.length === 0 &&
{t('therapy.noContacts', currentLang)}
} +
+
: null; + const clientPicker = ; const input = (k: string, placeholder = '') => set(k, e.target.value)} placeholder={placeholder} className="w-full px-3 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-sm outline-none" />; const selectClient = ; - const fields = kind === 'client' ? <>{input('name', t('therapy.name', currentLang))}{input('birthDate', t('therapy.birthDate', currentLang))}{input('contact', t('therapy.contact', currentLang))}{input('notes', t('therapy.notes', currentLang))} : - kind === 'session' ? <>{selectClient}{input('date')}{input('duration')}{}{input('intervention', t('therapy.intervention', currentLang))}{input('progress', t('therapy.progress', currentLang))} : - kind === 'appointment' ? <>{selectClient}{input('date')}{}{input('notes', t('therapy.notes', currentLang))} : + const fields = kind === 'client' ? <>{contactPicker}{input('name', t('therapy.name', currentLang))}{input('birthDate', t('therapy.birthDate', currentLang))}{input('contact', t('therapy.contact', currentLang))}{input('notes', t('therapy.notes', currentLang))} : + kind === 'session' ? <>{clientPicker}{input('date')}{input('duration')}{}{input('intervention', t('therapy.intervention', currentLang))}{input('progress', t('therapy.progress', currentLang))} : + kind === 'appointment' ? <>{clientPicker}{input('date')}{}{input('notes', t('therapy.notes', currentLang))} : kind === 'trip' ? <>{input('date')}{input('departure', t('therapy.departure', currentLang))}{input('destination', t('therapy.destination', currentLang))}{input('purpose', t('therapy.purpose', currentLang))}{input('startKm')}{input('endKm')}{input('rate')} : <>{selectClient}{input('date')}{input('service', t('therapy.service', currentLang))}{input('amount')}; return
e.stopPropagation()}>

{kind === 'client' ? t('therapy.newPatient', currentLang) : kind === 'session' ? t('therapy.newSession', currentLang) : kind === 'appointment' ? t('therapy.newAppointment', currentLang) : kind === 'trip' ? t('therapy.newTrip', currentLang) : t('therapy.billing', currentLang)}

{fields}
; diff --git a/src/lib/db.ts b/src/lib/db.ts index db08cc2..ec32d98 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -20,6 +20,8 @@ export class LocalOdooDB extends Dexie { pos_orders!: Table; chatter_messages!: Table; settings!: Table<{ key: string; value: unknown }, string>; + therapy_practice!: Table<{ key: string; userId: string; data: unknown; updatedAt: string }, string>; + therapy_backups!: Table<{ id: string; userId: string; data: unknown; createdAt: string }, string>; constructor() { super('LocalOdooERP_DB'); @@ -34,6 +36,18 @@ export class LocalOdooDB extends Dexie { chatter_messages: '++id, [res_model+res_id], created_at', settings: 'key' }); + this.version(3).stores({ + contacts: '++id, name, email, phone, company, type, createdAt', + products: '++id, name, sku, barcode, sale_price, cost_price, qty_available, category, min_qty', + stock_moves: '++id, product_id, qty, source_location, dest_location, date, reference', + invoices: '++id, contact_id, number, date, due_date, status, type, total, sent_at, paid_at', + purchase_orders: '++id, vendor_id, number, order_date, status, total', + pos_orders: '++id, receipt_number, date, total, payment_method', + chatter_messages: '++id, [res_model+res_id], created_at', + settings: 'key', + therapy_practice: 'key, userId, updatedAt', + therapy_backups: 'id, userId, createdAt' + }); } } diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 1a48c3b..2fb0e99 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -20,6 +20,14 @@ export const SUPPORTED_LANGUAGES: LanguageOption[] = [ export const translations: Record> = { en: { 'therapy.title': 'Practice', + 'therapy.integrations': 'Connected workflows', + 'therapy.integrationsDesc': 'Clients use the shared Contacts list. Appointments use the shared Calendar. Billing opens the existing invoice workflow.', + 'therapy.openCalendar': 'Open calendar', + 'therapy.openBilling': 'Open billing', + 'therapy.contactPicker': 'Use an existing contact', + 'therapy.searchContacts': 'Search contacts...', + 'therapy.noContacts': 'No contacts available.', + 'therapy.appointmentPrefix': 'Therapy appointment', 'therapy.subtitle': 'Therapy & consultation workspace', 'therapy.patients': 'Clients', 'therapy.sessions': 'Sessions', @@ -2060,6 +2068,14 @@ export const translations: Record> = { de: { 'therapy.title': 'Praxis', + 'therapy.integrations': 'Verknüpfte Arbeitsabläufe', + 'therapy.integrationsDesc': 'Klienten verwenden die gemeinsame Kontaktliste. Termine verwenden den gemeinsamen Kalender. Die Abrechnung öffnet den bestehenden Rechnungsablauf.', + 'therapy.openCalendar': 'Kalender öffnen', + 'therapy.openBilling': 'Abrechnung öffnen', + 'therapy.contactPicker': 'Bestehenden Kontakt verwenden', + 'therapy.searchContacts': 'Kontakte suchen...', + 'therapy.noContacts': 'Keine Kontakte vorhanden.', + 'therapy.appointmentPrefix': 'Therapietermin', 'therapy.subtitle': 'Arbeitsbereich für Therapie & Beratung', 'therapy.patients': 'Klienten', 'therapy.sessions': 'Sitzungen', @@ -4100,6 +4116,14 @@ export const translations: Record> = { fr: { 'therapy.title': 'Cabinet', + 'therapy.integrations': 'Flux de travail connectés', + 'therapy.integrationsDesc': 'Les clients utilisent la liste de contacts commune. Les rendez-vous utilisent le calendrier commun. La facturation ouvre le processus de facture existant.', + 'therapy.openCalendar': 'Ouvrir le calendrier', + 'therapy.openBilling': 'Ouvrir la facturation', + 'therapy.contactPicker': 'Utiliser un contact existant', + 'therapy.searchContacts': 'Rechercher des contacts...', + 'therapy.noContacts': 'Aucun contact disponible.', + 'therapy.appointmentPrefix': 'Rendez-vous thérapeutique', 'therapy.subtitle': 'Espace de travail thérapie & consultation', 'therapy.patients': 'Clients', 'therapy.sessions': 'Séances', @@ -6139,6 +6163,14 @@ export const translations: Record> = { es: { 'therapy.title': 'Consulta', + 'therapy.integrations': 'Flujos conectados', + 'therapy.integrationsDesc': 'Los clientes usan la lista de contactos compartida. Las citas usan el calendario compartido. La facturación abre el flujo de facturas existente.', + 'therapy.openCalendar': 'Abrir calendario', + 'therapy.openBilling': 'Abrir facturación', + 'therapy.contactPicker': 'Usar un contacto existente', + 'therapy.searchContacts': 'Buscar contactos...', + 'therapy.noContacts': 'No hay contactos disponibles.', + 'therapy.appointmentPrefix': 'Cita de terapia', 'therapy.subtitle': 'Espacio de trabajo de terapia y consulta', 'therapy.patients': 'Clientes', 'therapy.sessions': 'Sesiones',