diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 487e65a..61bc6e9 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -30,7 +30,9 @@ "copied": "Copied", "copyLink": "Copy link", "none": "None", - "preview": "Preview" + "preview": "Preview", + "openMenu": "Open menu", + "closeMenu": "Close menu" }, "language": { "label": "Language", @@ -64,6 +66,7 @@ "signOut": "Log out", "register": "Register", "startTrial": "Start Trial", + "startTrialShort": "Try free", "startFreeTrial": "Start Free Trial", "dashboard": "Dashboard", "signInTitle": "Sign in to your account", diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index 66eeacb..10a0ce2 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -30,7 +30,9 @@ "copied": "کپی شد", "copyLink": "کپی لینک", "none": "هیچکدام", - "preview": "پیش‌نمایش" + "preview": "پیش‌نمایش", + "openMenu": "باز کردن منو", + "closeMenu": "بستن منو" }, "language": { "label": "زبان", @@ -64,6 +66,7 @@ "signOut": "خروج", "register": "ثبت‌نام", "startTrial": "شروع دوره آزمایشی", + "startTrialShort": "آزمایشی", "startFreeTrial": "شروع دوره آزمایشی رایگان", "dashboard": "داشبورد", "signInTitle": "به حساب کاربری خود وارد شوید", diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index 271252c..4f919b3 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -30,7 +30,9 @@ "copied": "Gekopieerd", "copyLink": "Link kopiëren", "none": "Geen", - "preview": "Voorbeeld" + "preview": "Voorbeeld", + "openMenu": "Menu openen", + "closeMenu": "Menu sluiten" }, "language": { "label": "Taal", @@ -64,6 +66,7 @@ "signOut": "Uitloggen", "register": "Registreren", "startTrial": "Proefperiode starten", + "startTrialShort": "Gratis proberen", "startFreeTrial": "Gratis proefperiode starten", "dashboard": "Dashboard", "signInTitle": "Meld u aan bij uw account", diff --git a/frontend/src/app/[locale]/(dashboard)/appointments/page.tsx b/frontend/src/app/[locale]/(dashboard)/appointments/page.tsx index 573c324..417b101 100644 --- a/frontend/src/app/[locale]/(dashboard)/appointments/page.tsx +++ b/frontend/src/app/[locale]/(dashboard)/appointments/page.tsx @@ -283,7 +283,7 @@ export default function AppointmentsPage() { return (
-

{t('title')}

+

{t('title')}

{t('subtitle')}

diff --git a/frontend/src/app/[locale]/(dashboard)/billing/page.tsx b/frontend/src/app/[locale]/(dashboard)/billing/page.tsx index 3d90a1a..e2420c4 100644 --- a/frontend/src/app/[locale]/(dashboard)/billing/page.tsx +++ b/frontend/src/app/[locale]/(dashboard)/billing/page.tsx @@ -1,6 +1,7 @@ // src/app/(dashboard)/billing/page.tsx 'use client'; -import { useState } from 'react'; + +import { useMemo, useState } from 'react'; import { Pencil } from 'lucide-react'; import { Button } from '@/components/ui/shared/Button'; import { Badge } from '@/components/ui/shared/Badge'; @@ -9,206 +10,287 @@ import { Table } from '@/components/ui/shared/Table'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import { useAuth } from '@/lib/hooks/useAuth'; import { hasPermission } from '@/components/shared/permissions'; -// Mock data matching your design -const invoices = [ - { id: '#123456', patient: 'Ali Rahmani', date: '24/9/2026', service: 'Hygiene', amount: 300, paid: 0, status: 'unpaid' }, - { id: '#123457', patient: 'Neda Akbari', date: '01/10/2026', service: 'Filling', amount: 700, paid: 400, status: 'overdue' }, - { id: '#123458', patient: 'Nima Haghi', date: '09/12/2026', service: 'Extraction', amount: 450, paid: 450, status: 'paid' }, + +type InvoiceStatus = 'paid' | 'unpaid' | 'overdue'; + +type Invoice = { + id: string; + patient: string; + date: string; + service: string; + amount: number; + paid: number; + status: InvoiceStatus; +}; + +const invoices: Invoice[] = [ + { id: '#123456', patient: 'Ali Rahmani', date: '24/9/2026', service: 'Hygiene', amount: 300, paid: 0, status: 'unpaid' }, + { id: '#123457', patient: 'Neda Akbari', date: '01/10/2026', service: 'Filling', amount: 700, paid: 400, status: 'overdue' }, + { id: '#123458', patient: 'Nima Haghi', date: '09/12/2026', service: 'Extraction', amount: 450, paid: 450, status: 'paid' }, ]; + const statusColors = { - paid: 'success', - unpaid: 'warning', - overdue: 'danger', + paid: 'success', + unpaid: 'warning', + overdue: 'danger', } as const; + +const statusFilters = ['all', 'paid', 'unpaid', 'overdue'] as const; + type StatCardColor = 'blue' | 'yellow' | 'green' | 'red'; interface StatCardProps { - title: string; - count: number; - amount: number; - color: StatCardColor; + title: string; + count: number; + amount: number; + color: StatCardColor; } -export default function BillingPage() { - const { currentOrganization } = useAuth(); - const [search, setSearch] = useState(''); - const [statusFilter, setStatusFilter] = useState('all'); - const canEditBilling = hasPermission(currentOrganization, 'TAB_BILLING_EDIT'); - const stats = { - total: { count: 235, amount: 80900 }, - unpaid: { count: 30, amount: 2800 }, - paid: { count: 190, amount: 80900 }, - overdue: { count: 235, amount: 80900 }, - }; - return ( -
- {/* Header */} -
-

Billing

- -
- {/* Stats Cards - Matching your design */} -
- - - - -
- {/* Filters */} - - {['all', 'paid', 'unpaid', 'overdue'].map((status) => ( - - ))} - - )} - /> - {/* Invoices Table - Matching your design */} - - - - - - - - - - - } - body={ - <> - {invoices.map((invoice) => ( - - - - - - - - - - - ))} - - } - footer={( - <> - -
- Page 1 of 10 -
- - - )} - /> - - ); -} -function StatCard({ title, count, amount, color }: StatCardProps) { - const colors: Record = { - blue: '!bg-purpose-visit-bg !text-purpose-visit-fg !border-purpose-visit-border', - yellow: '!bg-badge-warning-bg !text-badge-warning-fg !border-badge-warning-border', - green: '!bg-badge-success-bg !text-badge-success-fg !border-badge-success-border', - red: '!bg-badge-danger-bg !text-badge-danger-fg !border-badge-danger-border', - }; - return ( - -

{title}

-

{count}

-

- ${amount.toLocaleString()} -

-
- ); +export default function BillingPage() { + const { currentOrganization } = useAuth(); + const [search, setSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState<(typeof statusFilters)[number]>('all'); + const canEditBilling = hasPermission(currentOrganization, 'TAB_BILLING_EDIT'); + + const stats = { + total: { count: 235, amount: 80900 }, + unpaid: { count: 30, amount: 2800 }, + paid: { count: 190, amount: 80900 }, + overdue: { count: 235, amount: 80900 }, + }; + + const filteredInvoices = useMemo(() => { + const query = search.trim().toLowerCase(); + + return invoices.filter((invoice) => { + const matchesStatus = statusFilter === 'all' || invoice.status === statusFilter; + const matchesSearch = + !query || + invoice.patient.toLowerCase().includes(query) || + invoice.id.toLowerCase().includes(query) || + invoice.service.toLowerCase().includes(query); + + return matchesStatus && matchesSearch; + }); + }, [search, statusFilter]); + + return ( +
+
+

Billing

+ +
+ +
+ + + + +
+ + + {statusFilters.map((status) => ( + + ))} + + )} + /> + +
+ {filteredInvoices.length === 0 ? ( +
No invoices match your filters.
+ ) : ( + filteredInvoices.map((invoice) => ( + + )) + )} + +
+ +
+
- Invoice ID - - Patient name - - Date - - Service - - Total amount - - Paid - - Status - - Action -
- {invoice.id} - - {invoice.patient} - - {invoice.date} - - {invoice.service} - - ${invoice.amount} - - ${invoice.paid} - - - {invoice.status} - - - -
+ + + + + + + + + + } + body={ + <> + {filteredInvoices.map((invoice) => ( + + + + + + + + + + + ))} + + } + footer={} + /> + + + ); +} + +function InvoiceMobileCard({ + invoice, + canEditBilling, +}: { + invoice: Invoice; + canEditBilling: boolean; +}) { + const remaining = invoice.amount - invoice.paid; + + return ( + +
+
+

{invoice.patient}

+

{invoice.id}

+
+ + {invoice.status} + +
+ +
+ {invoice.service} + · + {invoice.date} +
+ +
+
+

Total

+

${invoice.amount}

+
+
+

Paid

+

${invoice.paid}

+
+
+

Due

+

${remaining}

+
+
+ +
+ +
+
+ ); +} + +function InvoiceEditButton({ canEditBilling }: { canEditBilling: boolean }) { + return ( + + ); +} + +function InvoicePagination({ className = '' }: { className?: string }) { + return ( +
+ +
Page 1 of 10
+ +
+ ); +} + +function StatCard({ title, count, amount, color }: StatCardProps) { + const colors: Record = { + blue: '!bg-purpose-visit-bg !text-purpose-visit-fg !border-purpose-visit-border', + yellow: '!bg-badge-warning-bg !text-badge-warning-fg !border-badge-warning-border', + green: '!bg-badge-success-bg !text-badge-success-fg !border-badge-success-border', + red: '!bg-badge-danger-bg !text-badge-danger-fg !border-badge-danger-border', + }; + + return ( + +

{title}

+

{count}

+

+ ${amount.toLocaleString()} +

+
+ ); } diff --git a/frontend/src/app/[locale]/(dashboard)/cases/page.tsx b/frontend/src/app/[locale]/(dashboard)/cases/page.tsx index 7fd12b1..c6df3f4 100644 --- a/frontend/src/app/[locale]/(dashboard)/cases/page.tsx +++ b/frontend/src/app/[locale]/(dashboard)/cases/page.tsx @@ -19,6 +19,7 @@ import { tasksApi } from '@/lib/api/tasks'; import { treatmentCatalogApi } from '@/lib/api/treatment-catalog'; import { treatmentTypeLabelFromCatalog } from '@/components/ui/treatment/treatmentTypeDisplay'; import { Button } from '@/components/ui/shared/Button'; +import { MobileDetailBackButton } from '@/components/ui/shared/MobileDetailBackButton'; import { FORM_SELECT_CLASS } from '@/components/ui/shared/formSelectStyles'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import type { TreatmentCatalogEntry } from '@/types/treatment-catalog'; @@ -60,6 +61,7 @@ export default function CasesPage() { const [treatmentCatalog, setTreatmentCatalog] = useState([]); const [selectedCaseId, setSelectedCaseId] = useState(null); + const [mobileDetailOpen, setMobileDetailOpen] = useState(false); const [selectedCase, setSelectedCase] = useState(null); const [loadingList, setLoadingList] = useState(false); const [loadingDetail, setLoadingDetail] = useState(false); @@ -146,9 +148,16 @@ export default function CasesPage() { const caseIdFromUrl = searchParams.get('caseId'); if (caseIdFromUrl) { setSelectedCaseId(caseIdFromUrl); + setMobileDetailOpen(true); } }, [searchParams]); + useEffect(() => { + if (!selectedCaseId) { + setMobileDetailOpen(false); + } + }, [selectedCaseId]); + useEffect(() => { const timeout = setTimeout(() => { void loadCases({ @@ -220,12 +229,16 @@ export default function CasesPage() { return (
-

{t('title')}

+

{t('title')}

{t('subtitle')}

-
+
-
+
+ {mobileDetailOpen && selectedCaseId ? ( + setMobileDetailOpen(false)} /> + ) : null} {!selectedCaseId ? (

{t('selectCaseHint')}

) : loadingDetail || !selectedCase ? ( diff --git a/frontend/src/app/[locale]/(dashboard)/layout.tsx b/frontend/src/app/[locale]/(dashboard)/layout.tsx index 037e1c7..4d7ffbe 100644 --- a/frontend/src/app/[locale]/(dashboard)/layout.tsx +++ b/frontend/src/app/[locale]/(dashboard)/layout.tsx @@ -1,7 +1,8 @@ 'use client'; -import { memo, useEffect } from 'react'; +import { memo, useEffect, useState } from 'react'; import { useTranslations } from 'next-intl'; +import { Menu } from 'lucide-react'; import { usePathname, useRouter } from '@/i18n/navigation'; import { useAuth } from '@/lib/hooks/useAuth'; import Sidebar from '@/components/ui/shared/Sidebar'; @@ -17,6 +18,22 @@ export default function DashboardLayout({ children }: { children: React.ReactNod const { user, currentOrganization, isAuthReady } = useAuth(); const router = useRouter(); const pathname = usePathname(); + const [sidebarOpen, setSidebarOpen] = useState(false); + + useEffect(() => { + setSidebarOpen(false); + }, [pathname]); + + useEffect(() => { + if (!sidebarOpen) { + return; + } + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = previousOverflow; + }; + }, [sidebarOpen]); useEffect(() => { if (!isAuthReady) return; @@ -53,14 +70,26 @@ export default function DashboardLayout({ children }: { children: React.ReactNod } return ( -
- +
+ {sidebarOpen ? ( + +

{organizationName}

+
+ +
diff --git a/frontend/src/app/[locale]/(dashboard)/organizations/page.tsx b/frontend/src/app/[locale]/(dashboard)/organizations/page.tsx index cc1da47..954960e 100644 --- a/frontend/src/app/[locale]/(dashboard)/organizations/page.tsx +++ b/frontend/src/app/[locale]/(dashboard)/organizations/page.tsx @@ -15,6 +15,7 @@ import { } from '@/lib/api/organization'; import { invitationTargetFromConnectionRow } from '@/components/invitations/organizationInviteLinks'; import { CopyInvitationLinkButton } from '@/components/ui/organizations/CopyInvitationLinkButton'; +import { OrganizationConnectionsMobileList } from '@/components/ui/organizations/OrganizationConnectionsMobileList'; import { InvitationHistoryDialog } from '@/components/ui/organizations/InvitationHistoryDialog'; import { ConnectionCaseHistoryContent } from '@/components/ui/organizations/ConnectionCaseHistoryContent'; import { Button } from '@/components/ui/shared/Button'; @@ -306,10 +307,10 @@ export default function OrganizationsPage() {
-

{tabLabel}

+

{tabLabel}

{t('subtitle')}

-
@@ -344,6 +345,53 @@ export default function OrganizationsPage() { } /> + invitationTargetFromConnectionRow(row, currentOrganization.id)} + onCopyInvitation={(row) => void handleCopyInvitationFromRow(row)} + onRespond={(rowId, action) => void respondToPendingConnection(rowId, action)} + onViewCaseHistory={setCaseHistoryConnection} + onDeleteConnection={(rowId) => void deleteConnection(rowId)} + onSendConnectionRequest={(orgId) => void submitConnectionRequest(orgId)} + onToggleInviteForm={() => setShowInviteForm((v) => !v)} + onManualOrganizationNameChange={setManualOrganizationName} + onManualOwnerEmailChange={setManualOwnerEmail} + onSendInvite={() => void sendInvite()} + labels={{ + loading: tCommon('loadingEllipsis'), + emptyConnections: t('emptyConnections'), + noDirectoryResults: t('noDirectoryResults'), + hideInvitationFields: t('hideInvitationFields'), + sendInvitationLink: t('sendInvitationLink'), + counterpartNameLabel: t('counterpartNameLabel', { counterpart }), + ownerEmailLabel: t('ownerEmailLabel'), + sendInvitation: t('sendInvitation'), + sendRequest: t('sendRequest'), + acceptRequest: t('acceptRequest'), + declineRequest: t('declineRequest'), + viewCaseHistory: t('viewCaseHistory'), + removeConnection: t('removeConnection'), + statusToday: t('statusToday'), + statusFound: t('statusFound'), + }} + /> + +
+ Invoice ID + + Patient name + + Date + + Service + + Total amount + + Paid + + Status + + Action +
{invoice.id}{invoice.patient}{invoice.date}{invoice.service}${invoice.amount}${invoice.paid} + + {invoice.status} + + + +
@@ -537,6 +585,7 @@ export default function OrganizationsPage() { } /> + -
-

{t('title')}

+
+

{t('title')}

-
+

{isResetFlow ? t('resetPasswordTitle') : t('changePasswordTitle')}

diff --git a/frontend/src/app/[locale]/(dashboard)/settings/subscriptions/page.tsx b/frontend/src/app/[locale]/(dashboard)/settings/subscriptions/page.tsx index 01836c4..7888c07 100644 --- a/frontend/src/app/[locale]/(dashboard)/settings/subscriptions/page.tsx +++ b/frontend/src/app/[locale]/(dashboard)/settings/subscriptions/page.tsx @@ -80,20 +80,20 @@ export default function SubscriptionsSettingsPage() { > {tCommon('backToApp')} -

{t('subscriptionsTitle')}

+

{t('subscriptionsTitle')}

{t('subscriptionsSubtitle', { orgName: currentOrganization.name })}

-
+
{!hasActiveSubscription && (

{t('noSubscriptionNotice')}

)} -
+

{t('currentPlan')}

diff --git a/frontend/src/app/[locale]/(dashboard)/staff/page.tsx b/frontend/src/app/[locale]/(dashboard)/staff/page.tsx index e89a731..bdfe947 100644 --- a/frontend/src/app/[locale]/(dashboard)/staff/page.tsx +++ b/frontend/src/app/[locale]/(dashboard)/staff/page.tsx @@ -36,6 +36,7 @@ import { Checkbox } from '@/components/ui/shared/Checkbox'; import { Table } from '@/components/ui/shared/Table'; import { ToastStack } from '@/components/ui/shared/Toast'; import { formatApiErrorMessage } from '@/components/shared/formatApiError'; +import { StaffMembersMobileList } from '@/components/staff/StaffMembersMobileList'; import { useToast } from '@/lib/hooks/useToast'; type StoredInviteLink = { @@ -502,7 +503,7 @@ export default function StaffPage() {

-

{t('title')}

+

{t('title')}

{t('subtitle')}

@@ -763,12 +800,14 @@ export default function StaffPage() { } /> + + )} {inviteOpen && ( -
+
)} -
+
+ + )} - {inviteInfo?.status !== 'ACCEPTED' && ( -
- {step === 1 && ( - <> - } - /> - } - /> - } - passwordToggleLabels={passwordToggleLabels} - /> - } - passwordToggleLabels={passwordToggleLabels} - /> - - - )} - - {step === 2 && ( - <> - } - errors={errors as FieldErrors} - organizationType={organizationType} - setValue={setValue as unknown as UseFormSetValue} - /> -
- - -
- - )} - - )} - - )} -
+ +
+ + )} + + )} + + )}
-
+ ); } function AcceptOrganizationInviteFallback() { const t = useTranslations('auth'); return ( -
+

{t('loadingInvitation')}

); diff --git a/frontend/src/app/[locale]/(public)/forgot-password/page.tsx b/frontend/src/app/[locale]/(public)/forgot-password/page.tsx index c3a48b0..30442e7 100644 --- a/frontend/src/app/[locale]/(public)/forgot-password/page.tsx +++ b/frontend/src/app/[locale]/(public)/forgot-password/page.tsx @@ -9,9 +9,9 @@ import { Link, useRouter } from '@/i18n/navigation'; import { Phone, ShieldCheck } from 'lucide-react'; import { authApi } from '@/lib/api/auth'; import { useAuth } from '@/lib/hooks/useAuth'; +import { AuthPageShell } from '@/components/ui/auth/AuthPageShell'; import { Button } from '@/components/ui/shared/Button'; import { Input } from '@/components/ui/shared/Input'; -import { TopBarControls } from '@/components/ui/shared/TopBarControls'; type ForgotPasswordForm = { mobile: string; @@ -124,83 +124,81 @@ export default function ForgotPasswordPage() { }; return ( -
-
- + + + + {tCommon('appName')} + + +

+ {t('forgotPasswordTitle')} +

+

+ {step === 'mobile' ? t('forgotPasswordSubtitle') : t('codeSentHint')} +

+ + } + > +
+
undefined)} + > + {step === 'mobile' ? ( + } + /> + ) : ( + } + /> + )} + + {error && ( +
+

{error}

+
+ )} + + {step === 'mobile' ? ( + + ) : ( + + )} + +

+ + {t('backToSignIn')} + +

+
- -
- - {tCommon('appName')} - -

- {t('forgotPasswordTitle')} -

-

- {step === 'mobile' ? t('forgotPasswordSubtitle') : t('codeSentHint')} -

-
- -
-
-
undefined)} - > - {step === 'mobile' ? ( - } - /> - ) : ( - } - /> - )} - - {error && ( -
-

{error}

-
- )} - - {step === 'mobile' ? ( - - ) : ( - - )} - -

- - {t('backToSignIn')} - -

- -
-
-
+ ); } diff --git a/frontend/src/app/[locale]/(public)/login/page.tsx b/frontend/src/app/[locale]/(public)/login/page.tsx index fabceee..b37e80a 100644 --- a/frontend/src/app/[locale]/(public)/login/page.tsx +++ b/frontend/src/app/[locale]/(public)/login/page.tsx @@ -10,10 +10,10 @@ import { Link } from '@/i18n/navigation'; import { Mail, Lock } from 'lucide-react'; import { useAuth } from '@/lib/hooks/useAuth'; import { getRememberedEmail } from '@/lib/auth/rememberMe'; +import { AuthPageShell } from '@/components/ui/auth/AuthPageShell'; import { Button } from '@/components/ui/shared/Button'; import { Checkbox } from '@/components/ui/shared/Checkbox'; import { Input } from '@/components/ui/shared/Input'; -import { TopBarControls } from '@/components/ui/shared/TopBarControls'; type LoginForm = { email: string; @@ -74,82 +74,80 @@ export default function LoginPage() { if (!isAuthReady) { return ( -
+

{tCommon('loading')}

); } return ( -
-
- -
- -
- - {tCommon('appName')} - -

- {t('signInTitle')} -

-

- {tCommon('or')}{' '} - - {t('startTrialLink')} + + + + {tCommon('appName')} + -

-
+

+ {t('signInTitle')} +

+

+ {tCommon('or')}{' '} + + {t('startTrialLink')} + +

+ + } + > +
+
+ } + /> + } + passwordToggleLabels={{ + show: t('showPassword'), + hide: t('hidePassword'), + }} + /> -
-
- - } +
+ setValue('rememberMe', checked)} + label={t('rememberMe')} /> - } - passwordToggleLabels={{ - show: t('showPassword'), - hide: t('hidePassword'), - }} - /> - -
- setValue('rememberMe', checked)} - label={t('rememberMe')} - /> -
- - {t('forgotPassword')} - -
+
+ + {t('forgotPassword')} +
+
- {error && ( -
-

{error}

-
- )} + {error && ( +
+

{error}

+
+ )} - - -
+ +
-
+ ); } diff --git a/frontend/src/app/[locale]/(public)/page.tsx b/frontend/src/app/[locale]/(public)/page.tsx index 4ce81db..f000421 100644 --- a/frontend/src/app/[locale]/(public)/page.tsx +++ b/frontend/src/app/[locale]/(public)/page.tsx @@ -14,26 +14,33 @@ export default function HomePage() { const { user } = useAuth(); return ( -
+
-
-
+
+ {tCommon('appName')} -
+ -
+
{user ? ( - + ) : ( <> - - + + - + )} @@ -41,27 +48,34 @@ export default function HomePage() {
-
+
-

+

{t('heroTitle')} {t('heroHighlight')}

-

+

{t('heroSubtitle')}

{!user && ( - - - +
+ + + + + + +
)}
-
+
} title={t('featureClinicsTitle')} diff --git a/frontend/src/app/[locale]/(public)/register/page.tsx b/frontend/src/app/[locale]/(public)/register/page.tsx index 7643e5c..7ebe926 100644 --- a/frontend/src/app/[locale]/(public)/register/page.tsx +++ b/frontend/src/app/[locale]/(public)/register/page.tsx @@ -8,11 +8,11 @@ import { useTranslations } from 'next-intl'; import { Link } from '@/i18n/navigation'; import { Mail, Lock, User, Phone } from 'lucide-react'; import { useAuth } from '@/lib/hooks/useAuth'; +import { AuthPageShell } from '@/components/ui/auth/AuthPageShell'; import { OrganizationDetailsFields } from '@/components/ui/auth/OrganizationDetailsFields'; import { RegistrationProgressSteps } from '@/components/ui/auth/RegistrationProgressSteps'; import { Button } from '@/components/ui/shared/Button'; import { Input } from '@/components/ui/shared/Input'; -import { TopBarControls } from '@/components/ui/shared/TopBarControls'; type RegisterForm = { name: string; @@ -121,133 +121,139 @@ export default function RegisterPage() { }; return ( -
-
- -
+ + + + {tCommon('appName')} + + +

+ {t('registerTitle')} +

+

+ {t('registerPrompt')}{' '} + + {t('signInLink')} + +

+ + } + > +
+ +
+

{t('trialIncludes')}

+
    +
  • + + {t('trialTeamMembers')} +
  • +
  • + + {t('trialFullAccess')} +
  • +
  • + + {t('trialNoCard')} +
  • +
+
-
- - {tCommon('appName')} - -

- {t('registerTitle')} -

-

- {t('registerPrompt')}{' '} - - {t('signInLink')} +
+ {step === 1 && ( + <> + } + /> + } + /> + } + /> + } + passwordToggleLabels={passwordToggleLabels} + /> + } + passwordToggleLabels={passwordToggleLabels} + /> + + + )} + + {step === 2 && ( + <> + + {error && ( +

+

{error}

+
+ )} +
+ + +
+ + )} + + +

+ {t('termsIntro')}{' '} + + {t('termsOfService')} + {' '} + {tCommon('and')}{' '} + + {t('privacyPolicy')}

- -
-
- -
-

{t('trialIncludes')}

-
    -
  • - {t('trialTeamMembers')} -
  • -
  • - {t('trialFullAccess')} -
  • -
  • - {t('trialNoCard')} -
  • -
-
- -
- {step === 1 && ( - <> - } - /> - } - /> - } - /> - } - passwordToggleLabels={passwordToggleLabels} - /> - } - passwordToggleLabels={passwordToggleLabels} - /> - - - )} - - {step === 2 && ( - <> - - {error && ( -
-

{error}

-
- )} -
- - -
- - )} - - -

- {t('termsIntro')}{' '} - - {t('termsOfService')} - {' '} - {tCommon('and')}{' '} - - {t('privacyPolicy')} - -

-
-
-
+
); } diff --git a/frontend/src/app/[locale]/layout.tsx b/frontend/src/app/[locale]/layout.tsx index 570d0ab..adb377f 100644 --- a/frontend/src/app/[locale]/layout.tsx +++ b/frontend/src/app/[locale]/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from 'next'; +import type { Metadata, Viewport } from 'next'; import { NextIntlClientProvider } from 'next-intl'; import { getMessages, setRequestLocale } from 'next-intl/server'; import { hasLocale } from 'next-intl'; @@ -16,6 +16,11 @@ export const metadata: Metadata = { description: 'Connect dental clinics and laboratories seamlessly', }; +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, +}; + export function generateStaticParams() { return routing.locales.map((locale) => ({ locale })); } diff --git a/frontend/src/components/staff/StaffMembersMobileList.tsx b/frontend/src/components/staff/StaffMembersMobileList.tsx new file mode 100644 index 0000000..fb148d0 --- /dev/null +++ b/frontend/src/components/staff/StaffMembersMobileList.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { Check, Copy, Pencil, Trash2, UserCheck, UserX } from 'lucide-react'; +import type { StaffMemberDto } from '@/lib/api/staff'; +import { Badge } from '@/components/ui/shared/Badge'; +import { Card } from '@/components/ui/shared/Card'; + +type StaffMembersMobileListProps = { + members: StaffMemberDto[]; + canEdit: boolean; + organizationType: string | undefined; + copiedInviteMembershipId: string | null; + copyingInviteMembershipId: string | null; + enablingMembershipId: string | null; + disablingMembershipId: string | null; + formatAccessSummary: (member: StaffMemberDto) => string; + canShareInviteLink: (member: StaffMemberDto) => boolean; + canEnable: (member: StaffMemberDto) => boolean; + canDisable: (member: StaffMemberDto) => boolean; + onCopyInviteLink: (member: StaffMemberDto) => void; + onEnable: (member: StaffMemberDto) => void; + onDisable: (member: StaffMemberDto) => void; + onEdit: (member: StaffMemberDto) => void; + onDelete: (member: StaffMemberDto) => void; + labels: { + roleOwner: string; + roleStaff: string; + statusActive: string; + statusPending: string; + statusDisabled: string; + statusExpired: string; + allFeatures: string; + copyInviteLink: string; + enableMemberTitle: string; + disableMemberTitle: string; + editMemberAria: string; + deleteMemberAria: string; + }; +}; + +function memberStatusBadge( + member: StaffMemberDto, + labels: StaffMembersMobileListProps['labels'], +) { + if (member.isOwner || member.invitationStatus === 'ACTIVE') { + return {labels.statusActive}; + } + if (member.invitationStatus === 'PENDING') { + return {labels.statusPending}; + } + if (member.invitationStatus === 'DISABLED') { + return {labels.statusDisabled}; + } + return {labels.statusExpired}; +} + +export function StaffMembersMobileList({ + members, + canEdit, + copiedInviteMembershipId, + copyingInviteMembershipId, + enablingMembershipId, + disablingMembershipId, + formatAccessSummary, + canShareInviteLink, + canEnable, + canDisable, + onCopyInviteLink, + onEnable, + onDisable, + onEdit, + onDelete, + labels, +}: StaffMembersMobileListProps) { + return ( +
    + {members.map((member) => ( +
  • + +
    +
    +

    {member.name}

    +

    {member.email}

    +

    + {member.isOwner ? labels.roleOwner : labels.roleStaff} +

    +
    + {memberStatusBadge(member, labels)} +
    + +

    + {member.isOwner ? labels.allFeatures : formatAccessSummary(member)} +

    + + {!member.isOwner ? ( +
    + {canShareInviteLink(member) ? ( + + ) : null} + {canEnable(member) ? ( + + ) : null} + {canDisable(member) ? ( + + ) : null} + + +
    + ) : null} +
    +
  • + ))} +
+ ); +} diff --git a/frontend/src/components/staff/WorkingHoursEditor.tsx b/frontend/src/components/staff/WorkingHoursEditor.tsx index 0629225..98f5ed0 100644 --- a/frontend/src/components/staff/WorkingHoursEditor.tsx +++ b/frontend/src/components/staff/WorkingHoursEditor.tsx @@ -82,8 +82,8 @@ export function WorkingHoursEditor({ key={day.dayOfWeek} className="rounded-[var(--radius-md)] border border-border/60 bg-background-card/40 px-3 py-3 space-y-3" > -
- +
+ {t(WEEKDAY_KEYS[day.dayOfWeek])} -
+

@@ -194,7 +196,7 @@ export function AppointmentBookingModal({

-
+
+ + ); } diff --git a/frontend/src/components/ui/auth/AuthPageShell.tsx b/frontend/src/components/ui/auth/AuthPageShell.tsx new file mode 100644 index 0000000..e1582e7 --- /dev/null +++ b/frontend/src/components/ui/auth/AuthPageShell.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { TopBarControls } from '@/components/ui/shared/TopBarControls'; + +type AuthPageShellProps = { + header: React.ReactNode; + children: React.ReactNode; +}; + +export function AuthPageShell({ header, children }: AuthPageShellProps) { + return ( +
+
+ +
+ +
+ {header} +
{children}
+
+
+ ); +} diff --git a/frontend/src/components/ui/auth/OrganizationDetailsFields.tsx b/frontend/src/components/ui/auth/OrganizationDetailsFields.tsx index 4b9fa1e..7634f06 100644 --- a/frontend/src/components/ui/auth/OrganizationDetailsFields.tsx +++ b/frontend/src/components/ui/auth/OrganizationDetailsFields.tsx @@ -48,7 +48,7 @@ export function OrganizationDetailsFields({ {t('organizationType')} -
+
-
+ + ); } diff --git a/frontend/src/components/ui/organizations/ConnectionCaseHistoryContent.tsx b/frontend/src/components/ui/organizations/ConnectionCaseHistoryContent.tsx index 43ea972..44f1af6 100644 --- a/frontend/src/components/ui/organizations/ConnectionCaseHistoryContent.tsx +++ b/frontend/src/components/ui/organizations/ConnectionCaseHistoryContent.tsx @@ -10,6 +10,7 @@ import { organizationApi } from '@/lib/api/organization'; import { treatmentCatalogApi } from '@/lib/api/treatment-catalog'; import { treatmentTypeLabelFromCatalog } from '@/components/ui/treatment/treatmentTypeDisplay'; import { Button } from '@/components/ui/shared/Button'; +import { MobileDetailBackButton } from '@/components/ui/shared/MobileDetailBackButton'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import { ToastStack } from '@/components/ui/shared/Toast'; import { CaseDetailPanel, CaseTaskProgressBar } from '@/components/ui/lab/CaseDetailPanel'; @@ -51,6 +52,7 @@ export function ConnectionCaseHistoryContent({ totalPages: 1, }); const [selectedCaseId, setSelectedCaseId] = useState(null); + const [mobileDetailOpen, setMobileDetailOpen] = useState(false); const [selectedCase, setSelectedCase] = useState(null); const [treatmentCatalog, setTreatmentCatalog] = useState([]); const [loadingList, setLoadingList] = useState(false); @@ -152,6 +154,12 @@ export function ConnectionCaseHistoryContent({ }; }, [selectedCaseId, connection.id, showError, setError]); + useEffect(() => { + if (!selectedCaseId) { + setMobileDetailOpen(false); + } + }, [selectedCaseId]); + function scrollToComments() { document.getElementById('case-comments')?.scrollIntoView({ behavior: 'smooth' }); } @@ -193,7 +201,7 @@ export function ConnectionCaseHistoryContent({
-

+

{t('caseHistoryTitle', { name: connection.organizationName })}

@@ -204,7 +212,11 @@ export function ConnectionCaseHistoryContent({

-
+
-
+
+ {mobileDetailOpen && selectedCaseId ? ( + setMobileDetailOpen(false)} /> + ) : null} {!selectedCaseId ? (

{tCases('selectCaseHint')}

) : loadingDetail || !selectedCase ? ( diff --git a/frontend/src/components/ui/organizations/InvitationHistoryDialog.tsx b/frontend/src/components/ui/organizations/InvitationHistoryDialog.tsx index 08fdc3d..3563226 100644 --- a/frontend/src/components/ui/organizations/InvitationHistoryDialog.tsx +++ b/frontend/src/components/ui/organizations/InvitationHistoryDialog.tsx @@ -2,6 +2,11 @@ import { useTranslations } from 'next-intl'; import { DialogCloseButton } from '@/components/ui/shared/DialogCloseButton'; +import { Card } from '@/components/ui/shared/Card'; +import { + ResponsiveDialogOverlay, + ResponsiveDialogPanel, +} from '@/components/ui/shared/ResponsiveDialog'; import { ToastStack, type ToastMessages } from '@/components/ui/shared/Toast'; import type { OrganizationInvitationHistoryItemDto } from '@/lib/api/organization'; import { Badge, organizationConnectionStatusVariant } from '@/components/ui/shared/Badge'; @@ -51,12 +56,13 @@ export function InvitationHistoryDialog({ if (!open) return null; return ( -
-
+

@@ -72,55 +78,86 @@ export function InvitationHistoryDialog({ ) : items.length === 0 ? (

{t('historyEmpty')}

) : ( -

- - - - - - - } - body={ - <> - {items.map((inv) => ( - - - - - - + + + + ))} + + +
+
- {t('tableOrganization')} - - {t('tableOwnerEmail')} - - {t('tableDate')} - - {t('tableStatus')} - - {t('tableInvitationLink')} -
{inv.organizationName}{inv.ownerEmail} - {formatTableDate(inv.createdAt)} - + <> +
    + {items.map((inv) => ( +
  • + +
    +
    +

    {inv.organizationName}

    +

    {inv.ownerEmail}

    +

    {formatTableDate(inv.createdAt)}

    +
    {formatInvitationStatusLabel(inv.status)} -
+ +
onCopy(inv)} /> -
+ + + + + - ))} - - } - /> + } + body={ + <> + {items.map((inv) => ( + + + + + + + + ))} + + } + /> + + )} - - + + ); } diff --git a/frontend/src/components/ui/organizations/OrganizationConnectionsMobileList.tsx b/frontend/src/components/ui/organizations/OrganizationConnectionsMobileList.tsx new file mode 100644 index 0000000..95e1f21 --- /dev/null +++ b/frontend/src/components/ui/organizations/OrganizationConnectionsMobileList.tsx @@ -0,0 +1,258 @@ +'use client'; + +import { Check, History, Trash2, UserPlus, X } from 'lucide-react'; +import type { + CounterpartItemDto, + CounterpartSearchResultDto, +} from '@/lib/api/organization'; +import { Badge, organizationConnectionStatusVariant } from '@/components/ui/shared/Badge'; +import { Button } from '@/components/ui/shared/Button'; +import { Card } from '@/components/ui/shared/Card'; +import { Input } from '@/components/ui/shared/Input'; +import { CopyInvitationLinkButton } from '@/components/ui/organizations/CopyInvitationLinkButton'; +import type { InvitationLinkTarget } from '@/components/invitations/organizationInviteLinks'; + +type OrganizationConnectionsMobileListProps = { + loading: boolean; + mode: 'existing' | 'search'; + existingRows: CounterpartItemDto[]; + searchResults: CounterpartSearchResultDto[]; + currentOrganizationId: string; + counterpart: string; + pendingConnectionRowId: string | null; + deleteConnectionRowId: string | null; + copiedId: string | null; + copyingInvitationId: string | null; + showInviteForm: boolean; + manualOrganizationName: string; + manualOwnerEmail: string; + inviteLoading: boolean; + formatConnectionStatusLabel: (row: CounterpartItemDto, orgId: string) => string; + formatTableDate: (value: string) => string; + getInvitationTarget: (row: CounterpartItemDto) => InvitationLinkTarget | null; + onCopyInvitation: (row: CounterpartItemDto) => void; + onRespond: (rowId: string, action: 'ACCEPT' | 'REJECT') => void; + onViewCaseHistory: (row: CounterpartItemDto) => void; + onDeleteConnection: (rowId: string) => void; + onSendConnectionRequest: (orgId: string) => void; + onToggleInviteForm: () => void; + onManualOrganizationNameChange: (value: string) => void; + onManualOwnerEmailChange: (value: string) => void; + onSendInvite: () => void; + labels: { + loading: string; + emptyConnections: string; + noDirectoryResults: string; + hideInvitationFields: string; + sendInvitationLink: string; + counterpartNameLabel: string; + ownerEmailLabel: string; + sendInvitation: string; + sendRequest: string; + acceptRequest: string; + declineRequest: string; + viewCaseHistory: string; + removeConnection: string; + statusToday: string; + statusFound: string; + }; +}; + +export function OrganizationConnectionsMobileList({ + loading, + mode, + existingRows, + searchResults, + currentOrganizationId, + counterpart, + pendingConnectionRowId, + deleteConnectionRowId, + copiedId, + copyingInvitationId, + showInviteForm, + manualOrganizationName, + manualOwnerEmail, + inviteLoading, + formatConnectionStatusLabel, + formatTableDate, + getInvitationTarget, + onCopyInvitation, + onRespond, + onViewCaseHistory, + onDeleteConnection, + onSendConnectionRequest, + onToggleInviteForm, + onManualOrganizationNameChange, + onManualOwnerEmailChange, + onSendInvite, + labels, +}: OrganizationConnectionsMobileListProps) { + if (loading) { + return

{labels.loading}

; + } + + if (mode === 'existing') { + if (existingRows.length === 0) { + return ( +

+ {labels.emptyConnections} +

+ ); + } + + return ( +
    + {existingRows.map((row) => { + const canRespond = + row.status === 'PENDING' && + row.requestedByOrganizationId !== null && + row.requestedByOrganizationId !== currentOrganizationId; + const invitationTarget = getInvitationTarget(row); + + return ( +
  • + +
    +
    +

    {row.organizationName}

    +

    {row.ownerEmail}

    +

    {formatTableDate(row.createdAt)}

    +
    + + {formatConnectionStatusLabel(row, currentOrganizationId)} + +
    + +
    + {invitationTarget ? ( + onCopyInvitation(row)} + /> + ) : null} + {canRespond ? ( + <> + + + + ) : null} + {row.status === 'ACTIVE' ? ( + <> + + + + ) : null} +
    +
    +
  • + ); + })} +
+ ); + } + + if (searchResults.length > 0) { + return ( +
    + {searchResults.map((result) => ( +
  • + +
    +
    +

    {result.name}

    +

    {result.owner.email}

    +

    {labels.statusToday}

    +
    + + {labels.statusFound} + +
    +
    + +
    +
    +
  • + ))} +
+ ); + } + + return ( +
+

{labels.noDirectoryResults}

+ + {showInviteForm ? ( +
+ onManualOrganizationNameChange(e.target.value)} + /> + onManualOwnerEmailChange(e.target.value)} + /> + +
+ ) : null} +
+ ); +} diff --git a/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx b/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx index 4d1f225..a18efd6 100644 --- a/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx +++ b/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx @@ -62,7 +62,7 @@ export function OrganizationSelectorContent() {
-

{t('selectorTitle')}

+

{t('selectorTitle')}

{canCreateOrganization ? t('selectorSubtitleWithCreate') : t('selectorSubtitleSelectOnly')}

@@ -71,6 +71,7 @@ export function OrganizationSelectorContent() {
{canCreateOrganization && isCreateOpen && ( -
+
{tAuth('organizationType')} -
+
)} -
+
@@ -158,14 +160,14 @@ export function OrganizationSelectorContent() { diff --git a/frontend/src/components/ui/patient/CreatePatientModal.tsx b/frontend/src/components/ui/patient/CreatePatientModal.tsx index 6f792ad..e1da9a7 100644 --- a/frontend/src/components/ui/patient/CreatePatientModal.tsx +++ b/frontend/src/components/ui/patient/CreatePatientModal.tsx @@ -4,6 +4,10 @@ import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/shared/Button'; import { DialogCloseButton } from '@/components/ui/shared/DialogCloseButton'; import { Input } from '@/components/ui/shared/Input'; +import { + ResponsiveDialogOverlay, + ResponsiveDialogPanel, +} from '@/components/ui/shared/ResponsiveDialog'; import { CreatePatientInput } from '@/types/patient'; import { isValidMobile, normalizeMobile } from '@/lib/phone'; @@ -63,17 +67,23 @@ function CreatePatientFormFields({ />
-
+
{showCancel && ( - )} @@ -99,21 +109,13 @@ export function CreatePatientModal({ if (variant === 'dialog') { return ( -
{ - if (e.target === e.currentTarget) { - onClose(); - } - }} - > -
+ e.stopPropagation()} + className="surface-card space-y-4" >

-

-
+ + ); } return ( -
+
( form-select w-full appearance-none rounded-[var(--radius-md)] border ${error ? 'border-red-500' : 'border-border'} bg-background-card text-text-primary - pl-4 pr-14 py-2 text-sm + pl-4 pr-14 py-2.5 sm:py-2 text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-primary/35 focus:border-border-strong disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)] diff --git a/frontend/src/components/ui/shared/Input.tsx b/frontend/src/components/ui/shared/Input.tsx index 6d7b13e..f7e4676 100644 --- a/frontend/src/components/ui/shared/Input.tsx +++ b/frontend/src/components/ui/shared/Input.tsx @@ -91,7 +91,7 @@ export const Input = forwardRef( ${error ? 'border-red-500' : 'border-border'} bg-background-secondary/90 text-text-primary - ${icon ? 'pl-10' : 'pl-4'} ${resolvedEndIcon ? 'pr-10' : 'pr-4'} py-2 + ${icon ? 'pl-10' : 'pl-4'} ${resolvedEndIcon ? 'pr-10' : 'pr-4'} py-2.5 sm:py-2 text-base sm:text-sm placeholder:text-text-muted diff --git a/frontend/src/components/ui/shared/LanguageToggle.tsx b/frontend/src/components/ui/shared/LanguageToggle.tsx index db8a1a8..0ed6c48 100644 --- a/frontend/src/components/ui/shared/LanguageToggle.tsx +++ b/frontend/src/components/ui/shared/LanguageToggle.tsx @@ -70,7 +70,7 @@ export function LanguageToggle() {
    {LOCALE_OPTIONS.map((option) => { const selected = option === locale; diff --git a/frontend/src/components/ui/shared/MobileDetailBackButton.tsx b/frontend/src/components/ui/shared/MobileDetailBackButton.tsx new file mode 100644 index 0000000..711c8aa --- /dev/null +++ b/frontend/src/components/ui/shared/MobileDetailBackButton.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { ChevronLeft } from 'lucide-react'; +import { useTranslations } from 'next-intl'; + +export function MobileDetailBackButton({ onClick }: { onClick: () => void }) { + const t = useTranslations('common'); + + return ( + + ); +} diff --git a/frontend/src/components/ui/shared/ResponsiveDialog.tsx b/frontend/src/components/ui/shared/ResponsiveDialog.tsx new file mode 100644 index 0000000..bcb4623 --- /dev/null +++ b/frontend/src/components/ui/shared/ResponsiveDialog.tsx @@ -0,0 +1,61 @@ +'use client'; + +import type { ReactNode } from 'react'; + +type ResponsiveDialogOverlayProps = { + children: ReactNode; + onBackdropClick?: () => void; + className?: string; + zIndexClass?: string; +}; + +type ResponsiveDialogPanelProps = { + children: ReactNode; + className?: string; + maxWidthClass?: string; + role?: string; + 'aria-modal'?: boolean | 'true' | 'false'; + 'aria-labelledby'?: string; +}; + +export function ResponsiveDialogOverlay({ + children, + onBackdropClick, + className = '', + zIndexClass = 'z-50', +}: ResponsiveDialogOverlayProps) { + return ( +
    { + if (onBackdropClick && e.target === e.currentTarget) { + onBackdropClick(); + } + }} + > + {children} +
    + ); +} + +export function ResponsiveDialogPanel({ + children, + className = '', + maxWidthClass = 'sm:max-w-lg', + role, + 'aria-modal': ariaModal, + 'aria-labelledby': ariaLabelledby, +}: ResponsiveDialogPanelProps) { + return ( +
    e.stopPropagation()} + > + {children} +
    + ); +} diff --git a/frontend/src/components/ui/shared/SearchBar.tsx b/frontend/src/components/ui/shared/SearchBar.tsx index fb1c9da..e0619d4 100644 --- a/frontend/src/components/ui/shared/SearchBar.tsx +++ b/frontend/src/components/ui/shared/SearchBar.tsx @@ -21,8 +21,8 @@ export function SearchBar({ embedded = false, }: SearchBarProps) { const field = ( -
    -
    +
    +
    } />
    - {actions &&
    {actions}
    } + {actions &&
    {actions}
    }
    ); @@ -41,5 +41,5 @@ export function SearchBar({ return field; } - return
    {field}
    ; + return
    {field}
    ; } diff --git a/frontend/src/components/ui/shared/Sidebar.tsx b/frontend/src/components/ui/shared/Sidebar.tsx index d5a3799..7113849 100644 --- a/frontend/src/components/ui/shared/Sidebar.tsx +++ b/frontend/src/components/ui/shared/Sidebar.tsx @@ -13,6 +13,7 @@ import { CreditCard, Package, ListTodo, + X, } from 'lucide-react'; import type { OrgTypeName } from '@/components/shared/permissions'; import { useAuth } from '@/lib/hooks/useAuth'; @@ -36,7 +37,12 @@ type MenuItem = { orgTypes: OrgTypeName[]; }; -function Sidebar() { +type SidebarProps = { + mobileOpen?: boolean; + onClose?: () => void; +}; + +function Sidebar({ mobileOpen = false, onClose }: SidebarProps) { const t = useTranslations('nav'); const tCommon = useTranslations('common'); const pathname = usePathname(); @@ -87,9 +93,23 @@ function Sidebar() { ); return ( -
+ {t('tableOrganization')} + + {t('tableOwnerEmail')} + + {t('tableDate')} + + {t('tableStatus')} + + {t('tableInvitationLink')} +
{inv.organizationName}{inv.ownerEmail} + {formatTableDate(inv.createdAt)} + + + {formatInvitationStatusLabel(inv.status)} + + + onCopy(inv)} + /> +
- - {headers} - - - {body} - -
+
+ + + {headers} + + + {body} + +
+
{footer && ( -
+
{footer}
)} diff --git a/frontend/src/components/ui/treatment/AppointmentsStrip.tsx b/frontend/src/components/ui/treatment/AppointmentsStrip.tsx index 8326448..6473585 100644 --- a/frontend/src/components/ui/treatment/AppointmentsStrip.tsx +++ b/frontend/src/components/ui/treatment/AppointmentsStrip.tsx @@ -104,7 +104,7 @@ export function AppointmentsStrip({ padding="none" style={treatmentTypeBannerStyle(a.purpose, purposeIndex < 0 ? 0 : purposeIndex)} className={` - text-left rounded-[var(--radius-sm)] px-3 py-2 min-w-[200px] max-w-[280px] transition-shadow min-h-[52px] + text-left rounded-[var(--radius-sm)] px-3 py-2 w-full sm:w-auto sm:min-w-[200px] sm:max-w-[280px] transition-shadow min-h-[52px] focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45 ${sel ? 'ring-2 ring-primary ring-offset-2 ring-offset-background-secondary shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]' : 'hover:brightness-110'} `} diff --git a/frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx b/frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx index 48be805..215ac78 100644 --- a/frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx +++ b/frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx @@ -445,7 +445,7 @@ export function LabCasesDispatchPanel({ e.target.value, ) } - className={`${FORM_SELECT_CLASS} w-full min-w-[160px]`} + className={`${FORM_SELECT_CLASS} w-full min-w-0 sm:min-w-[160px]`} > {prosthesisOptions.map((opt) => ( diff --git a/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx b/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx index aa867af..085e565 100644 --- a/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx +++ b/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx @@ -60,13 +60,20 @@ export function TreatmentDetailsEditor({ const showPendingLabHint = isLabDependent && !locked && !readOnly; return ( -
-
+
+

{t('detailsTitle')}

{t('detailsSubtitle')}

-
diff --git a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx index b03d4c6..80527d6 100644 --- a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx +++ b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx @@ -1107,7 +1107,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor return (
-

{t('title')}

+

{t('title')}

{canEdit ? t('subtitleEditPhase4') : t('subtitleReadOnly')}

diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 3e7998b..f185186 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -251,6 +251,14 @@ select.form-select, select { color: var(--color-text-primary); background-color: var(--color-background-card); + font-size: 1rem; +} + +@media (min-width: 640px) { + select.form-select, + select { + font-size: 0.875rem; + } } select option {