2026-01-30 19:59:44 +00:00
|
|
|
// 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;
|
2026-01-31 12:56:51 +00:00
|
|
|
date_start: number | string | null;
|
|
|
|
|
date_end: number | string | null;
|
2026-01-30 19:59:44 +00:00
|
|
|
dateo: number | null; // fecha planificada inicio
|
|
|
|
|
datee: number | null; // fecha planificada fin
|
2026-01-31 12:56:51 +00:00
|
|
|
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
|
2026-01-30 19:59:44 +00:00
|
|
|
progress: number | string;
|
|
|
|
|
priority: string | number;
|
2026-01-31 12:56:51 +00:00
|
|
|
budget_amount: string | number | null;
|
|
|
|
|
rang: number | string;
|
2026-01-30 19:59:44 +00:00
|
|
|
status: string;
|
|
|
|
|
note_public: string;
|
|
|
|
|
note_private: string;
|
|
|
|
|
fk_user_creat: string | number;
|
2026-01-31 12:56:51 +00:00
|
|
|
fk_user_valid: string | number | null;
|
|
|
|
|
fk_user_modif: string | number | null;
|
2026-01-30 19:59:44 +00:00
|
|
|
// Campos adicionales que puede devolver la API
|
2026-01-31 12:56:51 +00:00
|
|
|
entity?: string | number;
|
|
|
|
|
billable?: string | number;
|
2026-01-30 19:59:44 +00:00
|
|
|
timespent?: number;
|
2026-01-31 12:56:51 +00:00
|
|
|
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
|
2026-01-30 19:59:44 +00:00
|
|
|
array_options?: Record<string, unknown>;
|
2026-01-31 12:56:51 +00:00
|
|
|
// Datos de tiempo
|
|
|
|
|
tms?: string | number | null;
|
2026-01-30 19:59:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
2026-02-06 19:06:25 +00:00
|
|
|
dependencies: number[]; // IDs de tareas que deben completarse antes de esta
|
2026-01-30 19:59:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configuración de estados de tarea
|
|
|
|
|
export const TASK_STATUS_CONFIG: Record<TaskStatus, { label: string; color: string; bgClass: string }> = {
|
|
|
|
|
'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<TaskPriority, { label: string; color: string; bgClass: string }> = {
|
|
|
|
|
'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
|
2026-01-31 12:56:51 +00:00
|
|
|
function timestampToDateString(timestamp: number | string | null | undefined): string | null {
|
|
|
|
|
if (!timestamp || timestamp === 0 || timestamp === '' || timestamp === '0') {
|
2026-01-30 19:59:44 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
2026-01-31 12:56:51 +00:00
|
|
|
const numericTimestamp = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
|
|
|
|
|
if (isNaN(numericTimestamp) || numericTimestamp === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return new Date(numericTimestamp * 1000).toISOString().split('T')[0];
|
2026-01-30 19:59:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper para convertir segundos a horas
|
2026-01-31 12:56:51 +00:00
|
|
|
function secondsToHours(seconds: number | string | null | undefined): number {
|
2026-01-30 19:59:44 +00:00
|
|
|
if (!seconds) return 0;
|
2026-01-31 12:56:51 +00:00
|
|
|
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;
|
2026-01-30 19:59:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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),
|
2026-01-31 12:56:51 +00:00
|
|
|
order: toNumber(dolibarr.rang),
|
2026-02-06 19:06:25 +00:00
|
|
|
dependencies: [], // Se cargan por separado desde el servicio de dependencias
|
2026-01-30 19:59:44 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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';
|
|
|
|
|
}
|
2026-01-31 12:56:51 +00:00
|
|
|
|
|
|
|
|
// ==================== 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;
|
2026-02-06 19:06:25 +00:00
|
|
|
dependencies: number[]; // IDs de tareas predecesoras
|
2026-01-31 12:56:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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<TaskFormData>): 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,
|
2026-02-06 19:06:25 +00:00
|
|
|
dependencies: task.dependencies || [],
|
2026-01-31 12:56:51 +00:00
|
|
|
};
|
|
|
|
|
}
|