feat: doctor visit log, schedule prediction, chart PDF, natural digest
- Doctor notes: add doctorName, reason, prescriptions, nextAppointmentAt fields (schema + API + UI) - Dashboard: baby schedule prediction widget (avg nap/bedtime/first-feed over 14 days) - Growth PDF: embed weight chart as image (SVG→Canvas capture) before data table - Weekly digest: natural language push notification instead of bullet stats Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,10 @@ interface DoctorNote {
|
||||
id: string;
|
||||
content: string;
|
||||
date: string;
|
||||
doctorName?: string | null;
|
||||
reason?: string | null;
|
||||
prescriptions?: string | null;
|
||||
nextAppointmentAt?: string | null;
|
||||
createdAt: string;
|
||||
userId: string;
|
||||
user: { id: string; name: string };
|
||||
@@ -29,8 +33,14 @@ export default function DoctorNotesPage() {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const [content, setContent] = useState("");
|
||||
const [date, setDate] = useState(today);
|
||||
const [doctorName, setDoctorName] = useState("");
|
||||
const [reason, setReason] = useState("");
|
||||
const [prescriptions, setPrescriptions] = useState("");
|
||||
const [nextAppointmentAt, setNextAppointmentAt] = useState("");
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const inputCls = "w-full px-3 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100";
|
||||
|
||||
const { data: notes = [], isLoading: notesLoading } = useQuery<DoctorNote[]>({
|
||||
queryKey: ["doctor-notes", selectedBaby?.id],
|
||||
queryFn: () =>
|
||||
@@ -39,7 +49,7 @@ export default function DoctorNotesPage() {
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (body: { babyId: string; content: string; date: string }) =>
|
||||
mutationFn: (body: { babyId: string; content: string; date: string; doctorName?: string; reason?: string; prescriptions?: string; nextAppointmentAt?: string }) =>
|
||||
fetch("/api/doctor-notes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -55,6 +65,10 @@ export default function DoctorNotesPage() {
|
||||
qc.invalidateQueries({ queryKey: ["doctor-notes", selectedBaby?.id] });
|
||||
setContent("");
|
||||
setDate(today);
|
||||
setDoctorName("");
|
||||
setReason("");
|
||||
setPrescriptions("");
|
||||
setNextAppointmentAt("");
|
||||
setFormError(null);
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
@@ -78,7 +92,15 @@ export default function DoctorNotesPage() {
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!selectedBaby?.id || !content.trim() || !date) return;
|
||||
createMutation.mutate({ babyId: selectedBaby.id, content: content.trim(), date });
|
||||
createMutation.mutate({
|
||||
babyId: selectedBaby.id,
|
||||
content: content.trim(),
|
||||
date,
|
||||
doctorName: doctorName || undefined,
|
||||
reason: reason || undefined,
|
||||
prescriptions: prescriptions || undefined,
|
||||
nextAppointmentAt: nextAppointmentAt || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (familyLoading) {
|
||||
@@ -135,6 +157,53 @@ export default function DoctorNotesPage() {
|
||||
className="w-full rounded-lg border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-slate-100 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 dark:focus:ring-emerald-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">
|
||||
Médecin <span className="text-slate-400 font-normal">(optionnel)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={doctorName}
|
||||
onChange={(e) => setDoctorName(e.target.value)}
|
||||
placeholder="Dr. Dupont"
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">
|
||||
Motif <span className="text-slate-400 font-normal">(optionnel)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
placeholder="Visite de routine, fièvre..."
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">
|
||||
Prescriptions <span className="text-slate-400 font-normal">(optionnel)</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={prescriptions}
|
||||
onChange={(e) => setPrescriptions(e.target.value)}
|
||||
rows={2}
|
||||
placeholder="Doliprane 2.4% si fièvre..."
|
||||
className={inputCls + " resize-none"}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">
|
||||
Prochain rendez-vous <span className="text-slate-400 font-normal">(optionnel)</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={nextAppointmentAt}
|
||||
onChange={(e) => setNextAppointmentAt(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">
|
||||
Observation
|
||||
@@ -214,6 +283,26 @@ export default function DoctorNotesPage() {
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 whitespace-pre-wrap leading-relaxed">
|
||||
{note.content}
|
||||
</p>
|
||||
{note.doctorName && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
<span className="font-medium">Médecin :</span> {note.doctorName}
|
||||
</p>
|
||||
)}
|
||||
{note.reason && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
<span className="font-medium">Motif :</span> {note.reason}
|
||||
</p>
|
||||
)}
|
||||
{note.prescriptions && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5 whitespace-pre-line">
|
||||
<span className="font-medium">Prescriptions :</span> {note.prescriptions}
|
||||
</p>
|
||||
)}
|
||||
{note.nextAppointmentAt && (
|
||||
<p className="text-xs text-indigo-600 dark:text-indigo-400 mt-1 font-medium">
|
||||
📅 Prochain RDV : {format(new Date(note.nextAppointmentAt), "d MMMM yyyy", { locale: fr })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user