forked from RiseUP/riseup-squad20
382 lines
14 KiB
TypeScript
382 lines
14 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Search, Filter, Plus, MoreHorizontal, Calendar, Gift, Eye, Edit, Trash2, CalendarPlus } from "lucide-react"
|
|
|
|
const patients = [
|
|
{
|
|
id: 1,
|
|
name: "Aaron Avalos Perez",
|
|
phone: "(75) 99982-6363",
|
|
city: "Aracaju",
|
|
state: "Sergipe",
|
|
lastAppointment: "26/09/2025 14:30",
|
|
nextAppointment: "19/08/2025 15:00",
|
|
isVip: false,
|
|
convenio: "unimed",
|
|
birthday: "1985-03-15",
|
|
age: 40,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "ABENANDO OLIVEIRA DE JESUS",
|
|
phone: "(75) 99986-0093",
|
|
city: "-",
|
|
state: "-",
|
|
lastAppointment: "Ainda não houve atendimento",
|
|
nextAppointment: "Nenhum atendimento agendado",
|
|
isVip: false,
|
|
convenio: "particular",
|
|
birthday: "1978-12-03",
|
|
age: 46,
|
|
},
|
|
{
|
|
id: 3,
|
|
name: "ABDIAS DANTAS DOS SANTOS",
|
|
phone: "(75) 99125-7267",
|
|
city: "São Cristóvão",
|
|
state: "Sergipe",
|
|
lastAppointment: "30/12/2024 08:40",
|
|
nextAppointment: "Nenhum atendimento agendado",
|
|
isVip: true,
|
|
convenio: "bradesco",
|
|
birthday: "1990-12-03",
|
|
age: 34,
|
|
},
|
|
{
|
|
id: 4,
|
|
name: "Abdias Matheus Rodrigues Ferreira",
|
|
phone: "(75) 99983-7711",
|
|
city: "Pirambu",
|
|
state: "Sergipe",
|
|
lastAppointment: "04/09/2024 16:20",
|
|
nextAppointment: "Nenhum atendimento agendado",
|
|
isVip: false,
|
|
convenio: "amil",
|
|
birthday: "1982-12-03",
|
|
age: 42,
|
|
},
|
|
{
|
|
id: 5,
|
|
name: "Abdon Ferreira Guerra",
|
|
phone: "(75) 99971-0228",
|
|
city: "-",
|
|
state: "-",
|
|
lastAppointment: "08/05/2025 08:00",
|
|
nextAppointment: "Nenhum atendimento agendado",
|
|
isVip: false,
|
|
convenio: "unimed",
|
|
birthday: "1975-12-03",
|
|
age: 49,
|
|
},
|
|
]
|
|
|
|
export default function PacientesPage() {
|
|
const [searchTerm, setSearchTerm] = useState("")
|
|
const [selectedConvenio, setSelectedConvenio] = useState("all") // Updated default value to "all"
|
|
const [showVipOnly, setShowVipOnly] = useState(false)
|
|
const [showBirthdays, setShowBirthdays] = useState(false)
|
|
const [advancedFilters, setAdvancedFilters] = useState({
|
|
city: "",
|
|
state: "",
|
|
minAge: "",
|
|
maxAge: "",
|
|
lastAppointmentFrom: "",
|
|
lastAppointmentTo: "",
|
|
})
|
|
const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false)
|
|
|
|
const filteredPatients = patients.filter((patient) => {
|
|
const matchesSearch =
|
|
patient.name.toLowerCase().includes(searchTerm.toLowerCase()) || patient.phone.includes(searchTerm)
|
|
|
|
const matchesConvenio = selectedConvenio === "all" || patient.convenio === selectedConvenio
|
|
const matchesVip = !showVipOnly || patient.isVip
|
|
|
|
// Check if patient has birthday this month
|
|
const currentMonth = new Date().getMonth() + 1
|
|
const patientBirthMonth = new Date(patient.birthday).getMonth() + 1
|
|
const matchesBirthday = !showBirthdays || patientBirthMonth === currentMonth
|
|
|
|
// Advanced filters
|
|
const matchesCity = !advancedFilters.city || patient.city.toLowerCase().includes(advancedFilters.city.toLowerCase())
|
|
const matchesState =
|
|
!advancedFilters.state || patient.state.toLowerCase().includes(advancedFilters.state.toLowerCase())
|
|
const matchesMinAge = !advancedFilters.minAge || patient.age >= Number.parseInt(advancedFilters.minAge)
|
|
const matchesMaxAge = !advancedFilters.maxAge || patient.age <= Number.parseInt(advancedFilters.maxAge)
|
|
|
|
return (
|
|
matchesSearch &&
|
|
matchesConvenio &&
|
|
matchesVip &&
|
|
matchesBirthday &&
|
|
matchesCity &&
|
|
matchesState &&
|
|
matchesMinAge &&
|
|
matchesMaxAge
|
|
)
|
|
})
|
|
|
|
const clearAdvancedFilters = () => {
|
|
setAdvancedFilters({
|
|
city: "",
|
|
state: "",
|
|
minAge: "",
|
|
maxAge: "",
|
|
lastAppointmentFrom: "",
|
|
lastAppointmentTo: "",
|
|
})
|
|
}
|
|
|
|
const handleViewDetails = (patientId: number) => {
|
|
console.log("[v0] Ver detalhes do paciente:", patientId)
|
|
// TODO: Navigate to patient details page
|
|
}
|
|
|
|
const handleEditPatient = (patientId: number) => {
|
|
console.log("[v0] Editar paciente:", patientId)
|
|
// TODO: Navigate to edit patient form
|
|
}
|
|
|
|
const handleScheduleAppointment = (patientId: number) => {
|
|
console.log("[v0] Marcar consulta para paciente:", patientId)
|
|
// TODO: Open appointment scheduling modal
|
|
}
|
|
|
|
const handleDeletePatient = (patientId: number) => {
|
|
console.log("[v0] Excluir paciente:", patientId)
|
|
// TODO: Show confirmation dialog and delete patient
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-foreground">Pacientes</h1>
|
|
<p className="text-muted-foreground">Gerencie as informações de seus pacientes</p>
|
|
</div>
|
|
<Button className="bg-primary hover:bg-primary/90">
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Adicionar
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap gap-4 items-center">
|
|
<div className="relative flex-1 min-w-64">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
|
<Input
|
|
placeholder="Buscar paciente"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
|
|
<Select value={selectedConvenio} onValueChange={setSelectedConvenio}>
|
|
<SelectTrigger className="w-48">
|
|
<SelectValue placeholder="Selecione o Convênio" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Todos os Convênios</SelectItem>
|
|
<SelectItem value="unimed">Unimed</SelectItem>
|
|
<SelectItem value="bradesco">Bradesco Saúde</SelectItem>
|
|
<SelectItem value="amil">Amil</SelectItem>
|
|
<SelectItem value="particular">Particular</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Button
|
|
variant={showVipOnly ? "default" : "outline"}
|
|
onClick={() => setShowVipOnly(!showVipOnly)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Gift className="h-4 w-4" />
|
|
VIP
|
|
</Button>
|
|
|
|
<Button
|
|
variant={showBirthdays ? "default" : "outline"}
|
|
onClick={() => setShowBirthdays(!showBirthdays)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Calendar className="h-4 w-4" />
|
|
Aniversariantes
|
|
</Button>
|
|
|
|
<Dialog open={isAdvancedFilterOpen} onOpenChange={setIsAdvancedFilterOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" className="flex items-center gap-2 bg-transparent">
|
|
<Filter className="h-4 w-4" />
|
|
Filtro avançado
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="sm:max-w-[425px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Filtros Avançados</DialogTitle>
|
|
<DialogDescription>
|
|
Use os filtros abaixo para refinar sua busca por pacientes específicos.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="city">Cidade</Label>
|
|
<Input
|
|
id="city"
|
|
value={advancedFilters.city}
|
|
onChange={(e) => setAdvancedFilters((prev) => ({ ...prev, city: e.target.value }))}
|
|
placeholder="Digite a cidade"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="state">Estado</Label>
|
|
<Input
|
|
id="state"
|
|
value={advancedFilters.state}
|
|
onChange={(e) => setAdvancedFilters((prev) => ({ ...prev, state: e.target.value }))}
|
|
placeholder="Digite o estado"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="minAge">Idade mínima</Label>
|
|
<Input
|
|
id="minAge"
|
|
type="number"
|
|
value={advancedFilters.minAge}
|
|
onChange={(e) => setAdvancedFilters((prev) => ({ ...prev, minAge: e.target.value }))}
|
|
placeholder="Ex: 18"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maxAge">Idade máxima</Label>
|
|
<Input
|
|
id="maxAge"
|
|
type="number"
|
|
value={advancedFilters.maxAge}
|
|
onChange={(e) => setAdvancedFilters((prev) => ({ ...prev, maxAge: e.target.value }))}
|
|
placeholder="Ex: 65"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2 pt-4">
|
|
<Button onClick={clearAdvancedFilters} variant="outline" className="flex-1 bg-transparent">
|
|
Limpar Filtros
|
|
</Button>
|
|
<Button onClick={() => setIsAdvancedFilterOpen(false)} className="flex-1">
|
|
Aplicar Filtros
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="border rounded-lg">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Nome</TableHead>
|
|
<TableHead>Telefone</TableHead>
|
|
<TableHead>Cidade</TableHead>
|
|
<TableHead>Estado</TableHead>
|
|
<TableHead>Último atendimento</TableHead>
|
|
<TableHead>Próximo atendimento</TableHead>
|
|
<TableHead className="w-[100px]">Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredPatients.map((patient) => (
|
|
<TableRow key={patient.id}>
|
|
<TableCell className="font-medium">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-8 h-8 bg-muted rounded-full flex items-center justify-center">
|
|
<span className="text-xs font-medium">{patient.name.charAt(0).toUpperCase()}</span>
|
|
</div>
|
|
<button onClick={() => handleViewDetails(patient.id)} className="hover:text-primary cursor-pointer">
|
|
{patient.name}
|
|
</button>
|
|
{patient.isVip && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
VIP
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{patient.phone}</TableCell>
|
|
<TableCell>{patient.city}</TableCell>
|
|
<TableCell>{patient.state}</TableCell>
|
|
<TableCell>
|
|
<span
|
|
className={patient.lastAppointment === "Ainda não houve atendimento" ? "text-muted-foreground" : ""}
|
|
>
|
|
{patient.lastAppointment}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
<span
|
|
className={patient.nextAppointment === "Nenhum atendimento agendado" ? "text-muted-foreground" : ""}
|
|
>
|
|
{patient.nextAppointment}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
<span className="sr-only">Abrir menu</span>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => handleViewDetails(patient.id)}>
|
|
<Eye className="mr-2 h-4 w-4" />
|
|
Ver detalhes
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleEditPatient(patient.id)}>
|
|
<Edit className="mr-2 h-4 w-4" />
|
|
Editar
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleScheduleAppointment(patient.id)}>
|
|
<CalendarPlus className="mr-2 h-4 w-4" />
|
|
Marcar consulta
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleDeletePatient(patient.id)} className="text-destructive">
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Excluir
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground">
|
|
Mostrando {filteredPatients.length} de {patients.length} pacientes
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|