import { useNavigate, useSearch } from '@tanstack/react-router' import { useCallback, useEffect, 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 { supabaseProcessRepo } from '@/persistence/supabase/process-repo' import { supabaseActivityRepo } from '@/persistence/supabase/activity-repo' import { supabaseGatewayRepo } from '@/persistence/supabase/gateway-repo' import type { Process, Activity, GatewayConfig } from '@/domain/types' import { bpmnTypeToGatewayType } from '@/domain/types' import { useToast } from '@/components/ui/use-toast' import { useAuth } from '@/auth/AuthContext' 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 { user } = useAuth() const [isDragging, setIsDragging] = useState(false) const [isProcessing, setIsProcessing] = useState(false) const [loadingSampleId, setLoadingSampleId] = useState(null) useEffect(() => { if (!user) return const allowed = ['platform_admin', 'member', 'client_editor'] if (!allowed.includes(user.platformRole)) { void navigate({ to: '/' }) } }, [user, navigate]) // 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: [], ownerId: user!.id, updatedBy: user!.id, orgId: user!.orgId ?? null, areaId: null, } 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.race([ (async () => { await supabaseProcessRepo.save(process, user!.id) await Promise.all([ supabaseActivityRepo.saveMany(activities, user!.id), supabaseGatewayRepo.saveMany(gateways), ]) })(), new Promise((_, reject) => setTimeout( () => reject(new Error('Timeout: Supabase no respondió en 20s — verificá tu conexión')), 20_000, ) ), ]) 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) } } if (user?.platformRole === 'client_viewer') return null return (
{/* Header */}
InQ ROI

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 — solo para InQuality */} {user?.platformRole !== 'client_editor' &&

O cargá un proceso de ejemplo con un click

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