feature: dashboard settings and invite activation flow consistency fixed. frontend warnings fixed
This commit is contained in:
@@ -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;
|
||||
@@ -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 },
|
||||
@@ -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)}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
69
frontend/src/components/ui/patient/CreatePatientModal.tsx
Normal file
69
frontend/src/components/ui/patient/CreatePatientModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
63
frontend/src/components/ui/patient/PatientSearchSelect.tsx
Normal file
63
frontend/src/components/ui/patient/PatientSearchSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
frontend/src/components/ui/patient/PatientSummaryCard.tsx
Normal file
28
frontend/src/components/ui/patient/PatientSummaryCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user