- Adiciona chatbot AI com interface responsiva e posicionamento otimizado - Implementa gerenciamento completo de disponibilidade e exceções médicas - Adiciona modal de visualização detalhada de laudos no painel do paciente - Corrige relatórios da secretária para mostrar nomes de médicos - Implementa mensagem de boas-vindas personalizada com nome real - Remove mensagens duplicadas de login - Remove arquivo cleanup-deps.ps1 desnecessário - Atualiza README com todas as novas funcionalidades
738 lines
26 KiB
TypeScript
738 lines
26 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { Clock, Plus, Trash2, Save, Copy, Calendar as CalendarIcon, X } from "lucide-react";
|
|
import toast from "react-hot-toast";
|
|
import { format } from "date-fns";
|
|
import { ptBR } from "date-fns/locale";
|
|
import { availabilityService, doctorService } from "../services/index";
|
|
import type {
|
|
DoctorException,
|
|
DoctorAvailability,
|
|
} from "../services/availability/types";
|
|
import { useAuth } from "../hooks/useAuth";
|
|
|
|
interface TimeSlot {
|
|
id: string;
|
|
dbId?: string; // ID do banco de dados (se já existir)
|
|
inicio: string;
|
|
fim: string;
|
|
ativo: boolean;
|
|
slotMinutes?: number;
|
|
appointmentType?: "presencial" | "telemedicina";
|
|
}
|
|
|
|
interface DaySchedule {
|
|
day: string;
|
|
dayOfWeek: number;
|
|
enabled: boolean;
|
|
slots: TimeSlot[];
|
|
}
|
|
|
|
const daysOfWeek = [
|
|
{ key: 0, label: "Domingo", dbKey: "domingo" },
|
|
{ key: 1, label: "Segunda-feira", dbKey: "segunda" },
|
|
{ key: 2, label: "Terça-feira", dbKey: "terca" },
|
|
{ key: 3, label: "Quarta-feira", dbKey: "quarta" },
|
|
{ key: 4, label: "Quinta-feira", dbKey: "quinta" },
|
|
{ key: 5, label: "Sexta-feira", dbKey: "sexta" },
|
|
{ key: 6, label: "Sábado", dbKey: "sabado" },
|
|
];
|
|
|
|
const DisponibilidadeMedico: React.FC = () => {
|
|
const { user } = useAuth();
|
|
const [doctorId, setDoctorId] = useState<string | null>(null);
|
|
|
|
const [schedule, setSchedule] = useState<Record<number, DaySchedule>>({});
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [activeTab, setActiveTab] = useState<"weekly" | "blocked">("weekly");
|
|
|
|
// States for adding/editing slots
|
|
const [showAddSlotDialog, setShowAddSlotDialog] = useState(false);
|
|
const [selectedDay, setSelectedDay] = useState<number | null>(null);
|
|
const [newSlot, setNewSlot] = useState({
|
|
inicio: "09:00",
|
|
fim: "10:00",
|
|
slotMinutes: 30,
|
|
appointmentType: "presencial" as "presencial" | "telemedicina"
|
|
});
|
|
|
|
// States for blocked dates
|
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
|
|
new Date()
|
|
);
|
|
const [blockedDates, setBlockedDates] = useState<Date[]>([]);
|
|
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
|
|
|
|
// States for exceptions form
|
|
const [showExceptionDialog, setShowExceptionDialog] = useState(false);
|
|
const [exceptionForm, setExceptionForm] = useState({
|
|
date: format(new Date(), "yyyy-MM-dd"),
|
|
kind: "bloqueio" as "bloqueio" | "disponibilidade_extra",
|
|
start_time: "09:00",
|
|
end_time: "18:00",
|
|
wholeDayBlock: true,
|
|
reason: "",
|
|
});
|
|
|
|
// Load doctor ID from doctors table
|
|
useEffect(() => {
|
|
const loadDoctorId = async () => {
|
|
if (!user?.id) return;
|
|
try {
|
|
const doctors = await doctorService.list({ user_id: user.id });
|
|
if (doctors.length > 0) {
|
|
setDoctorId(doctors[0].id);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao buscar ID do médico:", error);
|
|
}
|
|
};
|
|
loadDoctorId();
|
|
}, [user?.id]);
|
|
|
|
const loadAvailability = React.useCallback(async () => {
|
|
if (!doctorId) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
const availabilities = await availabilityService.list({
|
|
doctor_id: doctorId,
|
|
});
|
|
|
|
if (availabilities && availabilities.length > 0) {
|
|
const newSchedule: Record<number, DaySchedule> = {};
|
|
|
|
// Inicializar todos os dias
|
|
daysOfWeek.forEach(({ key, label }) => {
|
|
newSchedule[key] = {
|
|
day: label,
|
|
dayOfWeek: key,
|
|
enabled: false,
|
|
slots: [],
|
|
};
|
|
});
|
|
|
|
// Agrupar disponibilidades por dia da semana
|
|
availabilities.forEach((avail: DoctorAvailability) => {
|
|
// avail.weekday agora é um número (0-6)
|
|
const dayKey = avail.weekday;
|
|
|
|
if (!newSchedule[dayKey]) return;
|
|
|
|
if (!newSchedule[dayKey].enabled) {
|
|
newSchedule[dayKey].enabled = true;
|
|
}
|
|
|
|
newSchedule[dayKey].slots.push({
|
|
id: `${dayKey}-${avail.id || Math.random().toString(36).slice(2)}`,
|
|
dbId: avail.id, // Armazenar ID do banco
|
|
inicio: avail.start_time?.slice(0, 5) || "09:00",
|
|
fim: avail.end_time?.slice(0, 5) || "17:00",
|
|
ativo: avail.active ?? true,
|
|
});
|
|
});
|
|
|
|
setSchedule(newSchedule);
|
|
} else {
|
|
// Initialize empty schedule
|
|
const newSchedule: Record<number, DaySchedule> = {};
|
|
daysOfWeek.forEach(({ key, label }) => {
|
|
newSchedule[key] = {
|
|
day: label,
|
|
dayOfWeek: key,
|
|
enabled: false,
|
|
slots: [],
|
|
};
|
|
});
|
|
setSchedule(newSchedule);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar disponibilidade:", error);
|
|
toast.error("Erro ao carregar disponibilidade");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [doctorId]);
|
|
|
|
const loadExceptions = React.useCallback(async () => {
|
|
if (!doctorId) return;
|
|
|
|
try {
|
|
const exceptions = await availabilityService.listExceptions({
|
|
doctor_id: doctorId,
|
|
});
|
|
setExceptions(exceptions);
|
|
const blocked = exceptions
|
|
.filter((exc: DoctorException) => exc.kind === "bloqueio" && exc.date)
|
|
.map((exc: DoctorException) => new Date(exc.date!));
|
|
setBlockedDates(blocked);
|
|
} catch (error) {
|
|
console.error("Erro ao carregar exceções:", error);
|
|
}
|
|
}, [doctorId]);
|
|
|
|
useEffect(() => {
|
|
if (doctorId) {
|
|
loadAvailability();
|
|
loadExceptions();
|
|
}
|
|
}, [doctorId, loadAvailability, loadExceptions]);
|
|
|
|
const toggleDay = (dayKey: number) => {
|
|
setSchedule((prev) => ({
|
|
...prev,
|
|
[dayKey]: {
|
|
...prev[dayKey],
|
|
enabled: !prev[dayKey].enabled,
|
|
},
|
|
}));
|
|
};
|
|
|
|
const addTimeSlot = () => {
|
|
if (selectedDay !== null) {
|
|
const newSlotId = `${selectedDay}-${Date.now()}`;
|
|
setSchedule((prev) => ({
|
|
...prev,
|
|
[selectedDay]: {
|
|
...prev[selectedDay],
|
|
slots: [
|
|
...prev[selectedDay].slots,
|
|
{
|
|
id: newSlotId,
|
|
inicio: newSlot.inicio,
|
|
fim: newSlot.fim,
|
|
ativo: true,
|
|
},
|
|
],
|
|
},
|
|
}));
|
|
setShowAddSlotDialog(false);
|
|
setNewSlot({ inicio: "09:00", fim: "10:00", slotMinutes: 30, appointmentType: "presencial" });
|
|
setSelectedDay(null);
|
|
}
|
|
};
|
|
|
|
const removeTimeSlot = async (dayKey: number, slotId: string) => {
|
|
const slot = schedule[dayKey]?.slots.find((s) => s.id === slotId);
|
|
|
|
// Se o slot tem um ID do banco, deletar imediatamente
|
|
if (slot?.dbId) {
|
|
try {
|
|
await availabilityService.delete(slot.dbId);
|
|
toast.success("Horário removido com sucesso");
|
|
} catch (error) {
|
|
console.error("Erro ao remover horário:", error);
|
|
toast.error("Erro ao remover horário");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Atualizar o estado local
|
|
setSchedule((prev) => ({
|
|
...prev,
|
|
[dayKey]: {
|
|
...prev[dayKey],
|
|
slots: prev[dayKey].slots.filter((slot) => slot.id !== slotId),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const toggleSlotAvailability = (dayKey: number, slotId: string) => {
|
|
setSchedule((prev) => ({
|
|
...prev,
|
|
[dayKey]: {
|
|
...prev[dayKey],
|
|
slots: prev[dayKey].slots.map((slot) =>
|
|
slot.id === slotId ? { ...slot, ativo: !slot.ativo } : slot
|
|
),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const copySchedule = (fromDay: number) => {
|
|
const sourceSchedule = schedule[fromDay];
|
|
if (!sourceSchedule.enabled || sourceSchedule.slots.length === 0) {
|
|
toast.error("Dia não tem horários configurados");
|
|
return;
|
|
}
|
|
|
|
const updatedSchedule = { ...schedule };
|
|
Object.keys(updatedSchedule).forEach((key) => {
|
|
const dayKey = Number(key);
|
|
if (dayKey !== fromDay && updatedSchedule[dayKey].enabled) {
|
|
updatedSchedule[dayKey].slots = sourceSchedule.slots.map((slot) => ({
|
|
...slot,
|
|
id: `${dayKey}-${slot.id}`,
|
|
}));
|
|
}
|
|
});
|
|
setSchedule(updatedSchedule);
|
|
toast.success("Horários copiados com sucesso!");
|
|
};
|
|
|
|
const handleSaveSchedule = async () => {
|
|
try {
|
|
setSaving(true);
|
|
|
|
if (!doctorId) {
|
|
toast.error("Médico não autenticado");
|
|
return;
|
|
}
|
|
|
|
const requests: Array<Promise<unknown>> = [];
|
|
|
|
const timeToMinutes = (t: string) => {
|
|
const [hStr, mStr] = t.split(":");
|
|
const h = Number(hStr || "0");
|
|
const m = Number(mStr || "0");
|
|
return h * 60 + m;
|
|
};
|
|
|
|
// Para cada dia, processar slots
|
|
daysOfWeek.forEach(({ key }) => {
|
|
const daySchedule = schedule[key];
|
|
|
|
if (!daySchedule || !daySchedule.enabled) {
|
|
// Se o dia foi desabilitado, deletar todos os slots existentes
|
|
daySchedule?.slots.forEach((slot) => {
|
|
if (slot.dbId) {
|
|
requests.push(availabilityService.delete(slot.dbId));
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Processar cada slot do dia
|
|
daySchedule.slots.forEach((slot) => {
|
|
const inicio = slot.inicio
|
|
? slot.inicio.length === 5
|
|
? `${slot.inicio}:00`
|
|
: slot.inicio
|
|
: "00:00:00";
|
|
const fim = slot.fim
|
|
? slot.fim.length === 5
|
|
? `${slot.fim}:00`
|
|
: slot.fim
|
|
: "00:00:00";
|
|
const minutes = Math.max(
|
|
1,
|
|
timeToMinutes(fim.slice(0, 5)) - timeToMinutes(inicio.slice(0, 5))
|
|
);
|
|
|
|
const payload = {
|
|
weekday: key, // Agora usa número (0-6) ao invés de string
|
|
start_time: inicio.slice(0, 5), // HH:MM ao invés de HH:MM:SS
|
|
end_time: fim.slice(0, 5), // HH:MM ao invés de HH:MM:SS
|
|
slot_minutes: minutes,
|
|
appointment_type: "presencial" as const,
|
|
active: !!slot.ativo,
|
|
};
|
|
|
|
if (slot.dbId) {
|
|
// Atualizar slot existente
|
|
requests.push(availabilityService.update(slot.dbId, payload as any));
|
|
} else {
|
|
// Criar novo slot
|
|
requests.push(
|
|
availabilityService.create({
|
|
doctor_id: doctorId,
|
|
...payload,
|
|
} as any)
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
if (requests.length === 0) {
|
|
toast.error("Nenhuma alteração para salvar");
|
|
return;
|
|
}
|
|
|
|
const results = await Promise.allSettled(requests);
|
|
const errors: string[] = [];
|
|
let successCount = 0;
|
|
results.forEach((r, idx) => {
|
|
if (r.status === "fulfilled") {
|
|
const val = r.value as {
|
|
success?: boolean;
|
|
error?: string;
|
|
message?: string;
|
|
};
|
|
if (val && val.success) successCount++;
|
|
else
|
|
errors.push(`Item ${idx}: ${val?.error || val?.message || "Erro"}`);
|
|
} else {
|
|
errors.push(`Item ${idx}: ${r.reason?.message || String(r.reason)}`);
|
|
}
|
|
});
|
|
|
|
if (errors.length > 0) {
|
|
console.error("Erros ao salvar disponibilidades:", errors);
|
|
toast.error(
|
|
`Algumas disponibilidades não foram salvas (${errors.length})`
|
|
);
|
|
}
|
|
if (successCount > 0) {
|
|
toast.success(`${successCount} alteração(ões) salvas com sucesso!`);
|
|
await loadAvailability();
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao salvar disponibilidade:", error);
|
|
const errorMessage =
|
|
error instanceof Error
|
|
? error.message
|
|
: "Erro ao salvar disponibilidade";
|
|
toast.error(errorMessage);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const toggleBlockedDate = async () => {
|
|
if (!selectedDate) return;
|
|
|
|
const dateString = format(selectedDate, "yyyy-MM-dd");
|
|
const dateExists = blockedDates.some(
|
|
(d) => format(d, "yyyy-MM-dd") === dateString
|
|
);
|
|
|
|
try {
|
|
if (dateExists) {
|
|
// Remove block
|
|
const exception = exceptions.find(
|
|
(exc) =>
|
|
exc.date && format(new Date(exc.date), "yyyy-MM-dd") === dateString
|
|
);
|
|
if (exception && exception.id) {
|
|
await availabilityService.deleteException(exception.id);
|
|
setBlockedDates(
|
|
blockedDates.filter((d) => format(d, "yyyy-MM-dd") !== dateString)
|
|
);
|
|
toast.success("Data desbloqueada");
|
|
}
|
|
} else {
|
|
// Add block
|
|
await availabilityService.createException({
|
|
doctor_id: doctorId!,
|
|
date: dateString,
|
|
kind: "bloqueio",
|
|
reason: "Data bloqueada pelo médico",
|
|
created_by: user?.id || doctorId!,
|
|
});
|
|
setBlockedDates([...blockedDates, selectedDate]);
|
|
toast.success("Data bloqueada");
|
|
}
|
|
loadExceptions();
|
|
} catch (error) {
|
|
console.error("Erro ao alternar bloqueio de data:", error);
|
|
toast.error("Erro ao bloquear/desbloquear data");
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
Gerenciar Disponibilidade
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
Configure seus horários de atendimento
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleSaveSchedule}
|
|
disabled={saving}
|
|
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
|
>
|
|
<Save className="h-4 w-4" />
|
|
{saving ? "Salvando..." : "Salvar Alterações"}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
|
<nav className="-mb-px flex space-x-8">
|
|
<button
|
|
onClick={() => setActiveTab("weekly")}
|
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === "weekly"
|
|
? "border-indigo-500 text-indigo-600 dark:text-indigo-400"
|
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
}`}
|
|
>
|
|
Horário Semanal
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab("blocked")}
|
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === "blocked"
|
|
? "border-indigo-500 text-indigo-600 dark:text-indigo-400"
|
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
}`}
|
|
>
|
|
Exceções ({exceptions.length})
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Tab Content - Weekly Schedule */}
|
|
{activeTab === "weekly" && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
|
Horários por Dia da Semana
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
Defina seus horários de atendimento para cada dia da semana
|
|
</p>
|
|
</div>
|
|
|
|
{daysOfWeek.map(({ key, label }) => (
|
|
<div key={key} className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
className="sr-only peer"
|
|
checked={schedule[key]?.enabled || false}
|
|
onChange={() => toggleDay(key)}
|
|
/>
|
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"></div>
|
|
</label>
|
|
<span className="text-gray-900 dark:text-white font-medium">
|
|
{label}
|
|
</span>
|
|
{schedule[key]?.enabled && (
|
|
<span className="px-2 py-1 bg-indigo-100 dark:bg-indigo-900 text-indigo-600 dark:text-indigo-400 text-xs rounded">
|
|
{schedule[key]?.slots.length || 0} horário(s)
|
|
</span>
|
|
)}
|
|
</div>
|
|
{schedule[key]?.enabled && (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => copySchedule(key)}
|
|
className="flex items-center gap-1 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
Copiar
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setSelectedDay(key);
|
|
setShowAddSlotDialog(true);
|
|
}}
|
|
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Adicionar Horário
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{schedule[key]?.enabled && (
|
|
<div className="ml-14 space-y-2">
|
|
{schedule[key]?.slots.length === 0 ? (
|
|
<p className="text-gray-500 dark:text-gray-400">
|
|
Nenhum horário configurado
|
|
</p>
|
|
) : (
|
|
schedule[key]?.slots.map((slot) => (
|
|
<div
|
|
key={slot.id}
|
|
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
className="sr-only peer"
|
|
checked={slot.ativo}
|
|
onChange={() =>
|
|
toggleSlotAvailability(key, slot.id)
|
|
}
|
|
/>
|
|
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"></div>
|
|
</label>
|
|
<Clock className="h-4 w-4 text-gray-400" />
|
|
<span className="text-gray-900 dark:text-white">
|
|
{slot.inicio} - {slot.fim}
|
|
</span>
|
|
{!slot.ativo && (
|
|
<span className="px-2 py-1 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 text-xs rounded">
|
|
Bloqueado
|
|
</span>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => removeTimeSlot(key, slot.id)}
|
|
className="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
|
|
>
|
|
<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />
|
|
</button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tab Content - Blocked Dates */}
|
|
{activeTab === "blocked" && (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
Selecionar Datas
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
Clique em uma data no calendário e depois no botão para
|
|
bloquear/desbloquear
|
|
</p>
|
|
<div className="space-y-4">
|
|
<input
|
|
type="date"
|
|
value={selectedDate ? format(selectedDate, "yyyy-MM-dd") : ""}
|
|
onChange={(e) => setSelectedDate(new Date(e.target.value))}
|
|
className="form-input"
|
|
/>
|
|
<button
|
|
onClick={toggleBlockedDate}
|
|
disabled={!selectedDate}
|
|
className="w-full py-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
|
>
|
|
{selectedDate &&
|
|
blockedDates.some(
|
|
(d) =>
|
|
format(d, "yyyy-MM-dd") ===
|
|
format(selectedDate, "yyyy-MM-dd")
|
|
)
|
|
? "Desbloquear Data"
|
|
: "Bloquear Data"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
|
Datas Bloqueadas
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
{blockedDates.length} data(s) bloqueada(s)
|
|
</p>
|
|
{blockedDates.length === 0 ? (
|
|
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
|
|
Nenhuma data bloqueada
|
|
</p>
|
|
) : (
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
{blockedDates.map((date, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700"
|
|
>
|
|
<span className="text-gray-900 dark:text-white">
|
|
{format(date, "EEEE, dd 'de' MMMM 'de' yyyy", {
|
|
locale: ptBR,
|
|
})}
|
|
</span>
|
|
<button
|
|
onClick={() => {
|
|
setSelectedDate(date);
|
|
toggleBlockedDate();
|
|
}}
|
|
className="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
|
|
>
|
|
<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Add Time Slot Dialog */}
|
|
{showAddSlotDialog && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
Adicionar Horário
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
Defina o período de atendimento para{" "}
|
|
{selectedDay !== null ? schedule[selectedDay]?.day : ""}
|
|
</p>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-900 dark:text-white mb-2">
|
|
Horário de Início
|
|
</label>
|
|
<input
|
|
type="time"
|
|
value={newSlot.inicio}
|
|
onChange={(e) =>
|
|
setNewSlot({ ...newSlot, inicio: e.target.value })
|
|
}
|
|
className="form-input"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-900 dark:text-white mb-2">
|
|
Horário de Término
|
|
</label>
|
|
<input
|
|
type="time"
|
|
value={newSlot.fim}
|
|
onChange={(e) =>
|
|
setNewSlot({ ...newSlot, fim: e.target.value })
|
|
}
|
|
className="form-input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2 mt-6">
|
|
<button
|
|
onClick={() => {
|
|
setShowAddSlotDialog(false);
|
|
setSelectedDay(null);
|
|
}}
|
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
onClick={addTimeSlot}
|
|
className="flex-1 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
|
|
>
|
|
Adicionar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DisponibilidadeMedico;
|
|
|
|
|