forked from RiseUP/riseup-squad21
340 lines
15 KiB
TypeScript
340 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
import { Calendar } from "@/components/ui/calendar";
|
|
import { CalendarIcon, Clock, User, Trash2 } from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import TiptapEditor from "@/components/ui/tiptap-editor";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { toast } from "@/hooks/use-toast";
|
|
import Link from "next/link";
|
|
|
|
// [CORREÇÃO] Adicionando a importação que faltava para o AlertDialog
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
// [TIPAGEM] Importando tipos da camada de serviço
|
|
import { relatoriosApi, Report } from "@/services/relatoriosApi";
|
|
import { disponibilidadeApi, Availability } from "@/services/disponibilidadeApi";
|
|
import { excecoesApi, Exception } from "@/services/excecoesApi";
|
|
|
|
export default function EditarLaudoPage() {
|
|
const router = useRouter();
|
|
const params = useParams();
|
|
const patientId = params.id as string;
|
|
const laudoId = params.laudoId as string;
|
|
|
|
// [ESTADO] & [TIPAGEM] Estados robustos e tipados
|
|
const [formData, setFormData] = useState<Partial<Report>>({});
|
|
const [availability, setAvailability] = useState<Availability[]>([]);
|
|
const [exceptions, setExceptions] = useState<Exception[]>([]);
|
|
const [schedule, setSchedule] = useState<Record<string, { start: string; end: string }[]>>({});
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [exceptionToDelete, setExceptionToDelete] = useState<string | null>(null);
|
|
|
|
const formatTime = (time: string) => (time ? time.split(":").slice(0, 2).join(":") : "");
|
|
|
|
const weekdaysPT: Record<string, string> = {
|
|
sunday: "Domingo",
|
|
monday: "Segunda",
|
|
tuesday: "Terça",
|
|
wednesday: "Quarta",
|
|
thursday: "Quinta",
|
|
friday: "Sexta",
|
|
saturday: "Sábado",
|
|
};
|
|
|
|
// [API] & [ESTADO] Lógica de busca de dados centralizada e robusta
|
|
useEffect(() => {
|
|
if (!laudoId) {
|
|
setIsLoading(false);
|
|
setError("ID do laudo não encontrado.");
|
|
return;
|
|
}
|
|
|
|
const fetchData = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
// TODO: Remover ID fixo e obter do usuário logado
|
|
const doctorId = "3bb9ee4a-cfdd-4d81-b628-383907dfa225";
|
|
|
|
const [reportData, availabilityResponse, exceptionsResponse] = await Promise.all([
|
|
relatoriosApi.getById(laudoId),
|
|
disponibilidadeApi.list(),
|
|
excecoesApi.list(),
|
|
]);
|
|
|
|
if (reportData) {
|
|
setFormData({
|
|
...reportData,
|
|
due_at: reportData.due_at ? new Date(reportData.due_at) : null,
|
|
});
|
|
} else {
|
|
throw new Error("Laudo não encontrado.");
|
|
}
|
|
|
|
setAvailability(availabilityResponse.filter(disp => disp.doctor_id === doctorId));
|
|
setExceptions(exceptionsResponse.filter(exc => exc.doctor_id === doctorId));
|
|
|
|
} catch (e: any) {
|
|
setError("Falha ao carregar os dados. Por favor, tente novamente.");
|
|
console.error("Failed to fetch data:", e);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [laudoId]);
|
|
|
|
const openDeleteDialog = (exceptionId: string) => {
|
|
setExceptionToDelete(exceptionId);
|
|
setDeleteDialogOpen(true);
|
|
};
|
|
|
|
const handleDeleteException = async (exceptionId: string) => {
|
|
try {
|
|
await excecoesApi.delete(exceptionId);
|
|
toast({
|
|
title: "Sucesso",
|
|
description: "Exceção deletada com sucesso.",
|
|
});
|
|
setExceptions((prev) => prev.filter((p) => p.id !== exceptionId));
|
|
} catch (e: any) {
|
|
toast({
|
|
title: "Erro",
|
|
description: e?.message || "Não foi possível deletar a exceção.",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setDeleteDialogOpen(false);
|
|
setExceptionToDelete(null);
|
|
}
|
|
};
|
|
|
|
function formatAvailability(data: Availability[]) {
|
|
return data.reduce(
|
|
(acc: Record<string, { start: string; end: string }[]>, item) => {
|
|
const { weekday, start_time, end_time } = item;
|
|
if (!acc[weekday]) {
|
|
acc[weekday] = [];
|
|
}
|
|
acc[weekday].push({ start: start_time, end: end_time });
|
|
return acc;
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (availability.length) {
|
|
const formatted = formatAvailability(availability);
|
|
setSchedule(formatted);
|
|
}
|
|
}, [availability]);
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
const { id, value } = e.target;
|
|
setFormData((prev) => ({ ...prev, [id]: value }));
|
|
};
|
|
|
|
const handleSelectChange = (id: string, value: string) => {
|
|
setFormData((prev) => ({ ...prev, [id]: value }));
|
|
};
|
|
|
|
const handleCheckboxChange = (id: string, checked: boolean) => {
|
|
setFormData((prev) => ({ ...prev, [id]: checked }));
|
|
};
|
|
|
|
const handleDateSelect = (date: Date | undefined) => {
|
|
if (date) {
|
|
setFormData((prev) => ({ ...prev, due_at: date }));
|
|
}
|
|
setIsDatePickerOpen(false);
|
|
};
|
|
|
|
const handleEditorChange = (html: string, json: object) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
content_html: html,
|
|
content_json: json
|
|
}));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setIsSubmitting(true);
|
|
try {
|
|
const { id, patient_id, created_at, updated_at, created_by, updated_by, ...updateData } = formData;
|
|
await relatoriosApi.update(laudoId, updateData);
|
|
toast({ title: "Laudo atualizado com sucesso!" });
|
|
router.push(`/doctor/medicos/${patientId}/laudos`);
|
|
} catch (error) {
|
|
console.error("Failed to update laudo", error);
|
|
toast({ title: "Erro ao atualizar laudo", variant: "destructive" });
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// [UI] Feedback Visual
|
|
if (isLoading) {
|
|
return (
|
|
<div className="container mx-auto p-4">
|
|
<Card>
|
|
<CardHeader><Skeleton className="h-8 w-1/4" /></CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2"><Skeleton className="h-4 w-1/6" /><Skeleton className="h-10 w-full" /></div>
|
|
<div className="space-y-2"><Skeleton className="h-4 w-1/6" /><Skeleton className="h-10 w-full" /></div>
|
|
</div>
|
|
<div className="space-y-2"><Skeleton className="h-4 w-1/6" /><Skeleton className="h-40 w-full" /></div>
|
|
<div className="flex justify-end space-x-2"><Skeleton className="h-10 w-24" /><Skeleton className="h-10 w-24" /></div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return <div className="container mx-auto p-4 text-red-600">Erro: {error}</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto p-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Editar Laudo - {formData.order_number}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="order_number">Nº do Pedido</Label>
|
|
<Input id="order_number" value={formData.order_number || ''} onChange={handleInputChange} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="exam">Exame</Label>
|
|
<Input id="exam" value={formData.exam || ''} onChange={handleInputChange} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="diagnosis">Diagnóstico</Label>
|
|
<Input id="diagnosis" value={formData.diagnosis || ''} onChange={handleInputChange} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cid_code">Código CID</Label>
|
|
<Input id="cid_code" value={formData.cid_code || ''} onChange={handleInputChange} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="requested_by">Solicitado Por</Label>
|
|
<Input id="requested_by" value={formData.requested_by || ''} onChange={handleInputChange} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="status">Status</Label>
|
|
<Select onValueChange={(value) => handleSelectChange("status", value)} value={formData.status}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Selecione o status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="draft">Rascunho</SelectItem>
|
|
<SelectItem value="final">Finalizado</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="due_at">Data de Vencimento</Label>
|
|
<Dialog open={isDatePickerOpen} onOpenChange={setIsDatePickerOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant={"outline"} className="w-full justify-start text-left font-normal">
|
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
{formData.due_at ? format(new Date(formData.due_at), "PPP") : <span>Escolha uma data</span>}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="w-auto p-0">
|
|
<Calendar
|
|
mode="single"
|
|
selected={formData.due_at ? new Date(formData.due_at) : undefined}
|
|
onSelect={handleDateSelect}
|
|
initialFocus
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="conclusion">Conclusão</Label>
|
|
<Textarea id="conclusion" value={formData.conclusion || ''} onChange={handleInputChange} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Conteúdo do Laudo</Label>
|
|
<div className="rounded-md border border-input">
|
|
<TiptapEditor content={formData.content_html || ''} onChange={handleEditorChange} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="hide_date" checked={formData.hide_date} onCheckedChange={(checked) => handleCheckboxChange("hide_date", !!checked)} />
|
|
<Label htmlFor="hide_date">Ocultar Data</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="hide_signature" checked={formData.hide_signature} onCheckedChange={(checked) => handleCheckboxChange("hide_signature", !!checked)} />
|
|
<Label htmlFor="hide_signature">Ocultar Assinatura</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-2">
|
|
<Button type="button" variant="outline" onClick={() => router.back()} disabled={isSubmitting}>
|
|
Cancelar
|
|
</Button>
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
{isSubmitting ? "Salvando..." : "Salvar Alterações"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
|
|
<AlertDialogDescription>Tem certeza que deseja excluir esta exceção? Esta ação não pode ser desfeita.</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => exceptionToDelete && handleDeleteException(exceptionToDelete)} className="bg-red-600 hover:bg-red-700">
|
|
Excluir
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
} |