import { useNavigate, useSearch } from '@tanstack/react-router' import { useCallback, useState } from 'react' import { UploadCloud, FileText, Loader2, FlaskConical } from 'lucide-react' import { v4 as uuidv4 } from 'uuid' import { Card, CardContent } from '@/components/ui/card' import { parseBpmnXml, extractActivityElements, extractGatewayElements, BpmnParseError } from '@/domain/bpmn-parser' import { processRepo, activityRepo, gatewayRepo } from '@/persistence/repositories' import type { Process, Activity, GatewayConfig } from '@/domain/types' import { bpmnTypeToGatewayType } from '@/domain/types' import { useToast } from '@/components/ui/use-toast' import { AppFooter } from '@/components/AppFooter' import { AppHeader } from '@/components/AppHeader' const SAMPLE_PROCESSES = [ { id: 'simple-linear', fileName: 'simple-linear.bpmn', name: 'Proceso lineal simple', tags: ['5 tareas', 'Sin gateways'], }, { id: 'medium-with-gateways', fileName: 'medium-with-gateways.bpmn', name: 'Aprobación de crédito', tags: ['XOR', 'Paralelo', '11 tareas'], }, { id: 'complex-with-loop', fileName: 'complex-with-loop.bpmn', name: 'Control de calidad con revisión', tags: ['Loop', 'XOR', '9 tareas'], }, ] export function ImportPage() { const navigate = useNavigate() const { toast } = useToast() const [isDragging, setIsDragging] = useState(false) const [isProcessing, setIsProcessing] = useState(false) const [loadingSampleId, setLoadingSampleId] = useState(null) // groupId viene de la URL cuando el usuario importa desde dentro de un grupo const search = useSearch({ from: '/import' }) const targetGroupId = (search as { groupId?: string }).groupId ?? null const processFile = useCallback(async (file: File) => { if (!file.name.endsWith('.bpmn') && !file.name.endsWith('.xml')) { toast({ title: 'Formato no válido', description: 'El archivo debe tener extensión .bpmn o .xml', variant: 'destructive', }) return } setIsProcessing(true) try { const xml = await file.text() const graph = parseBpmnXml(xml) const processId = uuidv4() const now = Date.now() const process: Process = { id: processId, name: file.name.replace(/\.(bpmn|xml)$/, ''), clientName: '', bpmnXml: xml, currency: 'USD', overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, createdAt: now, updatedAt: now, groupId: targetGroupId, tags: [], } const activityElements = extractActivityElements(graph) const activities: Activity[] = activityElements.map((el) => ({ id: uuidv4(), processId, bpmnElementId: el.bpmnElementId, name: el.name, type: el.type, directCostFixed: 0, executionTimeMinutes: 0, assignedResources: [], automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0, automatedAssignedResources: [], })) const gatewayElements = extractGatewayElements(graph) const gateways: GatewayConfig[] = gatewayElements.map((el) => { const gwType = bpmnTypeToGatewayType(el.gatewayType as any) const n = el.outgoing.length return { id: uuidv4(), processId, bpmnElementId: el.bpmnElementId, gatewayType: gwType, branches: el.outgoing.map((flowId) => ({ flowId, targetElementId: graph.flows.get(flowId)?.targetRef ?? '', probability: gwType === 'parallel' ? 1.0 : parseFloat((1 / n).toFixed(4)), })), } }) await Promise.all([ processRepo.save(process), activityRepo.saveMany(activities), gatewayRepo.saveMany(gateways), ]) toast({ title: 'Proceso importado', description: `${activityElements.length} ${activityElements.length === 1 ? 'actividad' : 'actividades'} detectadas. Configurá los costos para simular.`, }) navigate({ to: '/workspace/$processId', params: { processId } }) } catch (err) { setIsProcessing(false) if (err instanceof BpmnParseError) { toast({ title: 'BPMN 2.0 no válido', description: 'El archivo no pudo ser interpretado como un diagrama BPMN 2.0 estándar.', variant: 'destructive', }) } else { toast({ title: 'Error al procesar el archivo', description: 'Ocurrió un error inesperado. Verificá que el archivo no esté dañado.', variant: 'destructive', }) console.error(err) } } }, [navigate, toast]) const onDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(false) const file = e.dataTransfer.files[0] if (file) processFile(file) }, [processFile]) const onFileInput = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) processFile(file) }, [processFile]) async function handleSampleLoad(sample: typeof SAMPLE_PROCESSES[0]) { setLoadingSampleId(sample.id) try { const response = await fetch(`/sample-processes/${sample.fileName}`) if (!response.ok) throw new Error('No se pudo cargar el proceso de ejemplo') const text = await response.text() const file = new File([text], sample.fileName, { type: 'text/xml' }) await processFile(file) } catch (err) { toast({ title: 'Error al cargar el ejemplo', description: 'No se pudo descargar el proceso. Intentá de nuevo.', variant: 'destructive', }) console.error(err) } finally { setLoadingSampleId(null) } } return (
{/* Header */}
Process Cost Platform · MVP Fase 0

Cuantificá el costo operativo
de tus procesos

Importá un archivo BPMN 2.0 y obtené un reporte visual de costos listo para presentar a tu cliente en minutos.

{/* Drop zone */} {/* Procesos de ejemplo — visibles directamente */}

O cargá un proceso de ejemplo con un click

{SAMPLE_PROCESSES.map((sample) => { const isLoading = loadingSampleId === sample.id return ( ) })}

100% en tu navegador · Sin backend · Sin registro · Datos guardados localmente

) }