MeshiTrack/packages/web/src/app/(dashboard)/medicines/LibraryTab.tsx

345 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { listMedicines, createMedicine, deleteMedicine } from '@/services/medicines';
import { MedicineForm, StrengthUnit, MedicineCategory } from '@meshitrack/shared';
import type { CreateMedicineInput } from '@meshitrack/shared';
type Medicine = {
_id: string;
name: string;
form: string;
strength: number;
strengthUnit: string;
category: string;
tags: string[];
};
const FORM_LABELS: Record<string, string> = {
tablet: 'Tablet',
capsule: 'Capsule',
liquid: 'Liquid',
injection: 'Injection',
other: 'Other',
};
const CATEGORY_LABELS: Record<string, string> = {
prescription: 'Prescription',
otc: 'OTC',
supplement: 'Supplement',
other: 'Other',
};
const CATEGORY_PILL: Record<string, string> = {
prescription: 'mt-pill--info',
otc: 'mt-pill--ok',
supplement: 'mt-pill--brand',
other: 'mt-pill--ghost',
};
export function LibraryTab({ householdId }: { householdId: string }) {
const [medicines, setMedicines] = useState<Medicine[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [showForm, setShowForm] = useState(false);
const [search, setSearch] = useState('');
const [filterCategory, setFilterCategory] = useState('');
const [filterForm, setFilterForm] = useState('');
const fetchMedicines = useCallback(async () => {
if (!householdId) return;
setLoading(true);
try {
const result = await listMedicines(householdId, {
q: search || undefined,
category: filterCategory || undefined,
form: filterForm || undefined,
limit: 50,
});
setMedicines(result.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load medicines');
} finally {
setLoading(false);
}
}, [householdId, search, filterCategory, filterForm]);
useEffect(() => {
if (householdId) {
fetchMedicines();
}
}, [householdId, fetchMedicines]);
async function handleDelete(id: string, name: string) {
if (!householdId || !confirm(`Delete "${name}"?`)) return;
try {
await deleteMedicine(householdId, id);
setMedicines((prev) => prev.filter((m) => m._id !== id));
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete');
}
}
return (
<div>
<div className="flex items-center justify-between mb-4">
<div />
<button onClick={() => setShowForm(!showForm)} className="mt-btn mt-btn--primary">
{showForm ? 'Cancel' : 'Add Medicine'}
</button>
</div>
{error && (
<div className="mt-alert mt-alert--danger mb-4">
{error}
<button onClick={() => setError('')} className="ml-2 underline">
Dismiss
</button>
</div>
)}
{showForm && (
<CreateMedicineForm
householdId={householdId}
onCreated={() => {
setShowForm(false);
fetchMedicines();
}}
onCancel={() => setShowForm(false)}
/>
)}
<div className="mb-4 flex flex-wrap items-center gap-3">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search medicines..."
className="mt-field max-w-md"
/>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="mt-field"
style={{ width: 'auto' }}
>
<option value="">All Categories</option>
{Object.values(MedicineCategory).map((c) => (
<option key={c} value={c}>
{CATEGORY_LABELS[c] ?? c}
</option>
))}
</select>
<select
value={filterForm}
onChange={(e) => setFilterForm(e.target.value)}
className="mt-field"
style={{ width: 'auto' }}
>
<option value="">All Forms</option>
{Object.values(MedicineForm).map((f) => (
<option key={f} value={f}>
{FORM_LABELS[f] ?? f}
</option>
))}
</select>
</div>
{loading ? (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="animate-pulse rounded-xl border bg-white p-4 h-20" />
))}
</div>
) : medicines.length === 0 ? (
<div className="rounded-xl border bg-white p-6 shadow-sm text-center text-gray-500">
{search || filterCategory || filterForm
? 'No medicines found matching your filters.'
: 'No medicines yet. Add your first one above.'}
</div>
) : (
<div className="space-y-3">
{medicines.map((med) => (
<div
key={med._id}
className="rounded-xl border bg-white p-4 shadow-sm flex items-center justify-between"
>
<Link href={`/medicines/${med._id}`} className="flex-1 min-w-0">
<div className="flex items-center gap-3">
<div>
<h3 className="font-medium text-gray-900">{med.name}</h3>
<p className="text-sm text-gray-500">
{med.strength} {med.strengthUnit} {FORM_LABELS[med.form] ?? med.form}
</p>
</div>
</div>
</Link>
<div className="flex items-center gap-3 ml-4">
<span
className={`mt-pill ${CATEGORY_PILL[med.category] ?? CATEGORY_PILL['other']}`}
>
{CATEGORY_LABELS[med.category] ?? med.category}
</span>
<button
onClick={() => handleDelete(med._id, med.name)}
className="mt-btn mt-btn--danger-icon"
title="Delete"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}
function CreateMedicineForm({
householdId,
onCreated,
onCancel,
}: {
householdId: string;
onCreated: () => void;
onCancel: () => void;
}) {
const [formData, setFormData] = useState<CreateMedicineInput>({
name: '',
form: MedicineForm.TABLET,
strength: 0,
strengthUnit: StrengthUnit.MG,
category: MedicineCategory.OTC,
tags: [],
});
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setSubmitting(true);
try {
await createMedicine(householdId, {
...formData,
name: formData.name.trim(),
strength: Number(formData.strength),
});
onCreated();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create medicine');
} finally {
setSubmitting(false);
}
}
return (
<div className="mt-card mb-6">
<h2 className="text-lg font-semibold mb-4">Add Medicine</h2>
{error && <div className="mt-alert mt-alert--danger mb-4">{error}</div>}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className="mt-field-label">Name</label>
<input
type="text"
required
maxLength={200}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="e.g. Metformin"
className="mt-field"
/>
</div>
<div>
<label className="mt-field-label">Form</label>
<select
value={formData.form}
onChange={(e) => setFormData({ ...formData, form: e.target.value as MedicineForm })}
className="mt-field"
>
{Object.values(MedicineForm).map((f) => (
<option key={f} value={f}>
{/* v8 ignore next */ FORM_LABELS[f] ?? f}
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mt-field-label">Strength</label>
<input
type="number"
required
min={0.01}
step="any"
value={formData.strength || ''}
onChange={(e) => setFormData({ ...formData, strength: Number(e.target.value) })}
placeholder="500"
className="mt-field"
/>
</div>
<div>
<label className="mt-field-label">Unit</label>
<select
value={formData.strengthUnit}
onChange={(e) =>
setFormData({ ...formData, strengthUnit: e.target.value as StrengthUnit })
}
className="mt-field"
>
{Object.values(StrengthUnit).map((u) => (
<option key={u} value={u}>
{u}
</option>
))}
</select>
</div>
</div>
<div>
<label className="mt-field-label">Category</label>
<select
value={formData.category}
onChange={(e) =>
setFormData({ ...formData, category: e.target.value as MedicineCategory })
}
className="mt-field"
>
{Object.values(MedicineCategory).map((c) => (
<option key={c} value={c}>
{/* v8 ignore next */ CATEGORY_LABELS[c] ?? c}
</option>
))}
</select>
</div>
<div>
<label className="mt-field-label">Notes (optional)</label>
<input
type="text"
maxLength={1000}
value={formData.notes ?? ''}
onChange={(e) => setFormData({ ...formData, notes: e.target.value || undefined })}
placeholder="Any additional notes"
className="mt-field"
/>
</div>
</div>
<div className="flex gap-3 pt-2">
<button type="submit" disabled={submitting} className="mt-btn mt-btn--primary">
{submitting ? 'Creating...' : 'Create Medicine'}
</button>
<button type="button" onClick={onCancel} className="mt-btn mt-btn--ghost">
Cancel
</button>
</div>
</form>
</div>
);
}