aplicação de laudo com API #23
@ -19,19 +19,20 @@ interface AccessibilityContextProps {
|
|||||||
const AccessibilityContext = createContext<AccessibilityContextProps | undefined>(undefined);
|
const AccessibilityContext = createContext<AccessibilityContextProps | undefined>(undefined);
|
||||||
|
|
||||||
export const AccessibilityProvider = ({ children }: { children: ReactNode }) => {
|
export const AccessibilityProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [theme, setThemeState] = useState<Theme>(() => {
|
const [theme, setThemeState] = useState<Theme>('light');
|
||||||
if (typeof window === 'undefined') return 'light';
|
const [contrast, setContrastState] = useState<Contrast>('normal');
|
||||||
return (localStorage.getItem('accessibility-theme') as Theme) || 'light';
|
const [fontSize, setFontSize] = useState<number>(16);
|
||||||
});
|
|
||||||
const [contrast, setContrastState] = useState<Contrast>(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return 'normal';
|
const storedTheme = (localStorage.getItem('accessibility-theme') as Theme) || 'light';
|
||||||
return (localStorage.getItem('accessibility-contrast') as Contrast) || 'normal';
|
const storedContrast = (localStorage.getItem('accessibility-contrast') as Contrast) || 'normal';
|
||||||
});
|
|
||||||
const [fontSize, setFontSize] = useState<number>(() => {
|
|
||||||
if (typeof window === 'undefined') return 16;
|
|
||||||
const storedSize = localStorage.getItem('accessibility-font-size');
|
const storedSize = localStorage.getItem('accessibility-font-size');
|
||||||
return storedSize ? parseFloat(storedSize) : 16;
|
setThemeState(storedTheme);
|
||||||
});
|
setContrastState(storedContrast);
|
||||||
|
if (storedSize) {
|
||||||
|
setFontSize(parseFloat(storedSize));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|||||||
237
app/doctor/medicos/[id]/laudos/[laudoId]/editar/page.tsx
Normal file
237
app/doctor/medicos/[id]/laudos/[laudoId]/editar/page.tsx
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import DoctorLayout from "@/components/doctor-layout";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } 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 } from "lucide-react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import TiptapEditor from "@/components/ui/tiptap-editor";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { reportsApi } from "@/services/reportsApi.mjs";
|
||||||
|
|
||||||
|
export default function EditarLaudoPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const patientId = params.id as string;
|
||||||
|
const laudoId = params.laudoId as string;
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<any>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (laudoId) {
|
||||||
|
setLoading(true);
|
||||||
|
reportsApi.getReportById(laudoId)
|
||||||
|
.then((data: any) => {
|
||||||
|
console.log("Fetched report data:", data);
|
||||||
|
// The API now returns an array, get the first element
|
||||||
|
const reportData = Array.isArray(data) && data.length > 0 ? data[0] : null;
|
||||||
|
if (reportData) {
|
||||||
|
setFormData({
|
||||||
|
...reportData,
|
||||||
|
due_at: reportData.due_at ? new Date(reportData.due_at) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Failed to fetch report details:", error);
|
||||||
|
// Here you could add a toast notification to inform the user
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If there's no laudoId, we shouldn't be in a loading state.
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [laudoId]);
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const { id, value } = e.target;
|
||||||
|
setFormData((prev: any) => ({ ...prev, [id]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (id: string, value: string) => {
|
||||||
|
setFormData((prev: any) => ({ ...prev, [id]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (id: string, checked: boolean) => {
|
||||||
|
setFormData((prev: any) => ({ ...prev, [id]: checked }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateChange = (date: Date | undefined) => {
|
||||||
|
console.log("Date selected:", date);
|
||||||
|
if (date) {
|
||||||
|
setFormData((prev: any) => ({ ...prev, due_at: date }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateSelect = (date: Date | undefined) => {
|
||||||
|
handleDateChange(date);
|
||||||
|
setIsDatePickerOpen(false); // Close the dialog after selection
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorChange = (html: string, json: object) => {
|
||||||
|
setFormData((prev: any) => ({
|
||||||
|
...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 reportsApi.updateReport(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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<DoctorLayout>
|
||||||
|
<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 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-24 w-full" /></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>
|
||||||
|
</DoctorLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DoctorLayout>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</DoctorLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
194
app/doctor/medicos/[id]/laudos/novo/page.tsx
Normal file
194
app/doctor/medicos/[id]/laudos/novo/page.tsx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } 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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import { CalendarIcon } from "lucide-react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import TiptapEditor from "@/components/ui/tiptap-editor";
|
||||||
|
|
||||||
|
import { reportsApi } from "@/services/reportsApi.mjs";
|
||||||
|
import DoctorLayout from "@/components/doctor-layout";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function NovoLaudoPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const patientId = params.id as string;
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
order_number: "",
|
||||||
|
exam: "",
|
||||||
|
diagnosis: "",
|
||||||
|
conclusion: "",
|
||||||
|
cid_code: "",
|
||||||
|
content_html: "",
|
||||||
|
content_json: {}, // Added for the JSON content from the editor
|
||||||
|
status: "draft",
|
||||||
|
requested_by: "",
|
||||||
|
due_at: new Date(),
|
||||||
|
hide_date: false,
|
||||||
|
hide_signature: false,
|
||||||
|
});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
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 handleDateChange = (date: Date | undefined) => {
|
||||||
|
if (date) {
|
||||||
|
setFormData(prev => ({ ...prev, due_at: date }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Updated to handle both HTML and JSON from the editor
|
||||||
|
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 laudoData = {
|
||||||
|
...formData,
|
||||||
|
patient_id: patientId,
|
||||||
|
due_at: formData.due_at.toISOString(), // Ensure date is in ISO format for the API
|
||||||
|
};
|
||||||
|
|
||||||
|
await reportsApi.createReport(laudoData);
|
||||||
|
|
||||||
|
// You can use a toast notification here for better user feedback
|
||||||
|
// toast({ title: "Laudo criado com sucesso!" });
|
||||||
|
|
||||||
|
router.push(`/doctor/medicos/${patientId}/laudos`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to create laudo", error);
|
||||||
|
// You can use a toast notification for errors
|
||||||
|
// toast({ title: "Erro ao criar laudo", description: error.message, variant: "destructive" });
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DoctorLayout>
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Criar Novo Laudo</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)} defaultValue={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>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger 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(formData.due_at, "PPP") : <span>Escolha uma data</span>}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0">
|
||||||
|
<Calendar mode="single" selected={formData.due_at} onSelect={handleDateChange} initialFocus />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</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 Laudo"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</DoctorLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,60 +1,128 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { Button } from '@/components/ui/button';
|
||||||
import DoctorLayout from "@/components/doctor-layout";
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import dynamic from "next/dynamic";
|
import Link from 'next/link';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { api } from '@/services/api.mjs';
|
||||||
|
import { reportsApi } from '@/services/reportsApi.mjs';
|
||||||
|
import DoctorLayout from '@/components/doctor-layout';
|
||||||
|
|
||||||
const Tiptap = dynamic(() => import("@/components/ui/tiptap-editor"), { ssr: false });
|
export default function LaudosPage() {
|
||||||
|
const [patient, setPatient] = useState(null);
|
||||||
|
const [laudos, setLaudos] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const params = useParams();
|
||||||
|
const patientId = params.id as string;
|
||||||
|
|
||||||
export default function LaudoEditorPage() {
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [laudoContent, setLaudoContent] = useState("");
|
const [itemsPerPage] = useState(5);
|
||||||
const [paciente, setPaciente] = useState<{ id: string; nome: string } | null>(null);
|
|
||||||
const params = useParams();
|
|
||||||
const router = useRouter();
|
|
||||||
const pacienteId = params.id;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pacienteId) {
|
if (patientId) {
|
||||||
// Em um caso real, você faria uma chamada de API para buscar os dados do paciente
|
const fetchPatientAndLaudos = async () => {
|
||||||
setPaciente({ id: pacienteId as string, nome: `Paciente ${pacienteId}` });
|
setLoading(true);
|
||||||
setLaudoContent(`<p>Laudo para o paciente ${paciente?.nome || ""}</p>`);
|
try {
|
||||||
}
|
const patientData = await api.get(`/rest/v1/patients?id=eq.${patientId}&select=*`).then(r => r?.[0]);
|
||||||
}, [pacienteId, paciente?.nome]);
|
setPatient(patientData);
|
||||||
|
|
||||||
const handleSave = () => {
|
const laudosData = await reportsApi.getReports(patientId);
|
||||||
console.log("Salvando laudo para o paciente ID:", pacienteId);
|
setLaudos(laudosData);
|
||||||
console.log("Conteúdo:", laudoContent);
|
} catch (error) {
|
||||||
// Aqui você implementaria a lógica para salvar o laudo no backend
|
console.error("Failed to fetch data:", error);
|
||||||
alert("Laudo salvo com sucesso!");
|
} finally {
|
||||||
};
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleContentChange = (richText: string) => {
|
fetchPatientAndLaudos();
|
||||||
setLaudoContent(richText);
|
}
|
||||||
};
|
}, [patientId]);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const indexOfLastItem = currentPage * itemsPerPage;
|
||||||
router.back();
|
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||||
};
|
const currentItems = laudos.slice(indexOfFirstItem, indexOfLastItem);
|
||||||
|
const totalPages = Math.ceil(laudos.length / itemsPerPage);
|
||||||
|
|
||||||
return (
|
const paginate = (pageNumber) => setCurrentPage(pageNumber);
|
||||||
<DoctorLayout>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Editor de Laudo</h1>
|
|
||||||
{paciente && <p className="text-gray-600">Editando laudo de: {paciente.nome}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
return (
|
||||||
<Tiptap content={laudoContent} onChange={handleContentChange} />
|
<DoctorLayout>
|
||||||
</div>
|
<div className="container mx-auto p-4">
|
||||||
|
{loading ? (
|
||||||
<div className="flex justify-end gap-4">
|
<p>Carregando...</p>
|
||||||
<Button variant="outline" onClick={handleCancel}>Cancelar</Button>
|
) : (
|
||||||
<Button onClick={handleSave}>Salvar Laudo</Button>
|
<>
|
||||||
</div>
|
{patient && (
|
||||||
</div>
|
<Card className="mb-4">
|
||||||
</DoctorLayout>
|
<CardHeader>
|
||||||
);
|
<CardTitle>Informações do Paciente</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p><strong>Nome:</strong> {patient.full_name}</p>
|
||||||
|
<p><strong>Email:</strong> {patient.email}</p>
|
||||||
|
<p><strong>Telefone:</strong> {patient.phone_mobile}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Laudos do Paciente</CardTitle>
|
||||||
|
<Link href={`/doctor/medicos/${patientId}/laudos/novo`}>
|
||||||
|
<Button>Criar Novo Laudo</Button>
|
||||||
|
</Link>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Nº do Pedido</TableHead>
|
||||||
|
<TableHead>Exame</TableHead>
|
||||||
|
<TableHead>Diagnóstico</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Data de Criação</TableHead>
|
||||||
|
<TableHead>Ações</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{currentItems.length > 0 ? (
|
||||||
|
currentItems.map((laudo) => (
|
||||||
|
<TableRow key={laudo.id}>
|
||||||
|
<TableCell>{laudo.order_number}</TableCell>
|
||||||
|
<TableCell>{laudo.exam}</TableCell>
|
||||||
|
<TableCell>{laudo.diagnosis}</TableCell>
|
||||||
|
<TableCell>{laudo.status}</TableCell>
|
||||||
|
<TableCell>{new Date(laudo.created_at).toLocaleDateString()}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Link href={`/doctor/medicos/${patientId}/laudos/${laudo.id}/editar`}>
|
||||||
|
<Button variant="outline" size="sm">Editar</Button>
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center">Nenhum laudo encontrado.</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex justify-center space-x-2 mt-4 p-4">
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => (
|
||||||
|
<Button key={i} onClick={() => paginate(i + 1)} variant={currentPage === i + 1 ? 'default' : 'outline'}>
|
||||||
|
{i + 1}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DoctorLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -133,17 +133,17 @@ useEffect(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
<div className="min-h-screen bg-background flex">
|
||||||
{/* Sidebar para desktop */}
|
{/* Sidebar para desktop */}
|
||||||
<div className={`bg-white border-r border-gray-200 transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}>
|
<div className={`bg-card border-r border transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}>
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||||
<div className="w-4 h-4 bg-white rounded-sm"></div>
|
<div className="w-4 h-4 bg-white rounded-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-gray-900">MidConnecta</span>
|
<span className="font-semibold text-foreground">MidConnecta</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
|
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
|
||||||
@ -159,43 +159,7 @@ useEffect(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={item.href} href={item.href}>
|
<Link key={item.href} href={item.href}>
|
||||||
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-blue-50 text-blue-600 border-r-2 border-blue-600" : "text-gray-600 hover:bg-gray-50"}`}>
|
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground border-r-2 border-primary" : "text-muted-foreground hover:bg-accent"}`}>
|
||||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
|
||||||
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
// ... (seu código anterior)
|
|
||||||
|
|
||||||
{/* Sidebar para desktop */}
|
|
||||||
<div className={`bg-white border-r border-gray-200 transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}>
|
|
||||||
<div className="p-4 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{!sidebarCollapsed && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
|
||||||
<div className="w-4 h-4 bg-white rounded-sm"></div>
|
|
||||||
</div>
|
|
||||||
<span className="font-semibold text-gray-900">MedConnect</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
|
|
||||||
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="flex-1 p-2 overflow-y-auto">
|
|
||||||
{menuItems.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link key={item.href} href={item.href}>
|
|
||||||
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-blue-50 text-blue-600 border-r-2 border-blue-600" : "text-gray-600 hover:bg-gray-50"}`}>
|
|
||||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||||
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
|
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
|
||||||
</div>
|
</div>
|
||||||
@ -219,8 +183,8 @@ useEffect(() => {
|
|||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-900 truncate">{doctorData.name}</p>
|
<p className="text-sm font-medium text-foreground truncate">{doctorData.name}</p>
|
||||||
<p className="text-xs text-gray-500 truncate">{doctorData.specialty}</p>
|
<p className="text-xs text-muted-foreground truncate">{doctorData.specialty}</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -239,7 +203,7 @@ useEffect(() => {
|
|||||||
|
|
||||||
{/* Novo botão de sair, usando a mesma estrutura dos itens de menu */}
|
{/* Novo botão de sair, usando a mesma estrutura dos itens de menu */}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors text-gray-600 hover:bg-gray-50 cursor-pointer ${sidebarCollapsed ? "justify-center" : ""}`}
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors text-muted-foreground hover:bg-accent cursor-pointer ${sidebarCollapsed ? "justify-center" : ""}`}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5 flex-shrink-0" />
|
<LogOut className="w-5 h-5 flex-shrink-0" />
|
||||||
@ -247,20 +211,16 @@ useEffect(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sidebar para mobile (apresentado como um menu overlay) */}
|
|
||||||
{isMobileMenuOpen && (
|
{isMobileMenuOpen && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" onClick={toggleMobileMenu}></div>
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" onClick={toggleMobileMenu}></div>
|
||||||
)}
|
)}
|
||||||
<div className={`bg-white border-r border-gray-200 fixed left-0 top-0 h-screen flex flex-col z-50 transition-transform duration-300 md:hidden ${isMobileMenuOpen ? "translate-x-0 w-64" : "-translate-x-full w-64"}`}>
|
<div className={`bg-card border-r border fixed left-0 top-0 h-screen flex flex-col z-50 transition-transform duration-300 md:hidden ${isMobileMenuOpen ? "translate-x-0 w-64" : "-translate-x-full w-64"}`}>
|
||||||
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
<div className="p-4 border-b border flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||||
<div className="w-4 h-4 bg-white rounded-sm"></div>
|
<div className="w-4 h-4 bg-white rounded-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-gray-900">Hospital System</span>
|
<span className="font-semibold text-foreground">Hospital System</span>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" onClick={toggleMobileMenu} className="p-1">
|
<Button variant="ghost" size="sm" onClick={toggleMobileMenu} className="p-1">
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
@ -274,7 +234,7 @@ useEffect(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={item.href} href={item.href} onClick={toggleMobileMenu}> {/* Fechar menu ao clicar */}
|
<Link key={item.href} href={item.href} onClick={toggleMobileMenu}> {/* Fechar menu ao clicar */}
|
||||||
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-blue-50 text-blue-600 border-r-2 border-blue-600" : "text-gray-600 hover:bg-gray-50"}`}>
|
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground border-r-2 border-primary" : "text-muted-foreground hover:bg-accent"}`}>
|
||||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||||
<span className="font-medium">{item.label}</span>
|
<span className="font-medium">{item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -295,8 +255,8 @@ useEffect(() => {
|
|||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-900 truncate">{doctorData.name}</p>
|
<p className="text-sm font-medium text-foreground truncate">{doctorData.name}</p>
|
||||||
<p className="text-xs text-gray-500 truncate">{doctorData.specialty}</p>
|
<p className="text-xs text-muted-foreground truncate">{doctorData.specialty}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}> {/* Fechar menu ao deslogar */}
|
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}> {/* Fechar menu ao deslogar */}
|
||||||
@ -310,12 +270,12 @@ useEffect(() => {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
|
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="bg-white border-b border-gray-200 px-6 py-4">
|
<header className="bg-card border-b border px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4 flex-1">
|
<div className="flex items-center gap-4 flex-1">
|
||||||
<div className="relative flex-1 max-w-md">
|
<div className="relative flex-1 max-w-md">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
<Input placeholder="Buscar paciente" className="pl-10 bg-gray-50 border-gray-200" />
|
<Input placeholder="Buscar paciente" className="pl-10 bg-background border" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,7 @@ const FontSizeExtension = Extension.create({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const Tiptap = ({ content, onChange }: { content: string, onChange: (richText: string) => void }) => {
|
const Tiptap = ({ content, onChange }: { content: string, onChange: (html: string, json: object) => void }) => {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit.configure(),
|
StarterKit.configure(),
|
||||||
@ -72,7 +72,7 @@ const Tiptap = ({ content, onChange }: { content: string, onChange: (richText: s
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
onUpdate({ editor }) {
|
onUpdate({ editor }) {
|
||||||
onChange(editor.getHTML())
|
onChange(editor.getHTML(), editor.getJSON())
|
||||||
},
|
},
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
})
|
})
|
||||||
@ -100,24 +100,28 @@ const Tiptap = ({ content, onChange }: { content: string, onChange: (richText: s
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 p-2 border-b">
|
<div className="flex items-center gap-2 p-2 border-b">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
className={editor.isActive('bold') ? 'is-active' : ''}
|
className={editor.isActive('bold') ? 'is-active' : ''}
|
||||||
>
|
>
|
||||||
<Bold className="w-5 h-5" />
|
<Bold className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
className={editor.isActive('italic') ? 'is-active' : ''}
|
className={editor.isActive('italic') ? 'is-active' : ''}
|
||||||
>
|
>
|
||||||
<Italic className="w-5 h-5" />
|
<Italic className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
className={editor.isActive('strike') ? 'is-active' : ''}
|
className={editor.isActive('strike') ? 'is-active' : ''}
|
||||||
>
|
>
|
||||||
<Strikethrough className="w-5 h-5" />
|
<Strikethrough className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
className={editor.isActive('underline') ? 'is-active' : ''}
|
className={editor.isActive('underline') ? 'is-active' : ''}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -16,7 +16,9 @@ export async function login() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
localStorage.setItem("token", data.access_token);
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem("token", data.access_token);
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -34,7 +36,7 @@ async function request(endpoint, options = {}) {
|
|||||||
loginPromise = null;
|
loginPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -77,7 +79,7 @@ async function request(endpoint, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const api = {
|
export const api = {
|
||||||
get: (endpoint) => request(endpoint, { method: "GET" }),
|
get: (endpoint, options) => request(endpoint, { method: "GET", ...options }),
|
||||||
post: (endpoint, data) => request(endpoint, { method: "POST", body: JSON.stringify(data) }),
|
post: (endpoint, data) => request(endpoint, { method: "POST", body: JSON.stringify(data) }),
|
||||||
patch: (endpoint, data) => request(endpoint, { method: "PATCH", body: JSON.stringify(data) }),
|
patch: (endpoint, data) => request(endpoint, { method: "PATCH", body: JSON.stringify(data) }),
|
||||||
delete: (endpoint) => request(endpoint, { method: "DELETE" }),
|
delete: (endpoint) => request(endpoint, { method: "DELETE" }),
|
||||||
|
|||||||
42
services/reportsApi.mjs
Normal file
42
services/reportsApi.mjs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { api } from "./api.mjs";
|
||||||
|
|
||||||
|
const REPORTS_API_URL = "/rest/v1/reports";
|
||||||
|
|
||||||
|
export const reportsApi = {
|
||||||
|
getReports: async (patientId) => {
|
||||||
|
try {
|
||||||
|
const data = await api.get(`${REPORTS_API_URL}?patient_id=eq.${patientId}`);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch reports:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getReportById: async (reportId) => {
|
||||||
|
try {
|
||||||
|
const data = await api.get(`${REPORTS_API_URL}?id=eq.${reportId}`);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch report ${reportId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createReport: async (reportData) => {
|
||||||
|
try {
|
||||||
|
const data = await api.post(REPORTS_API_URL, reportData);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create report:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateReport: async (reportId, reportData) => {
|
||||||
|
try {
|
||||||
|
const data = await api.patch(`${REPORTS_API_URL}?id=eq.${reportId}`, reportData);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to update report ${reportId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user