feat(crm): frontend del CRM (clientes API, navegación, dashboard + Kanban) y rebrand

- Clientes API tipados por entidad en src/lib/api/crm
- Navegación CRM en el sidebar
- Páginas: panel (KPIs + embudo), cuentas, contactos, prospectos, actividades
- Kanban de oportunidades con drag & drop (mueve entre etapas) y siembra de embudo
- Rebrand plantilla → CRM (.env, docker-compose name, package.json, README)
- Fix: CORE_DB_HOST=postgres (coincide con el servicio del compose)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aduanasoft
2026-07-14 09:53:56 -06:00
parent 088a8fc4df
commit af02145332
22 changed files with 2081 additions and 124 deletions

View File

@@ -0,0 +1,66 @@
/**
* Utilidades de formato y etiquetas legibles para el CRM.
*/
export function formatMoney(value: number | null | undefined, currency = 'MXN'): string {
if (value === null || value === undefined) return '—';
return new Intl.NumberFormat('es-MX', { style: 'currency', currency }).format(Number(value));
}
export function formatDate(value: string | null | undefined): string {
if (!value) return '—';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return '—';
return d.toLocaleDateString('es-MX', { year: 'numeric', month: 'short', day: 'numeric' });
}
export const ACCOUNT_TYPES: { value: string; label: string }[] = [
{ value: 'importador', label: 'Importador' },
{ value: 'exportador', label: 'Exportador' },
{ value: 'immex', label: 'IMMEX / Maquila' },
{ value: 'agencia_aduanal', label: 'Agencia aduanal' },
{ value: 'transportista', label: 'Transportista' },
{ value: 'otro', label: 'Otro' }
];
export const ACCOUNT_STATUS: { value: string; label: string }[] = [
{ value: 'active', label: 'Activa' },
{ value: 'prospect', label: 'Prospecto' },
{ value: 'inactive', label: 'Inactiva' }
];
export const LEAD_SOURCES: { value: string; label: string }[] = [
{ value: 'web', label: 'Web' },
{ value: 'referido', label: 'Referido' },
{ value: 'evento', label: 'Evento' },
{ value: 'llamada', label: 'Llamada' },
{ value: 'email', label: 'Email' },
{ value: 'otro', label: 'Otro' }
];
export const LEAD_STATUS: { value: string; label: string }[] = [
{ value: 'new', label: 'Nuevo' },
{ value: 'contacted', label: 'Contactado' },
{ value: 'qualified', label: 'Calificado' },
{ value: 'unqualified', label: 'No calificado' },
{ value: 'converted', label: 'Convertido' }
];
export const ACTIVITY_TYPES: { value: string; label: string }[] = [
{ value: 'call', label: 'Llamada' },
{ value: 'meeting', label: 'Reunión' },
{ value: 'task', label: 'Tarea' },
{ value: 'email', label: 'Correo' },
{ value: 'note', label: 'Nota' }
];
export const ACTIVITY_STATUS: { value: string; label: string }[] = [
{ value: 'pending', label: 'Pendiente' },
{ value: 'completed', label: 'Completada' },
{ value: 'canceled', label: 'Cancelada' }
];
export function labelOf(list: { value: string; label: string }[], value: string | null): string {
if (!value) return '—';
return list.find((x) => x.value === value)?.label ?? value;
}