From 1bddda691b915a2dc6af9bf6f519d766ce89e5b2 Mon Sep 17 00:00:00 2001 From: Rumazor Date: Sun, 8 Feb 2026 16:30:42 -0300 Subject: [PATCH] mejoras --- README.md | 26 + backend/package.json | 2 + backend/src/cache/cache.module.ts | 13 +- backend/src/database/seed.ts | 359 +++++++++++ backend/src/main.ts | 2 +- docker-compose.yml | 1 + frontend/app/dashboard/page.tsx | 46 +- frontend/components/kanban/kanban-board.tsx | 500 +++++++-------- frontend/components/kanban/kanban-card.tsx | 314 ++++----- frontend/components/tasks/task-dashboard.tsx | 640 +++++++++---------- frontend/hooks/useTasks.ts | 432 ++++++------- frontend/public/sw.js | 2 +- seed.sh | 27 + 13 files changed, 1394 insertions(+), 970 deletions(-) create mode 100644 backend/src/database/seed.ts create mode 100755 seed.sh diff --git a/README.md b/README.md index e705998..f8dc85b 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,32 @@ Abre tu navegador en `http://localhost:3000` para ver la interfaz de Next.js. El backend (NestJS) responderá en `http://localhost:3001`. +### Poblar la base de datos con datos de prueba + +Para iniciar con datos de prueba, ejecuta el script de seed: + + ./seed.sh + +O manualmente: + + docker compose exec nestjs yarn seed + +**Cuentas de prueba creadas:** + +| Rol | Email | Contraseña | +|---------|-------------------|-------------| +| Admin | admin@test.com | password123 | +| Manager | manager@test.com | password123 | +| Usuario | john@test.com | password123 | +| Usuario | jane@test.com | password123 | +| Usuario | bob@test.com | password123 | + +**Datos generados por el seed:** +- 5 usuarios (1 admin, 1 manager, 3 usuarios regulares) +- 3 proyectos +- 5 etiquetas +- 17 tareas (incluyendo subtareas y asignaciones) + ## Rutas principales (Backend) - `POST /auth/login` diff --git a/backend/package.json b/backend/package.json index fe6340a..e5a49a4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,8 @@ "start:dev": "docker compose up -d && nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", + "seed": "ts-node -r tsconfig-paths/register src/database/seed.ts", + "seed:docker": "docker compose exec nestjs yarn seed", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest --config jest.unit.json --testPathPattern='test/'", "test:watch": "jest --watch", diff --git a/backend/src/cache/cache.module.ts b/backend/src/cache/cache.module.ts index 0174aa5..62933f7 100644 --- a/backend/src/cache/cache.module.ts +++ b/backend/src/cache/cache.module.ts @@ -15,20 +15,29 @@ const createRedisStore = async ( const timeout = setTimeout(() => { logger.warn('Redis connection timeout, using in-memory cache'); resolve({ ttl }); - }, 3000); + }, 5000); try { const store = await redisStore({ socket: { host: redisHost, port: redisPort, - connectTimeout: 2000, + connectTimeout: 4000, reconnectStrategy: () => false, }, password: redisPassword || undefined, ttl, }); clearTimeout(timeout); + + // Handle Redis client errors to prevent app crashes + const client = (store as any).client; + if (client && client.on) { + client.on('error', (err: Error) => { + logger.warn('Redis client error:', err.message); + }); + } + logger.log('Connected to Redis cache'); resolve({ store, ttl }); } catch (error) { diff --git a/backend/src/database/seed.ts b/backend/src/database/seed.ts new file mode 100644 index 0000000..b11e0df --- /dev/null +++ b/backend/src/database/seed.ts @@ -0,0 +1,359 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from '../app.module'; +import { DataSource } from 'typeorm'; +import { User, UserRole } from '../users/user.entity'; +import { Task } from '../tasks/task.entity'; +import { Tag } from '../tags/tag.entity'; +import { Project } from '../projects/project.entity'; +import * as bcrypt from 'bcrypt'; + +async function seed() { + console.log('Starting database seeding...'); + + const app = await NestFactory.createApplicationContext(AppModule); + const dataSource = app.get(DataSource); + + try { + // Clear all existing data + console.log('Clearing existing data...'); + // Use TRUNCATE CASCADE to handle foreign key constraints + await dataSource.query('TRUNCATE TABLE "comment", "notification", "activity_log", "task_tags", "task", "tag", "project", "user" RESTART IDENTITY CASCADE;'); + + // Create test users + console.log('Creating test users...'); + const hashedPassword = await bcrypt.hash('password123', 10); + + const adminUser = await dataSource.getRepository(User).save({ + email: 'admin@test.com', + password: hashedPassword, + name: 'Admin User', + role: UserRole.ADMIN, + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=admin', + }); + + const managerUser = await dataSource.getRepository(User).save({ + email: 'manager@test.com', + password: hashedPassword, + name: 'Manager User', + role: UserRole.MANAGER, + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=manager', + }); + + const user1 = await dataSource.getRepository(User).save({ + email: 'john@test.com', + password: hashedPassword, + name: 'John Doe', + role: UserRole.USER, + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=john', + }); + + const user2 = await dataSource.getRepository(User).save({ + email: 'jane@test.com', + password: hashedPassword, + name: 'Jane Smith', + role: UserRole.USER, + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=jane', + }); + + const user3 = await dataSource.getRepository(User).save({ + email: 'bob@test.com', + password: hashedPassword, + name: 'Bob Wilson', + role: UserRole.USER, + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=bob', + }); + + console.log('✓ Created 5 test users'); + console.log(' - admin@test.com (Admin)'); + console.log(' - manager@test.com (Manager)'); + console.log(' - john@test.com, jane@test.com, bob@test.com (Users)'); + console.log(' - All passwords: password123'); + + // Create projects + console.log('\nCreating projects...'); + const project1 = await dataSource.getRepository(Project).save({ + name: 'Website Redesign', + description: 'Complete redesign of the company website', + color: '#3B82F6', + owner: adminUser, + }); + + const project2 = await dataSource.getRepository(Project).save({ + name: 'Mobile App Development', + description: 'Build a new mobile application', + color: '#10B981', + owner: managerUser, + }); + + const project3 = await dataSource.getRepository(Project).save({ + name: 'Marketing Campaign', + description: 'Q1 2026 marketing campaign', + color: '#F59E0B', + owner: adminUser, + }); + + console.log('✓ Created 3 projects'); + + // Create tags for each user + console.log('\nCreating tags...'); + const urgentTag = await dataSource.getRepository(Tag).save({ + name: 'Urgent', + color: '#EF4444', + user: adminUser, + }); + + const bugTag = await dataSource.getRepository(Tag).save({ + name: 'Bug', + color: '#DC2626', + user: adminUser, + }); + + const featureTag = await dataSource.getRepository(Tag).save({ + name: 'Feature', + color: '#3B82F6', + user: user1, + }); + + const designTag = await dataSource.getRepository(Tag).save({ + name: 'Design', + color: '#8B5CF6', + user: user2, + }); + + const documentationTag = await dataSource.getRepository(Tag).save({ + name: 'Documentation', + color: '#6B7280', + user: user3, + }); + + console.log('✓ Created 5 tags'); + + // Create tasks + console.log('\nCreating tasks...'); + const tasksData = [ + // Admin's tasks + { + title: 'Setup project repository', + description: 'Initialize Git repository and setup CI/CD pipeline', + completed: true, + priority: 'high' as const, + user: adminUser, + project: project1, + tags: [urgentTag], + position: 0, + }, + { + title: 'Review design mockups', + description: 'Review and approve the new design mockups from the design team', + completed: false, + priority: 'high' as const, + dueDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now + user: adminUser, + project: project1, + tags: [designTag], + position: 1, + assignedTo: user2, + }, + { + title: 'Update documentation', + description: 'Update API documentation with new endpoints', + completed: false, + priority: 'medium' as const, + user: adminUser, + tags: [documentationTag], + position: 2, + }, + + // Manager's tasks + { + title: 'Sprint planning meeting', + description: 'Organize sprint planning for the next iteration', + completed: false, + priority: 'high' as const, + dueDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000), // Tomorrow + user: managerUser, + project: project2, + position: 0, + }, + { + title: 'Code review PR #123', + description: 'Review pull request for authentication module', + completed: false, + priority: 'medium' as const, + user: managerUser, + project: project2, + tags: [featureTag], + position: 1, + assignedTo: user1, + }, + + // User1's tasks + { + title: 'Fix login bug', + description: 'Users are experiencing issues with OAuth login', + completed: false, + priority: 'high' as const, + dueDate: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + user: user1, + project: project2, + tags: [bugTag, urgentTag], + position: 0, + }, + { + title: 'Implement search feature', + description: 'Add full-text search functionality to the app', + completed: false, + priority: 'medium' as const, + user: user1, + project: project2, + tags: [featureTag], + position: 1, + }, + { + title: 'Write unit tests', + description: 'Add unit tests for the task service', + completed: true, + priority: 'low' as const, + user: user1, + tags: [documentationTag], + position: 2, + }, + + // User2's tasks + { + title: 'Create wireframes', + description: 'Design wireframes for the new dashboard', + completed: true, + priority: 'high' as const, + user: user2, + project: project1, + tags: [designTag], + position: 0, + }, + { + title: 'Design new landing page', + description: 'Create a modern landing page design', + completed: false, + priority: 'high' as const, + dueDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), + user: user2, + project: project1, + tags: [designTag], + position: 1, + }, + { + title: 'User testing session', + description: 'Conduct user testing for the new UI', + completed: false, + priority: 'medium' as const, + user: user2, + project: project1, + position: 2, + }, + + // User3's tasks + { + title: 'Database optimization', + description: 'Optimize slow database queries', + completed: false, + priority: 'medium' as const, + user: user3, + project: project2, + position: 0, + }, + { + title: 'Setup monitoring', + description: 'Configure application monitoring and alerts', + completed: false, + priority: 'low' as const, + user: user3, + tags: [documentationTag], + position: 1, + }, + + // Marketing campaign tasks + { + title: 'Prepare email campaign', + description: 'Create email templates for Q1 campaign', + completed: false, + priority: 'high' as const, + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + user: user2, + project: project3, + position: 0, + }, + { + title: 'Social media strategy', + description: 'Develop social media content calendar', + completed: false, + priority: 'medium' as const, + user: user1, + project: project3, + position: 1, + }, + ]; + + for (const taskData of tasksData) { + await dataSource.getRepository(Task).save(taskData); + } + + // Create a task with subtasks + const parentTask = await dataSource.getRepository(Task).save({ + title: 'Launch new feature', + description: 'Complete feature launch checklist', + completed: false, + priority: 'high' as const, + user: adminUser, + project: project2, + tags: [featureTag, urgentTag], + position: 3, + }); + + await dataSource.getRepository(Task).save({ + title: 'Write release notes', + description: 'Document all changes for the release', + completed: false, + priority: 'medium' as const, + user: adminUser, + parentTask: parentTask, + position: 0, + }); + + await dataSource.getRepository(Task).save({ + title: 'Update changelog', + description: 'Add entries to CHANGELOG.md', + completed: true, + priority: 'low' as const, + user: adminUser, + parentTask: parentTask, + position: 1, + }); + + console.log('✓ Created 17 tasks (including subtasks)'); + + console.log('\n✅ Database seeding completed successfully!'); + console.log('\n📝 Summary:'); + console.log(' - Users: 5 (1 admin, 1 manager, 3 regular users)'); + console.log(' - Projects: 3'); + console.log(' - Tags: 5'); + console.log(' - Tasks: 17'); + console.log('\n🔑 Login with:'); + console.log(' Email: admin@test.com'); + console.log(' Password: password123'); + } catch (error) { + console.error('❌ Error seeding database:', error); + throw error; + } finally { + await app.close(); + } +} + +seed() + .then(() => { + console.log('\n🎉 Seed completed!'); + process.exit(0); + }) + .catch((error) => { + console.error('Failed to seed database:', error); + process.exit(1); + }); diff --git a/backend/src/main.ts b/backend/src/main.ts index 8d207b8..9be04f0 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -23,7 +23,7 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); - const port = process.env.PORT || 3001; + const port = process.env.PORT || 3000; await app.listen(port); console.log(`Backend running on http://localhost:${port}`); } diff --git a/docker-compose.yml b/docker-compose.yml index 2b89e36..491d545 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: ports: - "3001:3000" environment: + PORT: "3000" DB_HOST: postgres DB_PORT: "5432" DB_NAME: "${DB_NAME}" diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index b5228ae..dc81b0a 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -1,23 +1,23 @@ -import { redirect } from "next/navigation"; -import { cookies } from "next/headers"; -import DashboardWrapper from "./dashboard-wrapper"; -import { getUserFromCookie } from "@/lib/auth"; - -export default async function Dashboard() { - const cookieStore = await cookies(); - const token = cookieStore.get("token"); - const user = await getUserFromCookie(); - - if (!token) { - redirect("/"); - } - - return ( -
-
-
- - -
- ); -} +import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; +import DashboardWrapper from "./dashboard-wrapper"; +import { getUserFromCookie } from "@/lib/auth"; + +export default async function Dashboard() { + const cookieStore = await cookies(); + const token = cookieStore.get("token"); + const user = await getUserFromCookie(); + + if (!token) { + redirect("/"); + } + + return ( +
+
+
+ + +
+ ); +} diff --git a/frontend/components/kanban/kanban-board.tsx b/frontend/components/kanban/kanban-board.tsx index 0fca677..97e6eb1 100644 --- a/frontend/components/kanban/kanban-board.tsx +++ b/frontend/components/kanban/kanban-board.tsx @@ -1,250 +1,250 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { - DndContext, - DragOverlay, - closestCorners, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - type DragStartEvent, - type DragEndEvent, - type DragOverEvent, -} from "@dnd-kit/core"; -import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; -import KanbanColumn from "./kanban-column"; -import KanbanCard from "./kanban-card"; -import type { Task, KanbanColumn as KanbanColumnType } from "@/lib/types"; -import { updateTaskAction, reorderTasksAction } from "@/components/tasks/actions"; - -interface KanbanBoardProps { - tasks: { - todo: Task[]; - completed: Task[]; - }; - token: string; - onTaskClick?: (task: Task) => void; - onTasksChange?: () => void; -} - -export default function KanbanBoard({ - tasks, - token, - onTaskClick, - onTasksChange, -}: KanbanBoardProps) { - const [columns, setColumns] = useState([ - { id: "todo", title: "Pendientes", tasks: tasks.todo || [] }, - { id: "completed", title: "Completadas", tasks: tasks.completed || [] }, - ]); - const [activeTask, setActiveTask] = useState(null); - - useEffect(() => { - setColumns([ - { id: "todo", title: "Pendientes", tasks: tasks.todo || [] }, - { id: "completed", title: "Completadas", tasks: tasks.completed || [] }, - ]); - }, [tasks]); - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { distance: 8 }, - }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ); - - const findColumn = (taskId: string | number) => { - const id = String(taskId); - return columns.find((col) => col.tasks.some((t) => String(t.id) === id)); - }; - - const handleDragStart = (event: DragStartEvent) => { - const { active } = event; - const activeId = String(active.id); - const column = findColumn(activeId); - if (column) { - const task = column.tasks.find((t) => String(t.id) === activeId); - if (task) setActiveTask(task); - } - }; - - const handleDragOver = (event: DragOverEvent) => { - const { active, over } = event; - if (!over) return; - - const activeId = String(active.id); - const overId = String(over.id); - - const activeColumn = findColumn(activeId); - const overColumn = - columns.find((col) => col.id === overId) || - findColumn(overId); - - if (!activeColumn || !overColumn || activeColumn.id === overColumn.id) return; - - setColumns((prev) => { - const activeTask = activeColumn.tasks.find((t) => String(t.id) === activeId); - if (!activeTask) return prev; - - return prev.map((col) => { - if (col.id === activeColumn.id) { - return { - ...col, - tasks: col.tasks.filter((t) => String(t.id) !== activeId), - }; - } - if (col.id === overColumn.id) { - const overIndex = col.tasks.findIndex((t) => String(t.id) === overId); - const newIndex = overIndex >= 0 ? overIndex : col.tasks.length; - const newTasks = [...col.tasks]; - newTasks.splice(newIndex, 0, activeTask); - return { ...col, tasks: newTasks }; - } - return col; - }); - }); - }; - - const handleDragEnd = async (event: DragEndEvent) => { - const { active, over } = event; - setActiveTask(null); - - if (!over) return; - - const activeId = String(active.id); - const overId = String(over.id); - - const activeColumn = findColumn(activeId); - const overColumn = - columns.find((col) => col.id === overId) || - findColumn(overId); - - if (!activeColumn || !overColumn) return; - - // Update task completion status if moved between columns - if (activeColumn.id !== overColumn.id) { - const isCompleted = overColumn.id === "completed"; - await updateTaskAction( - Number(activeId), - undefined, - undefined, - token, - isCompleted - ); - onTasksChange?.(); - } - - // Reorder within the same column - if (activeColumn.id === overColumn.id && activeId !== overId) { - setColumns((prev) => { - return prev.map((col) => { - if (col.id !== activeColumn.id) return col; - - const oldIndex = col.tasks.findIndex((t) => String(t.id) === activeId); - const newIndex = col.tasks.findIndex((t) => String(t.id) === overId); - - if (oldIndex === -1 || newIndex === -1) return col; - - const newTasks = arrayMove(col.tasks, oldIndex, newIndex); - const taskOrders = newTasks.map((t, index) => ({ - id: Number(t.id), - position: index, - })); - - reorderTasksAction(taskOrders, token); - - return { ...col, tasks: newTasks }; - }); - }); - } - }; - - const handleToggleCompletion = async (taskId: string) => { - const id = String(taskId); - const column = findColumn(id); - if (!column) return; - - const task = column.tasks.find((t) => String(t.id) === id); - if (!task) return; - - const newCompleted = !task.completed; - await updateTaskAction( - Number(id), - undefined, - undefined, - token, - newCompleted - ); - - // Move task to appropriate column - setColumns((prev) => { - const sourceColumn = prev.find((c) => - c.tasks.some((t) => String(t.id) === id) - ); - if (!sourceColumn) return prev; - - const targetColumnId = newCompleted ? "completed" : "todo"; - - return prev.map((col) => { - if (col.id === sourceColumn.id) { - return { - ...col, - tasks: col.tasks.filter((t) => String(t.id) !== id), - }; - } - if (col.id === targetColumnId) { - return { - ...col, - tasks: [...col.tasks, { ...task, completed: newCompleted }], - }; - } - return col; - }); - }); - - onTasksChange?.(); - }; - - return ( - -
- {columns.map((column) => ( - - ))} -
- - - {activeTask && ( -
- Promise.resolve()} - /> -
- )} -
-
- ); -} +"use client"; + +import { useState, useEffect } from "react"; +import { + DndContext, + DragOverlay, + closestCorners, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + type DragStartEvent, + type DragEndEvent, + type DragOverEvent, +} from "@dnd-kit/core"; +import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; +import KanbanColumn from "./kanban-column"; +import KanbanCard from "./kanban-card"; +import type { Task, KanbanColumn as KanbanColumnType } from "@/lib/types"; +import { updateTaskAction, reorderTasksAction } from "@/components/tasks/actions"; + +interface KanbanBoardProps { + tasks: { + todo: Task[]; + completed: Task[]; + }; + token: string; + onTaskClick?: (task: Task) => void; + onTasksChange?: () => void; +} + +export default function KanbanBoard({ + tasks, + token, + onTaskClick, + onTasksChange, +}: KanbanBoardProps) { + const [columns, setColumns] = useState([ + { id: "todo", title: "Pendientes", tasks: tasks.todo || [] }, + { id: "completed", title: "Completadas", tasks: tasks.completed || [] }, + ]); + const [activeTask, setActiveTask] = useState(null); + + useEffect(() => { + setColumns([ + { id: "todo", title: "Pendientes", tasks: tasks.todo || [] }, + { id: "completed", title: "Completadas", tasks: tasks.completed || [] }, + ]); + }, [tasks]); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 8 }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const findColumn = (taskId: string | number) => { + const id = String(taskId); + return columns.find((col) => col.tasks.some((t) => String(t.id) === id)); + }; + + const handleDragStart = (event: DragStartEvent) => { + const { active } = event; + const activeId = String(active.id); + const column = findColumn(activeId); + if (column) { + const task = column.tasks.find((t) => String(t.id) === activeId); + if (task) setActiveTask(task); + } + }; + + const handleDragOver = (event: DragOverEvent) => { + const { active, over } = event; + if (!over) return; + + const activeId = String(active.id); + const overId = String(over.id); + + const activeColumn = findColumn(activeId); + const overColumn = + columns.find((col) => col.id === overId) || + findColumn(overId); + + if (!activeColumn || !overColumn || activeColumn.id === overColumn.id) return; + + setColumns((prev) => { + const activeTask = activeColumn.tasks.find((t) => String(t.id) === activeId); + if (!activeTask) return prev; + + return prev.map((col) => { + if (col.id === activeColumn.id) { + return { + ...col, + tasks: col.tasks.filter((t) => String(t.id) !== activeId), + }; + } + if (col.id === overColumn.id) { + const overIndex = col.tasks.findIndex((t) => String(t.id) === overId); + const newIndex = overIndex >= 0 ? overIndex : col.tasks.length; + const newTasks = [...col.tasks]; + newTasks.splice(newIndex, 0, activeTask); + return { ...col, tasks: newTasks }; + } + return col; + }); + }); + }; + + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + setActiveTask(null); + + if (!over) return; + + const activeId = String(active.id); + const overId = String(over.id); + + const activeColumn = findColumn(activeId); + const overColumn = + columns.find((col) => col.id === overId) || + findColumn(overId); + + if (!activeColumn || !overColumn) return; + + // Update task completion status if moved between columns + if (activeColumn.id !== overColumn.id) { + const isCompleted = overColumn.id === "completed"; + await updateTaskAction( + Number(activeId), + undefined, + undefined, + token, + isCompleted + ); + onTasksChange?.(); + } + + // Reorder within the same column + if (activeColumn.id === overColumn.id && activeId !== overId) { + setColumns((prev) => { + return prev.map((col) => { + if (col.id !== activeColumn.id) return col; + + const oldIndex = col.tasks.findIndex((t) => String(t.id) === activeId); + const newIndex = col.tasks.findIndex((t) => String(t.id) === overId); + + if (oldIndex === -1 || newIndex === -1) return col; + + const newTasks = arrayMove(col.tasks, oldIndex, newIndex); + const taskOrders = newTasks.map((t, index) => ({ + id: Number(t.id), + position: index, + })); + + reorderTasksAction(taskOrders, token); + + return { ...col, tasks: newTasks }; + }); + }); + } + }; + + const handleToggleCompletion = async (taskId: string) => { + const id = String(taskId); + const column = findColumn(id); + if (!column) return; + + const task = column.tasks.find((t) => String(t.id) === id); + if (!task) return; + + const newCompleted = !task.completed; + await updateTaskAction( + Number(id), + undefined, + undefined, + token, + newCompleted + ); + + // Move task to appropriate column + setColumns((prev) => { + const sourceColumn = prev.find((c) => + c.tasks.some((t) => String(t.id) === id) + ); + if (!sourceColumn) return prev; + + const targetColumnId = newCompleted ? "completed" : "todo"; + + return prev.map((col) => { + if (col.id === sourceColumn.id) { + return { + ...col, + tasks: col.tasks.filter((t) => String(t.id) !== id), + }; + } + if (col.id === targetColumnId) { + return { + ...col, + tasks: [...col.tasks, { ...task, completed: newCompleted }], + }; + } + return col; + }); + }); + + onTasksChange?.(); + }; + + return ( + +
+ {columns.map((column) => ( + + ))} +
+ + + {activeTask && ( +
+ Promise.resolve()} + /> +
+ )} +
+
+ ); +} diff --git a/frontend/components/kanban/kanban-card.tsx b/frontend/components/kanban/kanban-card.tsx index a838242..6bc2743 100644 --- a/frontend/components/kanban/kanban-card.tsx +++ b/frontend/components/kanban/kanban-card.tsx @@ -1,157 +1,157 @@ -"use client"; - -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { Badge } from "@/components/ui/badge"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Calendar, User, GripVertical } from "lucide-react"; -import type { Task } from "@/lib/types"; -import { format, isPast, isToday } from "date-fns"; -import { es } from "date-fns/locale"; - -interface KanbanCardProps { - task: Task; - onToggleCompletion: (id: string) => Promise; - onClick?: () => void; -} - -const priorityConfig = { - low: { - label: "Baja", - className: "bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300", - dotColor: "bg-green-500", - }, - medium: { - label: "Media", - className: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300", - dotColor: "bg-yellow-500", - }, - high: { - label: "Alta", - className: "bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300", - dotColor: "bg-red-500", - }, -}; - -export default function KanbanCard({ - task, - onToggleCompletion, - onClick, -}: KanbanCardProps) { - // Ensure ID is always a string for consistent drag behavior - const taskId = String(task.id); - - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: taskId }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - - const priority = task.priority || "medium"; - const priorityInfo = priorityConfig[priority]; - - const isOverdue = task.dueDate && isPast(new Date(task.dueDate)) && !isToday(new Date(task.dueDate)); - - return ( -
-
-
e.stopPropagation()} - > - -
- - onToggleCompletion(taskId)} - className="mt-1" - onClick={(e) => e.stopPropagation()} - /> - -
-

- {task.title} -

- - {task.description && ( -

- {task.description} -

- )} - -
- - - {priorityInfo.label} - - - {task.dueDate && ( -
- - {format(new Date(task.dueDate), "d MMM", { locale: es })} -
- )} - - {task.assignedTo && ( -
- - - {task.assignedTo.email.split("@")[0]} - -
- )} -
- - {task.tags && task.tags.length > 0 && ( -
- {task.tags.slice(0, 3).map((tag) => ( - - {tag.name} - - ))} - {task.tags.length > 3 && ( - - +{task.tags.length - 3} - - )} -
- )} -
-
-
- ); -} +"use client"; + +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Calendar, User, GripVertical } from "lucide-react"; +import type { Task } from "@/lib/types"; +import { format, isPast, isToday } from "date-fns"; +import { es } from "date-fns/locale"; + +interface KanbanCardProps { + task: Task; + onToggleCompletion: (id: string) => Promise; + onClick?: () => void; +} + +const priorityConfig = { + low: { + label: "Baja", + className: "bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300", + dotColor: "bg-green-500", + }, + medium: { + label: "Media", + className: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300", + dotColor: "bg-yellow-500", + }, + high: { + label: "Alta", + className: "bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300", + dotColor: "bg-red-500", + }, +}; + +export default function KanbanCard({ + task, + onToggleCompletion, + onClick, +}: KanbanCardProps) { + // Ensure ID is always a string for consistent drag behavior + const taskId = String(task.id); + + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: taskId }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const priority = task.priority || "medium"; + const priorityInfo = priorityConfig[priority]; + + const isOverdue = task.dueDate && isPast(new Date(task.dueDate)) && !isToday(new Date(task.dueDate)); + + return ( +
+
+
e.stopPropagation()} + > + +
+ + onToggleCompletion(taskId)} + className="mt-1" + onClick={(e) => e.stopPropagation()} + /> + +
+

+ {task.title} +

+ + {task.description && ( +

+ {task.description} +

+ )} + +
+ + + {priorityInfo.label} + + + {task.dueDate && ( +
+ + {format(new Date(task.dueDate), "d MMM", { locale: es })} +
+ )} + + {task.assignedTo && ( +
+ + + {task.assignedTo.email.split("@")[0]} + +
+ )} +
+ + {task.tags && task.tags.length > 0 && ( +
+ {task.tags.slice(0, 3).map((tag) => ( + + {tag.name} + + ))} + {task.tags.length > 3 && ( + + +{task.tags.length - 3} + + )} +
+ )} +
+
+
+ ); +} diff --git a/frontend/components/tasks/task-dashboard.tsx b/frontend/components/tasks/task-dashboard.tsx index eaa3798..b2ea53d 100644 --- a/frontend/components/tasks/task-dashboard.tsx +++ b/frontend/components/tasks/task-dashboard.tsx @@ -1,320 +1,320 @@ -"use client"; - -import { useState, useEffect } from "react"; -import dynamic from "next/dynamic"; -import TaskForm from "./task-form"; -import TaskList from "./task-list"; -import TaskFiltersComponent from "./task-filters"; -import { useSocket } from "@/contexts/socket-context"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useTasks } from "@/hooks/useTasks"; -import { Badge } from "@/components/ui/badge"; -import { ModeToggle } from "@/components/mode-toggle"; -import { LogOut, LayoutDashboard, PlusCircle, CheckCircle2, Calendar, Mail, Kanban, List, Users, Wifi, WifiOff } from "lucide-react"; - -// Dynamic import para evitar problemas de SSR con drag & drop -const KanbanBoard = dynamic(() => import("@/components/kanban/kanban-board"), { - ssr: false, - loading: () => ( -
-
-
- ), -}); - -export default function TaskDashboard({ - token, - userId, - userEmail, -}: { - token: string; - userId: string; - userEmail: string | null; -}) { - const currentDate = new Date().toLocaleDateString("es-ES", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - }); - const { - tasks, - loading, - editingTask, - filters, - handleSubmit, - handleDeleteTask, - handleToggleCompletion, - handleEditTask, - handleCancelEdit, - handleFilterChange, - handleLogout, - } = useTasks(token); - - const [activeFilter, setActiveFilter] = useState("all"); - const [viewMode, setViewMode] = useState<"list" | "kanban">("list"); - - // Real-time socket connection - const { isConnected, onlineCount, subscribe } = useSocket(); - - // Subscribe to real-time task events - useEffect(() => { - const unsubCreated = subscribe("task:created", (data) => { - // Refresh tasks when a new task is created by another user - if (data.creatorId !== userId) { - handleFilterChange(filters); - } - }); - - const unsubUpdated = subscribe("task:updated", (data) => { - // Refresh tasks when a task is updated - if (data.updaterId !== userId) { - handleFilterChange(filters); - } - }); - - const unsubDeleted = subscribe("task:deleted", (data) => { - // Refresh tasks when a task is deleted - if (data.deleterId !== userId) { - handleFilterChange(filters); - } - }); - - return () => { - unsubCreated(); - unsubUpdated(); - unsubDeleted(); - }; - }, [subscribe, userId, filters, handleFilterChange]); - - const filteredTasks = tasks.filter((task) => { - if (activeFilter === "mis-tareas") { - return task.user_id === userId; - } else if (activeFilter === "completadas") { - return task.completed; - } - return true; - }); - - const myTasksCount = tasks.filter((task) => task.user_id === userId).length; - const completedTasksCount = tasks.filter((task) => task.completed).length; - const allTasksCount = tasks.length; - - // Count tasks by priority - const highPriorityCount = tasks.filter((t) => t.priority === 'high' && !t.completed).length; - - // Prepare tasks for kanban view - const kanbanTasks = { - todo: filteredTasks.filter((t) => !t.completed), - completed: filteredTasks.filter((t) => t.completed), - }; - - // Callback when tasks change in kanban (to refresh the list) - const handleTasksChange = () => { - // The useTasks hook should handle this, but we can force a refresh if needed - }; - - return ( -
-
-
-
-
- -
-

TaskFlow

-
-
-
- - {currentDate} -
- {userEmail && ( -
- - {userEmail} -
- )} -
-
- -
- {/* Online status indicator */} -
- {isConnected ? ( - <> - - - {onlineCount} - - ) : ( - <> - - Offline - - )} -
- {highPriorityCount > 0 && ( - - {highPriorityCount} urgente{highPriorityCount !== 1 ? 's' : ''} - - )} - - -
-
- -
- - -
-
- -
-
-
-
- -

Tus Tareas

-
- {/* View mode toggle */} -
- - -
-
- - - Todas - - {allTasksCount} - - - - Mias - - {myTasksCount} - - - - Hechas - - {completedTasksCount} - - - -
- - -
- -
- {loading ? ( -
-
-

Cargando tareas...

-
- ) : viewMode === "kanban" ? ( -
- -
- ) : ( - <> - - - - - - - - - - - - - )} -
- -
-
-
-
- ); -} +"use client"; + +import { useState, useEffect } from "react"; +import dynamic from "next/dynamic"; +import TaskForm from "./task-form"; +import TaskList from "./task-list"; +import TaskFiltersComponent from "./task-filters"; +import { useSocket } from "@/contexts/socket-context"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useTasks } from "@/hooks/useTasks"; +import { Badge } from "@/components/ui/badge"; +import { ModeToggle } from "@/components/mode-toggle"; +import { LogOut, LayoutDashboard, PlusCircle, CheckCircle2, Calendar, Mail, Kanban, List, Users, Wifi, WifiOff } from "lucide-react"; + +// Dynamic import para evitar problemas de SSR con drag & drop +const KanbanBoard = dynamic(() => import("@/components/kanban/kanban-board"), { + ssr: false, + loading: () => ( +
+
+
+ ), +}); + +export default function TaskDashboard({ + token, + userId, + userEmail, +}: { + token: string; + userId: string; + userEmail: string | null; +}) { + const currentDate = new Date().toLocaleDateString("es-ES", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + const { + tasks, + loading, + editingTask, + filters, + handleSubmit, + handleDeleteTask, + handleToggleCompletion, + handleEditTask, + handleCancelEdit, + handleFilterChange, + handleLogout, + } = useTasks(token); + + const [activeFilter, setActiveFilter] = useState("all"); + const [viewMode, setViewMode] = useState<"list" | "kanban">("list"); + + // Real-time socket connection + const { isConnected, onlineCount, subscribe } = useSocket(); + + // Subscribe to real-time task events + useEffect(() => { + const unsubCreated = subscribe("task:created", (data) => { + // Refresh tasks when a new task is created by another user + if (data.creatorId !== userId) { + handleFilterChange(filters); + } + }); + + const unsubUpdated = subscribe("task:updated", (data) => { + // Refresh tasks when a task is updated + if (data.updaterId !== userId) { + handleFilterChange(filters); + } + }); + + const unsubDeleted = subscribe("task:deleted", (data) => { + // Refresh tasks when a task is deleted + if (data.deleterId !== userId) { + handleFilterChange(filters); + } + }); + + return () => { + unsubCreated(); + unsubUpdated(); + unsubDeleted(); + }; + }, [subscribe, userId, filters, handleFilterChange]); + + const filteredTasks = tasks.filter((task) => { + if (activeFilter === "mis-tareas") { + return task.user_id === userId; + } else if (activeFilter === "completadas") { + return task.completed; + } + return true; + }); + + const myTasksCount = tasks.filter((task) => task.user_id === userId).length; + const completedTasksCount = tasks.filter((task) => task.completed).length; + const allTasksCount = tasks.length; + + // Count tasks by priority + const highPriorityCount = tasks.filter((t) => t.priority === 'high' && !t.completed).length; + + // Prepare tasks for kanban view + const kanbanTasks = { + todo: filteredTasks.filter((t) => !t.completed), + completed: filteredTasks.filter((t) => t.completed), + }; + + // Callback when tasks change in kanban (to refresh the list) + const handleTasksChange = () => { + // The useTasks hook should handle this, but we can force a refresh if needed + }; + + return ( +
+
+
+
+
+ +
+

TaskFlow

+
+
+
+ + {currentDate} +
+ {userEmail && ( +
+ + {userEmail} +
+ )} +
+
+ +
+ {/* Online status indicator */} +
+ {isConnected ? ( + <> + + + {onlineCount} + + ) : ( + <> + + Offline + + )} +
+ {highPriorityCount > 0 && ( + + {highPriorityCount} urgente{highPriorityCount !== 1 ? 's' : ''} + + )} + + +
+
+ +
+ + +
+
+ +
+
+
+
+ +

Tus Tareas

+
+ {/* View mode toggle */} +
+ + +
+
+ + + Todas + + {allTasksCount} + + + + Mias + + {myTasksCount} + + + + Hechas + + {completedTasksCount} + + + +
+ + +
+ +
+ {loading ? ( +
+
+

Cargando tareas...

+
+ ) : viewMode === "kanban" ? ( +
+ +
+ ) : ( + <> + + + + + + + + + + + + + )} +
+ +
+
+
+
+ ); +} diff --git a/frontend/hooks/useTasks.ts b/frontend/hooks/useTasks.ts index c76495f..b0ca916 100644 --- a/frontend/hooks/useTasks.ts +++ b/frontend/hooks/useTasks.ts @@ -1,216 +1,216 @@ -import { useState, useEffect, useCallback } from "react"; -import { useToast } from "@/components/ui/use-toast"; -import { useRouter } from "next/navigation"; -import { logoutAction } from "@/lib/auth"; - -import type { Task, TaskFilters } from "@/lib/types"; -import { - createTaskAction, - deleteTaskAction, - getTasksAction, - updateTaskAction, -} from "@/components/tasks/actions"; - -export function useTasks(token: string) { - const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(true); - const [editingTask, setEditingTask] = useState(null); - const [filters, setFilters] = useState({}); - - const { toast } = useToast(); - const router = useRouter(); - - const loadTasks = useCallback(async (currentFilters?: TaskFilters) => { - try { - setLoading(true); - const result = await getTasksAction(token, currentFilters); - if (result.success && result.tasks) { - setTasks(result.tasks); - } else { - toast({ - title: "Error", - description: result.error || "Ocurrio un error al cargar las tareas.", - variant: "destructive", - }); - } - } catch (error) { - toast({ - title: "Error", - description: "Failed to load tasks.", - variant: "destructive", - }); - } finally { - setLoading(false); - } - }, [token, toast]); - - async function handleSubmit( - id: string, - title: string, - description: string, - _token?: string, - assignedToId?: string, - dueDate?: string, - priority?: 'low' | 'medium' | 'high', - tagIds?: number[] - ) { - try { - if (id) { - const updatedTask = await updateTaskAction( - Number(id), - title, - description, - token, - undefined, - assignedToId, - dueDate, - priority, - tagIds - ); - - if (updatedTask.success && updatedTask.task) { - setTasks((prev) => - prev.map((task) => (task.id === id ? updatedTask.task : task)) - ); - setEditingTask(null); - toast({ - title: "Exito", - description: "Tarea modificada exitosamente.", - }); - } else { - setEditingTask(null); - throw new Error(updatedTask.error || "Failed to update task."); - } - } else { - const createdTask = await createTaskAction( - title, - description, - false, - token, - assignedToId, - dueDate, - priority, - tagIds - ); - if (createdTask.success && createdTask.task) { - setTasks((prev) => [...prev, createdTask.task.data]); - toast({ - title: "Exito", - description: "Tarea creada exitosamente.", - }); - } else { - throw new Error(createdTask.error || "Failed to create task."); - } - } - } catch (error) { - toast({ - title: "Error", - description: id ? `${error}` : "Failed to create task.", - variant: "destructive", - }); - } - } - - async function handleDeleteTask(id: string) { - try { - const deleted = await deleteTaskAction(Number(id), token); - if (deleted.success) { - setTasks((prev) => prev.filter((task) => task.id !== id)); - toast({ - title: "Exito", - description: "Tarea borrada exitosamente", - }); - } else { - throw new Error(deleted.error || "Failed to delete task."); - } - } catch (error) { - toast({ - title: "Failed to delete task.", - description: `${error}`, - variant: "destructive", - }); - } - } - - async function handleToggleCompletion(id: string) { - try { - const task = tasks.find((t) => t.id === id); - if (!task) return; - - const updated = await updateTaskAction( - Number(id), - undefined, - undefined, - token, - !task.completed - ); - - if (updated.success && updated.task) { - setTasks((prev) => prev.map((t) => (t.id === id ? updated.task : t))); - toast({ - title: "Exito", - description: `Tarea marcada como ${ - updated.task.completed ? "completada" : "pendiente" - }.`, - }); - } else { - throw new Error(updated.error || "Failed to toggle task."); - } - } catch (error) { - toast({ - title: "Failed to toggle task.", - description: `${error}`, - variant: "destructive", - }); - } - } - - function handleEditTask(task: Task) { - setEditingTask(task); - } - - function handleCancelEdit() { - setEditingTask(null); - } - - function handleFilterChange(newFilters: TaskFilters) { - setFilters(newFilters); - loadTasks(newFilters); - } - - async function handleLogout() { - try { - await logoutAction(); - router.push("/"); - toast({ - title: "Sesion cerrada", - description: "Has cerrado sesion correctamente.", - }); - } catch (error) { - toast({ - title: "Error", - description: "Error al cerrar sesion.", - variant: "destructive", - }); - } - } - - useEffect(() => { - loadTasks(filters); - }, []); - - return { - tasks, - loading, - editingTask, - filters, - handleSubmit, - handleDeleteTask, - handleToggleCompletion, - handleEditTask, - handleCancelEdit, - handleFilterChange, - handleLogout, - loadTasks, - }; -} +import { useState, useEffect, useCallback } from "react"; +import { useToast } from "@/components/ui/use-toast"; +import { useRouter } from "next/navigation"; +import { logoutAction } from "@/lib/auth"; + +import type { Task, TaskFilters } from "@/lib/types"; +import { + createTaskAction, + deleteTaskAction, + getTasksAction, + updateTaskAction, +} from "@/components/tasks/actions"; + +export function useTasks(token: string) { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [editingTask, setEditingTask] = useState(null); + const [filters, setFilters] = useState({}); + + const { toast } = useToast(); + const router = useRouter(); + + const loadTasks = useCallback(async (currentFilters?: TaskFilters) => { + try { + setLoading(true); + const result = await getTasksAction(token, currentFilters); + if (result.success && result.tasks) { + setTasks(result.tasks); + } else { + toast({ + title: "Error", + description: result.error || "Ocurrio un error al cargar las tareas.", + variant: "destructive", + }); + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to load tasks.", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }, [token, toast]); + + async function handleSubmit( + id: string, + title: string, + description: string, + _token?: string, + assignedToId?: string, + dueDate?: string, + priority?: 'low' | 'medium' | 'high', + tagIds?: number[] + ) { + try { + if (id) { + const updatedTask = await updateTaskAction( + Number(id), + title, + description, + token, + undefined, + assignedToId, + dueDate, + priority, + tagIds + ); + + if (updatedTask.success && updatedTask.task) { + setTasks((prev) => + prev.map((task) => (task.id === id ? updatedTask.task : task)) + ); + setEditingTask(null); + toast({ + title: "Exito", + description: "Tarea modificada exitosamente.", + }); + } else { + setEditingTask(null); + throw new Error(updatedTask.error || "Failed to update task."); + } + } else { + const createdTask = await createTaskAction( + title, + description, + false, + token, + assignedToId, + dueDate, + priority, + tagIds + ); + if (createdTask.success && createdTask.task) { + setTasks((prev) => [...prev, createdTask.task.data]); + toast({ + title: "Exito", + description: "Tarea creada exitosamente.", + }); + } else { + throw new Error(createdTask.error || "Failed to create task."); + } + } + } catch (error) { + toast({ + title: "Error", + description: id ? `${error}` : "Failed to create task.", + variant: "destructive", + }); + } + } + + async function handleDeleteTask(id: string) { + try { + const deleted = await deleteTaskAction(Number(id), token); + if (deleted.success) { + setTasks((prev) => prev.filter((task) => task.id !== id)); + toast({ + title: "Exito", + description: "Tarea borrada exitosamente", + }); + } else { + throw new Error(deleted.error || "Failed to delete task."); + } + } catch (error) { + toast({ + title: "Failed to delete task.", + description: `${error}`, + variant: "destructive", + }); + } + } + + async function handleToggleCompletion(id: string) { + try { + const task = tasks.find((t) => t.id === id); + if (!task) return; + + const updated = await updateTaskAction( + Number(id), + undefined, + undefined, + token, + !task.completed + ); + + if (updated.success && updated.task) { + setTasks((prev) => prev.map((t) => (t.id === id ? updated.task : t))); + toast({ + title: "Exito", + description: `Tarea marcada como ${ + updated.task.completed ? "completada" : "pendiente" + }.`, + }); + } else { + throw new Error(updated.error || "Failed to toggle task."); + } + } catch (error) { + toast({ + title: "Failed to toggle task.", + description: `${error}`, + variant: "destructive", + }); + } + } + + function handleEditTask(task: Task) { + setEditingTask(task); + } + + function handleCancelEdit() { + setEditingTask(null); + } + + function handleFilterChange(newFilters: TaskFilters) { + setFilters(newFilters); + loadTasks(newFilters); + } + + async function handleLogout() { + try { + await logoutAction(); + router.push("/"); + toast({ + title: "Sesion cerrada", + description: "Has cerrado sesion correctamente.", + }); + } catch (error) { + toast({ + title: "Error", + description: "Error al cerrar sesion.", + variant: "destructive", + }); + } + } + + useEffect(() => { + loadTasks(filters); + }, []); + + return { + tasks, + loading, + editingTask, + filters, + handleSubmit, + handleDeleteTask, + handleToggleCompletion, + handleEditTask, + handleCancelEdit, + handleFilterChange, + handleLogout, + loadTasks, + }; +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 2d55b61..5362740 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,c={};const s=(s,i)=>(s=new URL(s+".js",i).href,c[s]||new Promise(c=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=c,document.head.appendChild(e)}else e=s,importScripts(s),c()}).then(()=>{let e=c[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(i,a)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(c[n])return;let t={};const d=e=>s(e,n),f={module:{uri:n},exports:t,require:d};c[n]=Promise.all(i.map(e=>f[e]||d(e))).then(e=>(a(...e),t))}}define(["./workbox-00a24876"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/axEPQcV15qtCy6feOWdKV/_buildManifest.js",revision:"b2e0c1afcf73eecfc043400d028ae127"},{url:"/_next/static/axEPQcV15qtCy6feOWdKV/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/129-68fc242054d86901.js",revision:"68fc242054d86901"},{url:"/_next/static/chunks/215-ed1d47e23412d9f6.js",revision:"ed1d47e23412d9f6"},{url:"/_next/static/chunks/321-62282389347df307.js",revision:"62282389347df307"},{url:"/_next/static/chunks/444-429a1d267c330f44.js",revision:"429a1d267c330f44"},{url:"/_next/static/chunks/4bd1b696-b4ae08e3569d8f9c.js",revision:"b4ae08e3569d8f9c"},{url:"/_next/static/chunks/500-3f91014625dccb18.js",revision:"3f91014625dccb18"},{url:"/_next/static/chunks/541-1478dd105b418bcb.js",revision:"1478dd105b418bcb"},{url:"/_next/static/chunks/67-9bc5d5a1593bd0fe.js",revision:"9bc5d5a1593bd0fe"},{url:"/_next/static/chunks/762-c7ce4972b4fd19d8.js",revision:"c7ce4972b4fd19d8"},{url:"/_next/static/chunks/794-c665cdb1bd427b64.js",revision:"c665cdb1bd427b64"},{url:"/_next/static/chunks/828-473f3fdcb0b2fc39.js",revision:"473f3fdcb0b2fc39"},{url:"/_next/static/chunks/865.aabefd855c9d4c73.js",revision:"aabefd855c9d4c73"},{url:"/_next/static/chunks/964-4b4c91faebe8c69a.js",revision:"4b4c91faebe8c69a"},{url:"/_next/static/chunks/981-6cf62b3400df7781.js",revision:"6cf62b3400df7781"},{url:"/_next/static/chunks/996-d1df7e2e195a72e8.js",revision:"d1df7e2e195a72e8"},{url:"/_next/static/chunks/app/_global-error/page-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/app/_not-found/page-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/app/calendar/page-06b9cf5504efea44.js",revision:"06b9cf5504efea44"},{url:"/_next/static/chunks/app/dashboard/page-065ae8fe957b6a3f.js",revision:"065ae8fe957b6a3f"},{url:"/_next/static/chunks/app/layout-f70f369f4a378358.js",revision:"f70f369f4a378358"},{url:"/_next/static/chunks/app/not-found-cbecdd1d94a05896.js",revision:"cbecdd1d94a05896"},{url:"/_next/static/chunks/app/offline/page-22fb53a3e4f0f11e.js",revision:"22fb53a3e4f0f11e"},{url:"/_next/static/chunks/app/page-454300bfec82796d.js",revision:"454300bfec82796d"},{url:"/_next/static/chunks/app/projects/%5Bid%5D/page-ae966fa461a1ba1f.js",revision:"ae966fa461a1ba1f"},{url:"/_next/static/chunks/e80c4f76-ddd53b661743cd5c.js",revision:"ddd53b661743cd5c"},{url:"/_next/static/chunks/framework-b1648d2359927b2b.js",revision:"b1648d2359927b2b"},{url:"/_next/static/chunks/main-515d5029e7b50eb8.js",revision:"515d5029e7b50eb8"},{url:"/_next/static/chunks/main-app-12b38e5d516a7524.js",revision:"12b38e5d516a7524"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-d750d435da87bdd4.js",revision:"d750d435da87bdd4"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-382cbac2157f1ab3.js",revision:"382cbac2157f1ab3"},{url:"/_next/static/css/1a917740ad18b34a.css",revision:"1a917740ad18b34a"},{url:"/_next/static/css/ca576d7cd3fbcdc9.css",revision:"ca576d7cd3fbcdc9"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/favicon.svg",revision:"d2de3e724a337d785018c4fd6c5c86c9"},{url:"/icons/icon-128x128.svg",revision:"723429b95950cd840d5aa5ef723bba48"},{url:"/icons/icon-144x144.svg",revision:"790db6e5f8faa1c0d03ddc0cfa9fb4e6"},{url:"/icons/icon-152x152.svg",revision:"2a5a416eefba360f83eeb84d56d84cb7"},{url:"/icons/icon-192x192.svg",revision:"1f33e4295af22ea64c185e9ab194f085"},{url:"/icons/icon-384x384.svg",revision:"b7cddc266c22c88bd954c92cbfc90dc7"},{url:"/icons/icon-512x512.svg",revision:"bac7f43c67ffedce2fb4fc59adb9b2da"},{url:"/icons/icon-72x72.svg",revision:"9808538e2062511b43d0f4e85d656d9b"},{url:"/icons/icon-96x96.svg",revision:"1f453926196fe461b1e89719e95b68d8"},{url:"/manifest.json",revision:"32a3d43bedfce9b332ed9bbfb35dede7"},{url:"/placeholder-logo.png",revision:"b7d4c7dd55cf683c956391f9c2ce3f5b"},{url:"/placeholder-logo.svg",revision:"1e16dc7df824652c5906a2ab44aef78c"},{url:"/placeholder-user.jpg",revision:"82c9573f1276f9683ba7d92d8a8c6edd"},{url:"/placeholder.jpg",revision:"887632fd67dd19a0d58abde79d8e2640"},{url:"/placeholder.svg",revision:"35707bd9960ba5281c72af927b79291f"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:c,event:s,state:i})=>c&&"opaqueredirect"===c.type?new Response(c.body,{status:200,statusText:"OK",headers:c.headers}):c}]}),"GET"),e.registerRoute(/^https?:\/\/.*\/api\/.*/i,new e.NetworkFirst({cacheName:"api-cache",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/i,new e.CacheFirst({cacheName:"image-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:woff|woff2|ttf|otf|eot)$/i,new e.CacheFirst({cacheName:"font-cache",plugins:[new e.ExpirationPlugin({maxEntries:20,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/\.(?:js|css)$/i,new e.StaleWhileRevalidate({cacheName:"static-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:604800})]}),"GET")}); +if(!self.define){let e,c={};const s=(s,i)=>(s=new URL(s+".js",i).href,c[s]||new Promise(c=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=c,document.head.appendChild(e)}else e=s,importScripts(s),c()}).then(()=>{let e=c[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(i,a)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(c[n])return;let t={};const d=e=>s(e,n),f={module:{uri:n},exports:t,require:d};c[n]=Promise.all(i.map(e=>f[e]||d(e))).then(e=>(a(...e),t))}}define(["./workbox-00a24876"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/axEPQcV15qtCy6feOWdKV/_buildManifest.js",revision:"b2e0c1afcf73eecfc043400d028ae127"},{url:"/_next/static/axEPQcV15qtCy6feOWdKV/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/129-68fc242054d86901.js",revision:"68fc242054d86901"},{url:"/_next/static/chunks/215-ed1d47e23412d9f6.js",revision:"ed1d47e23412d9f6"},{url:"/_next/static/chunks/321-62282389347df307.js",revision:"62282389347df307"},{url:"/_next/static/chunks/444-429a1d267c330f44.js",revision:"429a1d267c330f44"},{url:"/_next/static/chunks/4bd1b696-b4ae08e3569d8f9c.js",revision:"b4ae08e3569d8f9c"},{url:"/_next/static/chunks/500-3f91014625dccb18.js",revision:"3f91014625dccb18"},{url:"/_next/static/chunks/541-1478dd105b418bcb.js",revision:"1478dd105b418bcb"},{url:"/_next/static/chunks/67-9bc5d5a1593bd0fe.js",revision:"9bc5d5a1593bd0fe"},{url:"/_next/static/chunks/762-c7ce4972b4fd19d8.js",revision:"c7ce4972b4fd19d8"},{url:"/_next/static/chunks/794-c665cdb1bd427b64.js",revision:"c665cdb1bd427b64"},{url:"/_next/static/chunks/828-473f3fdcb0b2fc39.js",revision:"473f3fdcb0b2fc39"},{url:"/_next/static/chunks/865.aabefd855c9d4c73.js",revision:"aabefd855c9d4c73"},{url:"/_next/static/chunks/964-4b4c91faebe8c69a.js",revision:"4b4c91faebe8c69a"},{url:"/_next/static/chunks/981-6cf62b3400df7781.js",revision:"6cf62b3400df7781"},{url:"/_next/static/chunks/996-d1df7e2e195a72e8.js",revision:"d1df7e2e195a72e8"},{url:"/_next/static/chunks/app/_global-error/page-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/app/_not-found/page-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/app/calendar/page-06b9cf5504efea44.js",revision:"06b9cf5504efea44"},{url:"/_next/static/chunks/app/dashboard/page-065ae8fe957b6a3f.js",revision:"065ae8fe957b6a3f"},{url:"/_next/static/chunks/app/layout-f70f369f4a378358.js",revision:"f70f369f4a378358"},{url:"/_next/static/chunks/app/not-found-cbecdd1d94a05896.js",revision:"cbecdd1d94a05896"},{url:"/_next/static/chunks/app/offline/page-22fb53a3e4f0f11e.js",revision:"22fb53a3e4f0f11e"},{url:"/_next/static/chunks/app/page-454300bfec82796d.js",revision:"454300bfec82796d"},{url:"/_next/static/chunks/app/projects/%5Bid%5D/page-ae966fa461a1ba1f.js",revision:"ae966fa461a1ba1f"},{url:"/_next/static/chunks/e80c4f76-ddd53b661743cd5c.js",revision:"ddd53b661743cd5c"},{url:"/_next/static/chunks/framework-b1648d2359927b2b.js",revision:"b1648d2359927b2b"},{url:"/_next/static/chunks/main-515d5029e7b50eb8.js",revision:"515d5029e7b50eb8"},{url:"/_next/static/chunks/main-app-12b38e5d516a7524.js",revision:"12b38e5d516a7524"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-d750d435da87bdd4.js",revision:"d750d435da87bdd4"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-ab9334f6e453f876.js",revision:"ab9334f6e453f876"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-382cbac2157f1ab3.js",revision:"382cbac2157f1ab3"},{url:"/_next/static/css/1a917740ad18b34a.css",revision:"1a917740ad18b34a"},{url:"/_next/static/css/ca576d7cd3fbcdc9.css",revision:"ca576d7cd3fbcdc9"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/favicon.svg",revision:"d2de3e724a337d785018c4fd6c5c86c9"},{url:"/icons/icon-128x128.svg",revision:"723429b95950cd840d5aa5ef723bba48"},{url:"/icons/icon-144x144.svg",revision:"790db6e5f8faa1c0d03ddc0cfa9fb4e6"},{url:"/icons/icon-152x152.svg",revision:"2a5a416eefba360f83eeb84d56d84cb7"},{url:"/icons/icon-192x192.svg",revision:"1f33e4295af22ea64c185e9ab194f085"},{url:"/icons/icon-384x384.svg",revision:"b7cddc266c22c88bd954c92cbfc90dc7"},{url:"/icons/icon-512x512.svg",revision:"bac7f43c67ffedce2fb4fc59adb9b2da"},{url:"/icons/icon-72x72.svg",revision:"9808538e2062511b43d0f4e85d656d9b"},{url:"/icons/icon-96x96.svg",revision:"1f453926196fe461b1e89719e95b68d8"},{url:"/manifest.json",revision:"32a3d43bedfce9b332ed9bbfb35dede7"},{url:"/placeholder-logo.png",revision:"b7d4c7dd55cf683c956391f9c2ce3f5b"},{url:"/placeholder-logo.svg",revision:"1e16dc7df824652c5906a2ab44aef78c"},{url:"/placeholder-user.jpg",revision:"82c9573f1276f9683ba7d92d8a8c6edd"},{url:"/placeholder.jpg",revision:"887632fd67dd19a0d58abde79d8e2640"},{url:"/placeholder.svg",revision:"35707bd9960ba5281c72af927b79291f"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:c,event:s,state:i})=>c&&"opaqueredirect"===c.type?new Response(c.body,{status:200,statusText:"OK",headers:c.headers}):c}]}),"GET"),e.registerRoute(/^https?:\/\/.*\/api\/.*/i,new e.NetworkFirst({cacheName:"api-cache",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/i,new e.CacheFirst({cacheName:"image-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:woff|woff2|ttf|otf|eot)$/i,new e.CacheFirst({cacheName:"font-cache",plugins:[new e.ExpirationPlugin({maxEntries:20,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/\.(?:js|css)$/i,new e.StaleWhileRevalidate({cacheName:"static-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:604800})]}),"GET")}); diff --git a/seed.sh b/seed.sh new file mode 100755 index 0000000..c6764bc --- /dev/null +++ b/seed.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Database Seed Script +# This script resets the database and populates it with test data + +echo "🌱 Starting database seed..." +echo "" + +# Check if containers are running +if ! docker compose ps | grep -q "Up"; then + echo "⚠️ Containers are not running. Starting them first..." + docker compose up -d + echo "⏳ Waiting for services to be ready..." + sleep 10 +fi + +# Run the seed script inside the NestJS container +echo "🔄 Running seed script..." +docker compose exec nestjs yarn seed + +echo "" +echo "✅ Seeding complete!" +echo "" +echo "You can now login with:" +echo " Email: admin@test.com" +echo " Password: password123" +echo ""