// types/task.ts // Estado de la tarea en Dolibarr export type TaskStatus = '0' | '1' | '2'; // 0: borrador, 1: validada, 2: cerrada/completada // Prioridad de la tarea export type TaskPriority = '0' | '1' | '2' | '3'; // 0: ninguna, 1: baja, 2: media, 3: alta // Interface para los datos crudos que vienen de Dolibarr export interface DolibarrTask { id: string | number; ref: string; label: string; description: string; fk_project: string | number; fk_task_parent: string | number; date_start: number | string | null; date_end: number | string | null; dateo: number | null; // fecha planificada inicio datee: number | null; // fecha planificada fin date_c: number | null; // fecha creación date_m: number | null; // fecha modificación duration_effective: number | string; // segundos trabajados planned_workload: number | string; // segundos planificados progress: number | string; priority: string | number; budget_amount: string | number | null; rang: number | string; status: string; note_public: string; note_private: string; fk_user_creat: string | number; fk_user_valid: string | number | null; fk_user_modif: string | number | null; // Campos adicionales que puede devolver la API entity?: string | number; billable?: string | number; timespent?: number; timespent_total_duration?: number | null; timespent_total_amount?: number | null; timespent_nblines?: number | null; // Referencias a proyecto projectref?: string | null; projectlabel?: string | null; projectstatus?: string | null; // Referencias a tercero socid?: string | number | null; thirdparty_id?: string | number | null; thirdparty_name?: string | null; // Tarea padre task_parent_ref?: string | null; // Campos extra array_options?: Record; // Datos de tiempo tms?: string | number | null; } // Interface normalizada para la UI export interface Task { id: number; ref: string; title: string; description: string; projectId: number; parentTaskId: number | null; status: TaskStatus; priority: TaskPriority; progress: number; plannedHours: number; workedHours: number; budget: number; startDate: string | null; endDate: string | null; plannedStartDate: string | null; plannedEndDate: string | null; createdAt: string; updatedAt: string; createdBy: number; order: number; dependencies: number[]; // IDs de tareas que deben completarse antes de esta } // Configuración de estados de tarea export const TASK_STATUS_CONFIG: Record = { '0': { label: 'Borrador', color: 'gray', bgClass: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' }, '1': { label: 'Validada', color: 'blue', bgClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' }, '2': { label: 'Completada', color: 'green', bgClass: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' }, }; // Configuración de prioridades export const TASK_PRIORITY_CONFIG: Record = { '0': { label: 'Sin prioridad', color: 'gray', bgClass: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400' }, '1': { label: 'Baja', color: 'blue', bgClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' }, '2': { label: 'Media', color: 'yellow', bgClass: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' }, '3': { label: 'Alta', color: 'red', bgClass: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' }, }; // Helper para convertir timestamp a fecha ISO function timestampToDateString(timestamp: number | string | null | undefined): string | null { if (!timestamp || timestamp === 0 || timestamp === '' || timestamp === '0') { return null; } const numericTimestamp = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp; if (isNaN(numericTimestamp) || numericTimestamp === 0) { return null; } return new Date(numericTimestamp * 1000).toISOString().split('T')[0]; } // Helper para convertir segundos a horas function secondsToHours(seconds: number | string | null | undefined): number { if (!seconds) return 0; const numericSeconds = typeof seconds === 'string' ? parseFloat(seconds) : seconds; if (isNaN(numericSeconds)) return 0; return Math.round((numericSeconds / 3600) * 10) / 10; // Redondear a 1 decimal } // Helper para convertir string/number a número seguro function toNumber(value: string | number | null | undefined): number { if (value === null || value === undefined) return 0; const num = typeof value === 'string' ? parseInt(value) : value; return isNaN(num) ? 0 : num; } // Función para mapear tarea de Dolibarr al formato UI export function mapDolibarrTask(dolibarr: DolibarrTask): Task { const progress = typeof dolibarr.progress === 'string' ? parseFloat(dolibarr.progress) : (dolibarr.progress || 0); const priority = String(dolibarr.priority || '0') as TaskPriority; const validPriorities: TaskPriority[] = ['0', '1', '2', '3']; const safePriority: TaskPriority = validPriorities.includes(priority) ? priority : '0'; const status = String(dolibarr.status || '0') as TaskStatus; const validStatuses: TaskStatus[] = ['0', '1', '2']; const safeStatus: TaskStatus = validStatuses.includes(status) ? status : '0'; return { id: typeof dolibarr.id === 'string' ? parseInt(dolibarr.id) : dolibarr.id, ref: dolibarr.ref || '', title: dolibarr.label || 'Sin título', description: dolibarr.description || '', projectId: typeof dolibarr.fk_project === 'string' ? parseInt(dolibarr.fk_project) : dolibarr.fk_project, parentTaskId: dolibarr.fk_task_parent ? (typeof dolibarr.fk_task_parent === 'string' ? parseInt(dolibarr.fk_task_parent) : dolibarr.fk_task_parent) : null, status: safeStatus, priority: safePriority, progress: Math.min(Math.max(progress, 0), 100), // Asegurar entre 0-100 plannedHours: secondsToHours(dolibarr.planned_workload), workedHours: secondsToHours(dolibarr.duration_effective || dolibarr.timespent), budget: typeof dolibarr.budget_amount === 'string' ? parseFloat(dolibarr.budget_amount) || 0 : (dolibarr.budget_amount || 0), startDate: timestampToDateString(dolibarr.date_start), endDate: timestampToDateString(dolibarr.date_end), plannedStartDate: timestampToDateString(dolibarr.dateo), plannedEndDate: timestampToDateString(dolibarr.datee), createdAt: timestampToDateString(dolibarr.date_c) || new Date().toISOString().split('T')[0], updatedAt: timestampToDateString(dolibarr.date_m) || new Date().toISOString().split('T')[0], createdBy: typeof dolibarr.fk_user_creat === 'string' ? parseInt(dolibarr.fk_user_creat) : (dolibarr.fk_user_creat || 0), order: toNumber(dolibarr.rang), dependencies: [], // Se cargan por separado desde el servicio de dependencias }; } // Helper para obtener label de estado export function getTaskStatusLabel(status: TaskStatus): string { return TASK_STATUS_CONFIG[status]?.label || 'Desconocido'; } // Helper para obtener label de prioridad export function getTaskPriorityLabel(priority: TaskPriority): string { return TASK_PRIORITY_CONFIG[priority]?.label || 'Sin prioridad'; } // ==================== TIPOS PARA API DE CREAR/ACTUALIZAR ==================== /** * Datos para crear una nueva tarea * Los campos requeridos por Dolibarr: label, fk_project, ref */ export interface CreateTaskData { ref: string; // Referencia única (requerido por Dolibarr) label: string; fk_project: number; description?: string; priority?: TaskPriority; progress?: number; planned_workload?: number; // en segundos date_start?: number; // timestamp date_end?: number; // timestamp dateo?: number; // fecha planificada inicio (timestamp) datee?: number; // fecha planificada fin (timestamp) fk_task_parent?: number; budget_amount?: number; } /** * Datos para actualizar una tarea existente * Todos los campos son opcionales */ export interface UpdateTaskData { label?: string; description?: string; priority?: string; progress?: number; status?: TaskStatus; planned_workload?: number; // en segundos date_start?: number; // timestamp date_end?: number; // timestamp dateo?: number; // fecha planificada inicio (timestamp) datee?: number; // fecha planificada fin (timestamp) fk_task_parent?: number; budget_amount?: number; } /** * Interface para el formulario de tareas (UI) * Usa formatos amigables (horas en lugar de segundos, fechas como strings) */ export interface TaskFormData { title: string; description: string; projectId: number; parentTaskId: number | null; status: TaskStatus; priority: TaskPriority; progress: number; plannedHours: number; startDate: string; // YYYY-MM-DD endDate: string; // YYYY-MM-DD budget: number; dependencies: number[]; // IDs de tareas predecesoras } // Helper para convertir fecha string a timestamp (segundos) export function dateStringToTimestamp(dateStr: string | null | undefined): number | undefined { if (!dateStr || dateStr === '') return undefined; const date = new Date(dateStr); if (isNaN(date.getTime())) return undefined; return Math.floor(date.getTime() / 1000); } // Helper para convertir horas a segundos export function hoursToSeconds(hours: number): number { return Math.round(hours * 3600); } // Helper para generar una referencia única para tareas function generateTaskRef(projectId: number): string { const timestamp = Date.now().toString(36).toUpperCase(); const random = Math.random().toString(36).substring(2, 6).toUpperCase(); return `TK${projectId}-${timestamp}-${random}`; } /** * Convierte datos del formulario UI al formato para crear tarea en Dolibarr */ export function formDataToCreateTask(formData: TaskFormData): CreateTaskData { return { ref: generateTaskRef(formData.projectId), label: formData.title, fk_project: formData.projectId, description: formData.description || undefined, priority: formData.priority, progress: formData.progress, planned_workload: formData.plannedHours > 0 ? hoursToSeconds(formData.plannedHours) : undefined, date_start: dateStringToTimestamp(formData.startDate), date_end: dateStringToTimestamp(formData.endDate), dateo: dateStringToTimestamp(formData.startDate), datee: dateStringToTimestamp(formData.endDate), fk_task_parent: formData.parentTaskId || undefined, budget_amount: formData.budget > 0 ? formData.budget : undefined, }; } /** * Convierte datos del formulario UI al formato para actualizar tarea en Dolibarr */ export function formDataToUpdateTask(formData: Partial): UpdateTaskData { const updateData: UpdateTaskData = {}; if (formData.title !== undefined) updateData.label = formData.title; if (formData.description !== undefined) updateData.description = formData.description; if (formData.priority !== undefined) updateData.priority = formData.priority; if (formData.progress !== undefined) updateData.progress = formData.progress; if (formData.status !== undefined) updateData.status = formData.status; if (formData.plannedHours !== undefined && formData.plannedHours > 0) { updateData.planned_workload = hoursToSeconds(formData.plannedHours); } if (formData.startDate !== undefined) { updateData.date_start = dateStringToTimestamp(formData.startDate); updateData.dateo = dateStringToTimestamp(formData.startDate); } if (formData.endDate !== undefined) { updateData.date_end = dateStringToTimestamp(formData.endDate); updateData.datee = dateStringToTimestamp(formData.endDate); } if (formData.parentTaskId !== undefined) { updateData.fk_task_parent = formData.parentTaskId || undefined; } if (formData.budget !== undefined && formData.budget > 0) { updateData.budget_amount = formData.budget; } return updateData; } /** * Convierte una tarea existente a datos del formulario */ export function taskToFormData(task: Task): TaskFormData { return { title: task.title, description: task.description || '', projectId: task.projectId, parentTaskId: task.parentTaskId, status: task.status, priority: task.priority, progress: task.progress, plannedHours: task.plannedHours, startDate: task.startDate || task.plannedStartDate || '', endDate: task.endDate || task.plannedEndDate || '', budget: task.budget, dependencies: task.dependencies || [], }; }