feature: a very minimal patients feature implemented. it needs lots of improvments though
This commit is contained in:
69
frontend/src/components/patients/CreatePatientModal.tsx
Normal file
69
frontend/src/components/patients/CreatePatientModal.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/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/patients/PatientSearchSelect.tsx
Normal file
63
frontend/src/components/patients/PatientSearchSelect.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { Search } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/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/patients/PatientSummaryCard.tsx
Normal file
28
frontend/src/components/patients/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>
|
||||
);
|
||||
}
|
||||
37
frontend/src/components/patients/TreatmentHistoryPreview.tsx
Normal file
37
frontend/src/components/patients/TreatmentHistoryPreview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user