page.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import { MainLayout } from '@/components/layout/main-layout';
  4. import { Button } from '@/components/ui/button';
  5. import { Input } from '@/components/ui/input';
  6. import { Label } from '@/components/ui/label';
  7. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
  8. import { Badge } from '@/components/ui/badge';
  9. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
  10. import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
  11. import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
  12. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
  13. import { Switch } from '@/components/ui/switch';
  14. import { toast } from 'sonner';
  15. import { Plus, Search, Edit, Trash2, Users, GraduationCap, BookOpen } from 'lucide-react';
  16. interface Teacher {
  17. id: string;
  18. firstName: string;
  19. lastName: string;
  20. cedula: string;
  21. email: string;
  22. phone: string;
  23. }
  24. interface Section {
  25. id: string;
  26. name: string;
  27. class: {
  28. id: string;
  29. name: string;
  30. code: string;
  31. period: {
  32. id: string;
  33. name: string;
  34. isActive: boolean;
  35. };
  36. };
  37. }
  38. interface TeacherAssignment {
  39. id: string;
  40. teacherId: string;
  41. sectionId: string;
  42. isActive: boolean;
  43. createdAt: string;
  44. updatedAt: string;
  45. teacher: Teacher;
  46. section: Section;
  47. }
  48. interface CreateAssignmentData {
  49. teacherId: string;
  50. sectionId: string;
  51. }
  52. export default function TeacherAssignmentsPage() {
  53. const [assignments, setAssignments] = useState<TeacherAssignment[]>([]);
  54. const [teachers, setTeachers] = useState<Teacher[]>([]);
  55. const [sections, setSections] = useState<Section[]>([]);
  56. const [loading, setLoading] = useState(true);
  57. const [searchTerm, setSearchTerm] = useState('');
  58. const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
  59. const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
  60. const [editingAssignment, setEditingAssignment] = useState<TeacherAssignment | null>(null);
  61. const [formData, setFormData] = useState<CreateAssignmentData>({
  62. teacherId: '',
  63. sectionId: '',
  64. });
  65. useEffect(() => {
  66. fetchAssignments();
  67. fetchTeachers();
  68. fetchSections();
  69. }, []);
  70. const fetchAssignments = async () => {
  71. try {
  72. const response = await fetch('/api/admin/teacher-assignments');
  73. if (response.ok) {
  74. const data = await response.json();
  75. setAssignments(data);
  76. } else {
  77. toast.error('Error al cargar las asignaciones');
  78. }
  79. } catch (error) {
  80. console.error('Error fetching assignments:', error);
  81. toast.error('Error al cargar las asignaciones');
  82. } finally {
  83. setLoading(false);
  84. }
  85. };
  86. const fetchTeachers = async () => {
  87. try {
  88. const response = await fetch('/api/admin/users');
  89. if (response.ok) {
  90. const users = await response.json();
  91. const teacherUsers = users.filter((user: any) => user.role === 'TEACHER' && user.teacher);
  92. const teachersData = teacherUsers.map((user: any) => ({
  93. id: user.teacher.id,
  94. firstName: user.teacher.firstName,
  95. lastName: user.teacher.lastName,
  96. cedula: user.teacher.cedula,
  97. email: user.teacher.email,
  98. phone: user.teacher.phone,
  99. }));
  100. setTeachers(teachersData);
  101. }
  102. } catch (error) {
  103. console.error('Error fetching teachers:', error);
  104. }
  105. };
  106. const fetchSections = async () => {
  107. try {
  108. const response = await fetch('/api/admin/sections');
  109. if (response.ok) {
  110. const data = await response.json();
  111. setSections(data);
  112. }
  113. } catch (error) {
  114. console.error('Error fetching sections:', error);
  115. }
  116. };
  117. const handleCreateAssignment = async () => {
  118. if (!validateForm()) return;
  119. try {
  120. const response = await fetch('/api/admin/teacher-assignments', {
  121. method: 'POST',
  122. headers: {
  123. 'Content-Type': 'application/json',
  124. },
  125. body: JSON.stringify(formData),
  126. });
  127. const result = await response.json();
  128. if (response.ok) {
  129. toast.success('Asignación creada exitosamente');
  130. setIsCreateDialogOpen(false);
  131. resetForm();
  132. fetchAssignments();
  133. } else {
  134. toast.error(result.message || 'Error al crear la asignación');
  135. }
  136. } catch (error) {
  137. console.error('Error creating assignment:', error);
  138. toast.error('Error al crear la asignación');
  139. }
  140. };
  141. const handleUpdateAssignment = async () => {
  142. if (!editingAssignment || !validateForm()) return;
  143. try {
  144. const response = await fetch(`/api/admin/teacher-assignments/${editingAssignment.id}`, {
  145. method: 'PUT',
  146. headers: {
  147. 'Content-Type': 'application/json',
  148. },
  149. body: JSON.stringify({
  150. ...formData,
  151. isActive: editingAssignment.isActive,
  152. }),
  153. });
  154. const result = await response.json();
  155. if (response.ok) {
  156. toast.success('Asignación actualizada exitosamente');
  157. setIsEditDialogOpen(false);
  158. setEditingAssignment(null);
  159. resetForm();
  160. fetchAssignments();
  161. } else {
  162. toast.error(result.message || 'Error al actualizar la asignación');
  163. }
  164. } catch (error) {
  165. console.error('Error updating assignment:', error);
  166. toast.error('Error al actualizar la asignación');
  167. }
  168. };
  169. const handleDeleteAssignment = async (assignmentId: string) => {
  170. try {
  171. const response = await fetch(`/api/admin/teacher-assignments/${assignmentId}`, {
  172. method: 'DELETE',
  173. });
  174. const result = await response.json();
  175. if (response.ok) {
  176. toast.success(result.message || 'Asignación eliminada exitosamente');
  177. fetchAssignments();
  178. } else {
  179. toast.error(result.message || 'Error al eliminar la asignación');
  180. }
  181. } catch (error) {
  182. console.error('Error deleting assignment:', error);
  183. toast.error('Error al eliminar la asignación');
  184. }
  185. };
  186. const handleToggleActive = async (assignment: TeacherAssignment) => {
  187. try {
  188. const response = await fetch(`/api/admin/teacher-assignments/${assignment.id}`, {
  189. method: 'PUT',
  190. headers: {
  191. 'Content-Type': 'application/json',
  192. },
  193. body: JSON.stringify({
  194. teacherId: assignment.teacherId,
  195. sectionId: assignment.sectionId,
  196. isActive: !assignment.isActive,
  197. }),
  198. });
  199. const result = await response.json();
  200. if (response.ok) {
  201. toast.success(`Asignación ${!assignment.isActive ? 'activada' : 'desactivada'} exitosamente`);
  202. fetchAssignments();
  203. } else {
  204. toast.error(result.message || 'Error al cambiar el estado de la asignación');
  205. }
  206. } catch (error) {
  207. console.error('Error toggling assignment status:', error);
  208. toast.error('Error al cambiar el estado de la asignación');
  209. }
  210. };
  211. const validateForm = (): boolean => {
  212. if (!formData.teacherId) {
  213. toast.error('Por favor selecciona un profesor');
  214. return false;
  215. }
  216. if (!formData.sectionId) {
  217. toast.error('Por favor selecciona una sección');
  218. return false;
  219. }
  220. return true;
  221. };
  222. const resetForm = () => {
  223. setFormData({
  224. teacherId: '',
  225. sectionId: '',
  226. });
  227. };
  228. const openEditDialog = (assignment: TeacherAssignment) => {
  229. setEditingAssignment(assignment);
  230. setFormData({
  231. teacherId: assignment.teacherId,
  232. sectionId: assignment.sectionId,
  233. });
  234. setIsEditDialogOpen(true);
  235. };
  236. const filteredAssignments = assignments.filter(assignment => {
  237. const searchLower = searchTerm.toLowerCase();
  238. const teacherName = `${assignment.teacher.firstName} ${assignment.teacher.lastName}`.toLowerCase();
  239. const sectionName = assignment.section.name.toLowerCase();
  240. const className = assignment.section.class.name.toLowerCase();
  241. const classCode = assignment.section.class.code.toLowerCase();
  242. return teacherName.includes(searchLower) ||
  243. sectionName.includes(searchLower) ||
  244. className.includes(searchLower) ||
  245. classCode.includes(searchLower);
  246. });
  247. const getStatusBadgeVariant = (isActive: boolean) => {
  248. return isActive ? 'default' : 'secondary';
  249. };
  250. const getTeacherName = (teacher: Teacher) => {
  251. return `${teacher.firstName} ${teacher.lastName}`;
  252. };
  253. const getSectionDisplay = (section: Section) => {
  254. return `${section.class.code} - ${section.class.name} (${section.name})`;
  255. };
  256. return (
  257. <MainLayout>
  258. <div className="space-y-6">
  259. <div className="flex items-center justify-between">
  260. <div>
  261. <h1 className="text-3xl font-bold tracking-tight">Asignaciones de Profesores</h1>
  262. <p className="text-muted-foreground">
  263. Gestiona las asignaciones de profesores a secciones
  264. </p>
  265. </div>
  266. <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
  267. <DialogTrigger asChild>
  268. <Button onClick={() => resetForm()}>
  269. <Plus className="mr-2 h-4 w-4" />
  270. Nueva Asignación
  271. </Button>
  272. </DialogTrigger>
  273. <DialogContent className="sm:max-w-[425px]">
  274. <DialogHeader>
  275. <DialogTitle>Crear Nueva Asignación</DialogTitle>
  276. <DialogDescription>
  277. Asigna un profesor a una sección específica.
  278. </DialogDescription>
  279. </DialogHeader>
  280. <div className="grid gap-4 py-4">
  281. <div className="grid gap-2">
  282. <Label htmlFor="teacher">Profesor</Label>
  283. <Select
  284. value={formData.teacherId}
  285. onValueChange={(value) => setFormData({ ...formData, teacherId: value })}
  286. >
  287. <SelectTrigger>
  288. <SelectValue placeholder="Selecciona un profesor" />
  289. </SelectTrigger>
  290. <SelectContent>
  291. {teachers.map((teacher) => (
  292. <SelectItem key={teacher.id} value={teacher.id}>
  293. {getTeacherName(teacher)} - {teacher.cedula}
  294. </SelectItem>
  295. ))}
  296. </SelectContent>
  297. </Select>
  298. </div>
  299. <div className="grid gap-2">
  300. <Label htmlFor="section">Sección</Label>
  301. <Select
  302. value={formData.sectionId}
  303. onValueChange={(value) => setFormData({ ...formData, sectionId: value })}
  304. >
  305. <SelectTrigger>
  306. <SelectValue placeholder="Selecciona una sección" />
  307. </SelectTrigger>
  308. <SelectContent>
  309. {sections
  310. .filter(section => section.class.period.isActive)
  311. .map((section) => (
  312. <SelectItem key={section.id} value={section.id}>
  313. {getSectionDisplay(section)}
  314. </SelectItem>
  315. ))}
  316. </SelectContent>
  317. </Select>
  318. </div>
  319. </div>
  320. <DialogFooter>
  321. <Button type="submit" onClick={handleCreateAssignment}>
  322. Crear Asignación
  323. </Button>
  324. </DialogFooter>
  325. </DialogContent>
  326. </Dialog>
  327. </div>
  328. <Card>
  329. <CardHeader>
  330. <CardTitle className="flex items-center gap-2">
  331. <GraduationCap className="h-5 w-5" />
  332. Asignaciones del Sistema
  333. </CardTitle>
  334. <CardDescription>
  335. Lista de todas las asignaciones de profesores a secciones
  336. </CardDescription>
  337. <div className="flex items-center space-x-2">
  338. <Search className="h-4 w-4 text-muted-foreground" />
  339. <Input
  340. placeholder="Buscar por profesor, sección o materia..."
  341. value={searchTerm}
  342. onChange={(e) => setSearchTerm(e.target.value)}
  343. className="max-w-sm"
  344. />
  345. </div>
  346. </CardHeader>
  347. <CardContent>
  348. {loading ? (
  349. <div className="flex items-center justify-center py-8">
  350. <div className="text-muted-foreground">Cargando asignaciones...</div>
  351. </div>
  352. ) : (
  353. <Table>
  354. <TableHeader>
  355. <TableRow>
  356. <TableHead>Profesor</TableHead>
  357. <TableHead>Cédula</TableHead>
  358. <TableHead>Materia</TableHead>
  359. <TableHead>Sección</TableHead>
  360. <TableHead>Periodo</TableHead>
  361. <TableHead>Estado</TableHead>
  362. <TableHead>Fecha Asignación</TableHead>
  363. <TableHead className="text-right">Acciones</TableHead>
  364. </TableRow>
  365. </TableHeader>
  366. <TableBody>
  367. {filteredAssignments.length === 0 ? (
  368. <TableRow>
  369. <TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
  370. {searchTerm ? 'No se encontraron asignaciones que coincidan con la búsqueda' : 'No hay asignaciones registradas'}
  371. </TableCell>
  372. </TableRow>
  373. ) : (
  374. filteredAssignments.map((assignment) => (
  375. <TableRow key={assignment.id}>
  376. <TableCell className="font-medium">
  377. {getTeacherName(assignment.teacher)}
  378. </TableCell>
  379. <TableCell>{assignment.teacher.cedula}</TableCell>
  380. <TableCell>
  381. <div className="flex items-center gap-2">
  382. <BookOpen className="h-4 w-4 text-muted-foreground" />
  383. <div>
  384. <div className="font-medium">{assignment.section.class.name}</div>
  385. <div className="text-sm text-muted-foreground">{assignment.section.class.code}</div>
  386. </div>
  387. </div>
  388. </TableCell>
  389. <TableCell>{assignment.section.name}</TableCell>
  390. <TableCell>
  391. <Badge variant={assignment.section.class.period.isActive ? 'default' : 'secondary'}>
  392. {assignment.section.class.period.name}
  393. </Badge>
  394. </TableCell>
  395. <TableCell>
  396. <div className="flex items-center gap-2">
  397. <Switch
  398. checked={assignment.isActive}
  399. onCheckedChange={() => handleToggleActive(assignment)}
  400. />
  401. <Badge variant={getStatusBadgeVariant(assignment.isActive)}>
  402. {assignment.isActive ? 'Activa' : 'Inactiva'}
  403. </Badge>
  404. </div>
  405. </TableCell>
  406. <TableCell>
  407. {new Date(assignment.createdAt).toLocaleDateString('es-ES')}
  408. </TableCell>
  409. <TableCell className="text-right">
  410. <div className="flex items-center justify-end gap-2">
  411. <Button
  412. variant="outline"
  413. size="sm"
  414. onClick={() => openEditDialog(assignment)}
  415. >
  416. <Edit className="h-4 w-4" />
  417. </Button>
  418. <AlertDialog>
  419. <AlertDialogTrigger asChild>
  420. <Button variant="outline" size="sm">
  421. <Trash2 className="h-4 w-4" />
  422. </Button>
  423. </AlertDialogTrigger>
  424. <AlertDialogContent>
  425. <AlertDialogHeader>
  426. <AlertDialogTitle>¿Estás seguro?</AlertDialogTitle>
  427. <AlertDialogDescription>
  428. Esta acción eliminará la asignación de {getTeacherName(assignment.teacher)} a la sección {assignment.section.name}.
  429. {assignment.isActive && ' Si existen registros de asistencia, la asignación será desactivada en lugar de eliminada.'}
  430. </AlertDialogDescription>
  431. </AlertDialogHeader>
  432. <AlertDialogFooter>
  433. <AlertDialogCancel>Cancelar</AlertDialogCancel>
  434. <AlertDialogAction
  435. onClick={() => handleDeleteAssignment(assignment.id)}
  436. className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
  437. >
  438. Eliminar
  439. </AlertDialogAction>
  440. </AlertDialogFooter>
  441. </AlertDialogContent>
  442. </AlertDialog>
  443. </div>
  444. </TableCell>
  445. </TableRow>
  446. ))
  447. )}
  448. </TableBody>
  449. </Table>
  450. )}
  451. </CardContent>
  452. </Card>
  453. {/* Edit Dialog */}
  454. <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
  455. <DialogContent className="sm:max-w-[425px]">
  456. <DialogHeader>
  457. <DialogTitle>Editar Asignación</DialogTitle>
  458. <DialogDescription>
  459. Modifica los detalles de la asignación.
  460. </DialogDescription>
  461. </DialogHeader>
  462. <div className="grid gap-4 py-4">
  463. <div className="grid gap-2">
  464. <Label htmlFor="edit-teacher">Profesor</Label>
  465. <Select
  466. value={formData.teacherId}
  467. onValueChange={(value) => setFormData({ ...formData, teacherId: value })}
  468. >
  469. <SelectTrigger>
  470. <SelectValue placeholder="Selecciona un profesor" />
  471. </SelectTrigger>
  472. <SelectContent>
  473. {teachers.map((teacher) => (
  474. <SelectItem key={teacher.id} value={teacher.id}>
  475. {getTeacherName(teacher)} - {teacher.cedula}
  476. </SelectItem>
  477. ))}
  478. </SelectContent>
  479. </Select>
  480. </div>
  481. <div className="grid gap-2">
  482. <Label htmlFor="edit-section">Sección</Label>
  483. <Select
  484. value={formData.sectionId}
  485. onValueChange={(value) => setFormData({ ...formData, sectionId: value })}
  486. >
  487. <SelectTrigger>
  488. <SelectValue placeholder="Selecciona una sección" />
  489. </SelectTrigger>
  490. <SelectContent>
  491. {sections
  492. .filter(section => section.class.period.isActive)
  493. .map((section) => (
  494. <SelectItem key={section.id} value={section.id}>
  495. {getSectionDisplay(section)}
  496. </SelectItem>
  497. ))}
  498. </SelectContent>
  499. </Select>
  500. </div>
  501. </div>
  502. <DialogFooter>
  503. <Button type="submit" onClick={handleUpdateAssignment}>
  504. Actualizar Asignación
  505. </Button>
  506. </DialogFooter>
  507. </DialogContent>
  508. </Dialog>
  509. </div>
  510. </MainLayout>
  511. );
  512. }