feature: dashboard settings and invite activation flow consistency fixed. frontend warnings fixed

This commit is contained in:
2026-04-30 14:05:44 +03:30
parent 1b8856f64e
commit 1387e4cb5e
38 changed files with 375 additions and 362 deletions

View File

@@ -1,7 +1,7 @@
// src/components/ui/OrganizationCard.tsx
import React from 'react';
import { Building2, Beaker, ChevronRight } from 'lucide-react';
import { Organization } from '@/types';
import type { Organization } from '@/types/organization';
interface OrganizationCardProps {
organization: Organization;

View File

@@ -13,7 +13,7 @@ import {
CreditCard,
} from 'lucide-react';
import { useAuth } from '@/lib/hooks/useAuth';
import { canViewTab } from '@/access';
import { canViewTab } from '@/shared/permissions';
const menu = [
{ name: 'Today', path: '/today', icon: LayoutDashboard, read: 'TAB_TODAY_READ' as const },

View File

@@ -13,7 +13,7 @@ import {
} from 'lucide-react';
import { useAuth } from '@/lib/hooks/useAuth';
import { authApi } from '@/lib/api/auth';
import type { SubscriptionAlertData } from '@/types';
import type { SubscriptionAlertData } from '@/types/subscription';
function warningTooltip(data: SubscriptionAlertData | null): string {
if (!data?.showWarning) return '';
@@ -68,7 +68,7 @@ export function DashboardAccountMenu() {
}, [logout]);
return (
<div className="relative" ref={menuRef}>
<div className="relative z-[120]" ref={menuRef}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
@@ -94,7 +94,7 @@ export function DashboardAccountMenu() {
{open && (
<div
role="menu"
className="absolute right-0 mt-2 w-72 rounded-[var(--radius-md)] border border-border bg-background-secondary/95 py-2 shadow-lg z-50 backdrop-blur-sm"
className="absolute right-0 mt-2 w-72 rounded-[var(--radius-md)] border border-border bg-background-secondary/95 py-2 shadow-lg z-[200] backdrop-blur-sm"
>
<div className="px-3 py-2 border-b border-border/60">
<p className="text-xs text-text-muted">Signed in</p>
@@ -106,7 +106,7 @@ export function DashboardAccountMenu() {
<div className="py-1">
<Link
href="/select-organization"
href="/settings/organizations"
role="menuitem"
className="flex items-center gap-3 px-3 py-2.5 text-sm text-text-primary hover:bg-background-card/70"
onClick={() => setOpen(false)}

View File

@@ -0,0 +1,162 @@
'use client';
import { useState } from 'react';
import { useAuth } from '@/lib/hooks/useAuth';
import { Building2, Beaker, Mail, Plus } from 'lucide-react';
import { Input } from '@/components/ui/common/Input';
import { Button } from '@/components/ui/common/Button';
export function OrganizationSelectorContent() {
const { organizations, selectOrganization, createOrganization, isLoading, error, clearError } = useAuth();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [organizationName, setOrganizationName] = useState('');
const [organizationEmail, setOrganizationEmail] = useState('');
const [organizationType, setOrganizationType] = useState<'CLINIC' | 'LAB'>('CLINIC');
const getIcon = (type: string) =>
type === 'CLINIC' ? <Building2 className="h-8 w-8 icon-flat" /> : <Beaker className="h-8 w-8 icon-flat" />;
const handleCreateOrganization = async () => {
try {
clearError();
const createdId = await createOrganization(
organizationName.trim(),
organizationEmail.trim(),
organizationType,
);
setOrganizationName('');
setOrganizationEmail('');
setOrganizationType('CLINIC');
setIsCreateOpen(false);
await selectOrganization(createdId);
} catch {
// handled by auth context
}
};
if (isLoading) {
return <p className="text-text-secondary">Loading...</p>;
}
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-semibold text-text-primary">Organizations</h1>
<p className="text-text-secondary mt-2">
Select an organization to continue, or create a new one.
</p>
</div>
<Button
type="button"
variant={isCreateOpen ? 'outline' : 'primary'}
onClick={() => {
clearError();
setIsCreateOpen((prev) => !prev);
}}
>
<Plus className="h-4 w-4 mr-2 icon-flat" />
{isCreateOpen ? 'Cancel' : 'Create Organization'}
</Button>
</div>
{isCreateOpen && (
<div className="surface-card p-6 space-y-4">
<Input
label="Organization name"
value={organizationName}
onChange={(event) => setOrganizationName(event.target.value)}
placeholder="Sunshine Dental Clinic"
icon={<Building2 className="h-5 w-5 icon-flat" />}
/>
<Input
label="Organization email"
value={organizationEmail}
onChange={(event) => setOrganizationEmail(event.target.value)}
placeholder="contact@sunshineclinic.com"
type="email"
icon={<Mail className="h-5 w-5 icon-flat" />}
/>
<div>
<label className="block text-sm font-medium text-text-secondary mb-2">
Organization type
</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setOrganizationType('CLINIC')}
className={`p-3 border rounded-[var(--radius-md)] text-sm ${
organizationType === 'CLINIC'
? 'border-primary/60 bg-primary-soft text-text-primary'
: 'border-border text-text-secondary hover:border-border-strong'
}`}
>
Dental Clinic
</button>
<button
type="button"
onClick={() => setOrganizationType('LAB')}
className={`p-3 border rounded-[var(--radius-md)] text-sm ${
organizationType === 'LAB'
? 'border-primary/60 bg-primary-soft text-text-primary'
: 'border-border text-text-secondary hover:border-border-strong'
}`}
>
Dental Lab
</button>
</div>
</div>
{error && (
<div className="p-3 bg-red-950/30 border border-red-600/40 rounded-[var(--radius-md)]">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
<div className="flex justify-end">
<Button
type="button"
variant="primary"
onClick={handleCreateOrganization}
isLoading={isLoading}
disabled={!organizationName.trim() || !organizationEmail.trim()}
>
Create and Continue
</Button>
</div>
</div>
)}
{!organizations.length ? (
<div className="surface-card p-8 text-center">
<p className="text-text-secondary">No organizations found. Create your first one to continue.</p>
</div>
) : (
<div className="grid gap-4">
{organizations.map((org) => (
<button
key={org.id}
onClick={() => selectOrganization(org.id)}
className="surface-card p-6 transition-all text-left flex items-center gap-4 hover:border-primary/60"
>
<div className="p-3 bg-primary-soft rounded-[var(--radius-sm)] text-primary">
{getIcon(org.type)}
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-text-primary">
{org.name}
</h3>
<p className="text-sm text-text-secondary">
{org.type === 'CLINIC' ? 'Dental Clinic' : 'Dental Lab'}
</p>
</div>
<div className="text-primary text-sm">
Continue
</div>
</button>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,69 @@
'use client';
import { Button } from '@/components/ui/common/Button';
import { Input } from '@/components/ui/common/Input';
import { CreatePatientInput } from '@/types/patient';
interface CreatePatientModalProps {
isOpen: boolean;
formData: CreatePatientInput;
onChange: (patch: Partial<CreatePatientInput>) => void;
onSubmit: () => void;
onClose: () => void;
loading?: boolean;
}
export function CreatePatientModal({
isOpen,
formData,
onChange,
onSubmit,
onClose,
loading = false,
}: CreatePatientModalProps) {
if (!isOpen) {
return null;
}
return (
<div className="surface-card p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Input
label="First name"
value={formData.firstName || ''}
onChange={(e) => onChange({ firstName: e.target.value })}
/>
<Input
label="Last name"
value={formData.lastName || ''}
onChange={(e) => onChange({ lastName: e.target.value })}
/>
<Input
label="Phone"
value={formData.phone || ''}
onChange={(e) => onChange({ phone: e.target.value })}
/>
<Input
label="Email"
type="email"
value={formData.email || ''}
onChange={(e) => onChange({ email: e.target.value })}
/>
</div>
<div className="flex gap-2">
<Button
variant="primary"
onClick={onSubmit}
isLoading={loading}
disabled={!formData.firstName || !formData.lastName}
>
Save Patient
</Button>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,63 @@
'use client';
import { Search } from 'lucide-react';
import { Input } from '@/components/ui/common/Input';
import { Patient } from '@/types/patient';
interface PatientSearchSelectProps {
search: string;
onSearchChange: (value: string) => void;
patients: Patient[];
selectedPatientId?: string;
onSelectPatient: (patient: Patient) => void;
loading?: boolean;
}
export function PatientSearchSelect({
search,
onSearchChange,
patients,
selectedPatientId,
onSelectPatient,
loading = false,
}: PatientSearchSelectProps) {
return (
<div className="surface-card p-4 space-y-4">
<Input
placeholder="Search patients by name, phone, email"
value={search}
onChange={(e) => onSearchChange(e.target.value)}
icon={<Search className="h-4 w-4 icon-flat" />}
/>
<div className="space-y-2 max-h-80 overflow-y-auto">
{loading && <p className="text-sm text-text-muted">Loading patients...</p>}
{!loading && patients.length === 0 && (
<p className="text-sm text-text-muted">No patients found for this search.</p>
)}
{patients.map((patient) => {
const isSelected = selectedPatientId === patient.id;
return (
<button
key={patient.id}
type="button"
onClick={() => onSelectPatient(patient)}
className={`w-full text-left rounded-[var(--radius-sm)] border px-3 py-2 transition-colors ${
isSelected
? 'bg-primary-soft border-primary/60'
: 'border-border/60 hover:bg-background-card/70'
}`}
>
<p className="text-sm font-medium text-text-primary">
{patient.firstName} {patient.lastName}
</p>
<p className="text-xs text-text-muted">{patient.phone || patient.email || 'No contact'}</p>
</button>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
import { Patient } from '@/types/patient';
interface PatientSummaryCardProps {
patient?: Patient;
}
export function PatientSummaryCard({ patient }: PatientSummaryCardProps) {
if (!patient) {
return (
<div className="surface-card p-4">
<p className="text-sm text-text-muted">Select a patient to view details.</p>
</div>
);
}
return (
<div className="surface-card p-4 space-y-2">
<h2 className="text-lg font-semibold text-text-primary">
{patient.firstName} {patient.lastName}
</h2>
<p className="text-sm text-text-secondary">Phone: {patient.phone || '-'}</p>
<p className="text-sm text-text-secondary">Email: {patient.email || '-'}</p>
<p className="text-sm text-text-secondary">
Status: {patient.isActive ? 'Active' : 'Inactive'}
</p>
</div>
);
}

View File

@@ -0,0 +1,37 @@
import { TreatmentHistoryItem } from '@/types/patient';
interface TreatmentHistoryPreviewProps {
items: TreatmentHistoryItem[];
loading?: boolean;
}
export function TreatmentHistoryPreview({ items, loading = false }: TreatmentHistoryPreviewProps) {
return (
<div className="surface-card p-4 space-y-3">
<h3 className="text-base font-semibold text-text-primary">Treatment History</h3>
{loading && <p className="text-sm text-text-muted">Loading treatment history...</p>}
{!loading && items.length === 0 && (
<p className="text-sm text-text-muted">No treatment history yet.</p>
)}
<div className="space-y-2">
{items.map((item) => (
<div key={item.id} className="border border-border/60 rounded-[var(--radius-sm)] p-3">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-text-primary">{item.title}</p>
<p className="text-xs text-text-muted">
{new Date(item.treatmentAt).toLocaleDateString()}
</p>
</div>
<p className="text-xs text-text-secondary mt-1">
Status: {item.status}
{item.tooth ? ` | Tooth: ${item.tooth}` : ''}
</p>
</div>
))}
</div>
</div>
);
}