+function model(records:HistoricalRecord[]){const operational=records.filter(record=>record.record_status==='operational');const guideKg=operational.reduce((sum,row)=>sum+n(row.guide_kg),0),receivedKg=operational.reduce((sum,row)=>sum+n(row.received_kg),0);const providers=new Set(operational.map(row=>key(row.supplier_name??row.supplier_original)).filter(Boolean));const critical=operational.filter(row=>n(row.guide_kg)>0&&Math.abs(n(row.guide_kg)-n(row.received_kg))/n(row.guide_kg)>=.15);const over=operational.filter(row=>row.guide_kg!=null&&row.received_kg!=null&&n(row.received_kg)>n(row.guide_kg));const months=new Map<number,{guide:number;received:number}>();for(const row of operational){const raw=row.event_date??row.production_date??row.process_date??row.reception_date;if(!raw)continue;const date=new Date(raw);if(!Number.isFinite(date.getTime())||date.getFullYear()!==2025)continue;const month=date.getMonth(),entry=months.get(month)??{guide:0,received:0};entry.guide+=n(row.guide_kg);entry.received+=n(row.received_kg);months.set(month,entry)}const monthly=[...months.entries()].sort(([a],[b])=>a-b).map(([month,value])=>({month:monthNames[month],...value,difference:value.guide>0?((value.guide-value.received)/value.guide)*100:0}));const providerMap=new Map<string,{name:string;lots:number;guide:number;received:number;rejectionWeighted:number;rejectionWeight:number}>();for(const row of operational){const name=row.supplier_name??row.supplier_original??'Proveedor no identificado',id=key(name),entry=providerMap.get(id)??{name,lots:0,guide:0,received:0,rejectionWeighted:0,rejectionWeight:0};const guide=n(row.guide_kg),rejection=row.yields?.rechazo;entry.lots++;entry.guide+=guide;entry.received+=n(row.received_kg);if(rejection!=null&&Number.isFinite(Number(rejection))&&guide>0){entry.rejectionWeighted+=Number(rejection)*guide;entry.rejectionWeight+=guide}providerMap.set(id,entry)}const providerPerformance=[...providerMap.values()].sort((a,b)=>b.guide-a.guide).slice(0,5).map(item=>({name:item.name,lots:item.lots,guide:item.guide,difference:item.guide>0?((item.guide-item.received)/item.guide)*100:0,rejection:item.rejectionWeight>0?(item.rejectionWeighted/item.rejectionWeight)*100:0}));const exceptions=[...operational].map(row=>{const guide=n(row.guide_kg),received=n(row.received_kg),ratio=guide>0?Math.abs(guide-received)/guide:0,text=(row.observations??'').toLowerCase();const unsafe=/(vidrio|tijereta|plastico|plástico)/.test(text),receivedOver=guide>0&&received>guide;return {row,score:unsafe?4:receivedOver?3:ratio>=.15?2:/(rechaz|respaldo|mala calidad|sin producto)/.test(text)?1:0,type:unsafe?'Inocuidad':receivedOver?'Peso recibido mayor a guía':ratio>=.15?'Diferencia de peso ≥15%':'Observación de fuente'}}).filter(item=>item.score>0).sort((a,b)=>b.score-a.score||a.row.source_row-b.row.source_row).slice(0,4).map(item=>({id:`EX-${item.row.source_row}`,lot:item.row.lot_code,type:item.type,detail:`${item.row.guide_kg==null?'Sin kg guía':kg(n(item.row.guide_kg))} · ${item.row.received_kg==null?'sin kg recibidos':kg(n(item.row.received_kg))}${item.row.observations?` · ${item.row.observations}`:''}`,severity:item.score>=3?'Crítica':'Alta'}));return {records:records.length,operational:operational.length,guideKg,receivedKg,differenceKg:guideKg-receivedKg,providers:providers.size,duplicateGuides:duplicateValues(operational,row=>row.guide_number),duplicateLots:duplicateValues(operational,row=>row.lot_code),negativeDifferences:over.length,criticalDifferences:critical.length,monthly,providerPerformance,exceptions}}
0 commit comments