feat(mi-dia): app /hoy + CRM ligero para vendedores

- App Mi día: KPIs, mis actividades, mis leads con acciones rápidas
  (contactado/intento/agenda cita/descartar con motivo), captura rápida
  de lead (409 en duplicado por teléfono), vista equipo para coordinadores
- API: GET /api/mi-dia/resumen, POST /api/leads, /leads/:id/estado,
  /leads/:id/descartar; mapeo Usuario.mondayUserId (seed listo para
  cuando inviten a los vendedores a Monday)
- Landing: tarjeta Mi día; tests api 188, mi-dia 8, landing 4
This commit is contained in:
2026-08-28 06:23:15 +00:00
parent 1f5b6ee088
commit bc0f9da8fe
31 changed files with 5917 additions and 3 deletions

View File

@@ -0,0 +1,96 @@
// Tipos y transformaciones de la app "Mi día" (espejo del contrato del API)
export interface LeadMiDia {
id: string
nombre: string
telefono: string
email: string
estado: string
origen: string
updatedAt: string
}
export interface ActividadMiDia {
id: string
nombre: string
tipo: string
fecha: string // 'YYYY-MM-DD'
estado: string
}
export interface ApartadoPorVencer {
id: string
nombre: string
unidadCodigo: string
fechaLimite: string // 'YYYY-MM-DD'
estado: string
diasRestantes: number
}
export interface ResumenMiDia {
generadoEn: string
sinMapeoMonday: boolean
misLeads: LeadMiDia[]
misActividadesHoy: ActividadMiDia[]
apartadosPorVencer: ApartadoPorVencer[]
misOportunidades: { porEtapa: Record<string, number> } | null
notaOportunidades: string | null
conteos: {
leadsNuevosSemana: number
actividadesAbiertas: number
}
equipo?: {
leadsNuevosSinAsignar: number
porVendedor: { mondayUserId: number; nombre: string | null; leadsAsignados: number }[]
}
}
// Labels reales de la columna status 'Origen' del board Leads (verificados en Monday)
export const ORIGENES_LEAD = ['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro'] as const
// Categorías del dropdown Motivo No Compra (vocabulario CRM FRASA, igual que el API)
export const MOTIVOS_NO_COMPRA = [
'Precio',
'Producto',
'Ubicacion',
'Financiamiento',
'Competencia',
'Cliente',
'Timing',
] as const
export type MotivoNoCompra = (typeof MOTIVOS_NO_COMPRA)[number]
const ROLES_CON_EQUIPO = ['coordinador', 'corporativo', 'admin']
/** ¿El rol ve la sección de equipo? (misma regla que el API) */
export function esRolConEquipo(rol: string): boolean {
return ROLES_CON_EQUIPO.includes(rol)
}
/** 'YYYY-MM-DD' → 'dd/mm' (sin año: la agenda es de esta semana) */
export function formatoFechaCorta(fecha: string): string {
const [, m, d] = fecha.split('-')
if (!m || !d) return fecha
return `${d}/${m}`
}
/** Texto de urgencia de un apartado según los días que faltan */
export function textoDiasRestantes(dias: number): string {
if (dias <= 0) return 'Vence hoy'
if (dias === 1) return 'Vence mañana'
return `Vence en ${dias} días`
}
/** ¿La fecha de una actividad ya pasó? (vencida, para marcarla en la lista) */
export function esActividadVencida(fecha: string, hoy: string): boolean {
return fecha !== '' && fecha < hoy
}
/** Fecha local 'YYYY-MM-DD' de hoy (para comparar contra las del API) */
export function hoyLocal(): string {
const d = new Date()
const m = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${d.getFullYear()}-${m}-${dd}`
}