feat: MVP Fase 0 — Process Cost Platform v0.1.0

Plataforma completa de análisis de costos operativos basada en BPMN 2.0.

Funcionalidades incluidas:
- Import de archivos BPMN 2.0 por drag & drop + 3 procesos de ejemplo
- Visualización read-only del diagrama (bpmn-js)
- Configuración de actividades (costo, tiempo, recursos asignados)
- CRUD de recursos (rol, persona, sistema, equipo, insumo)
- Configuración global (moneda, overhead %, nombre, cliente)
- Motor de simulación determinístico agregado con propagación de gateways XOR/AND
- Reporte visual con mapa de calor en dos modos (relativo/absoluto)
- Gráficos: KPIs, top actividades, composición, costo por recurso
- Export PDF (jsPDF + html2canvas) y CSV (papaparse)
- Persistencia en IndexedDB (Dexie)
- 268 tests unitarios/integración + 6 E2E con Playwright
- Deploy: https://process-cost-platform.pages.dev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 01:58:40 -03:00
commit bed161c92a
97 changed files with 19638 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
import { useState, useEffect } from 'react'
import { Info } from 'lucide-react'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { useProcessStore } from '@/store/process-store'
import { formatPercent } from '@/lib/format'
const CURRENCIES = [
{ code: 'USD', label: 'USD — Dólar estadounidense' },
{ code: 'PYG', label: 'PYG — Guaraní paraguayo' },
{ code: 'BRL', label: 'BRL — Real brasileño' },
{ code: 'ARS', label: 'ARS — Peso argentino' },
{ code: 'EUR', label: 'EUR — Euro' },
{ code: 'COP', label: 'COP — Peso colombiano' },
{ code: 'MXN', label: 'MXN — Peso mexicano' },
{ code: 'CLP', label: 'CLP — Peso chileno' },
]
export function GlobalSettingsPanel() {
const { currentProcess, updateProcess } = useProcessStore()
const [localName, setLocalName] = useState('')
const [localClient, setLocalClient] = useState('')
const [localCurrency, setLocalCurrency] = useState('USD')
const [localOverhead, setLocalOverhead] = useState(20)
const [isDirty, setIsDirty] = useState(false)
useEffect(() => {
if (!currentProcess) return
setLocalName(currentProcess.name)
setLocalClient(currentProcess.clientName)
setLocalCurrency(currentProcess.currency)
setLocalOverhead(Math.round(currentProcess.overheadPercentage * 100))
setIsDirty(false)
}, [currentProcess])
function markDirty() { setIsDirty(true) }
async function handleSave() {
await updateProcess({
name: localName.trim() || 'Proceso sin nombre',
clientName: localClient.trim(),
currency: localCurrency,
overheadPercentage: localOverhead / 100,
})
setIsDirty(false)
}
if (!currentProcess) return null
return (
<ScrollArea className="h-full">
<div className="p-4 space-y-5">
{/* Nombre del proceso */}
<div className="space-y-1.5">
<Label htmlFor="proc-name" className="text-xs font-medium">Nombre del proceso</Label>
<Input
id="proc-name"
value={localName}
onChange={(e) => { setLocalName(e.target.value); markDirty() }}
placeholder="ej. Proceso de ventas"
className="h-8 text-sm"
/>
</div>
{/* Cliente */}
<div className="space-y-1.5">
<Label htmlFor="client-name" className="text-xs font-medium">Cliente</Label>
<Input
id="client-name"
value={localClient}
onChange={(e) => { setLocalClient(e.target.value); markDirty() }}
placeholder="ej. Empresa ABC"
className="h-8 text-sm"
/>
</div>
{/* Moneda */}
<div className="space-y-1.5">
<Label className="text-xs font-medium">Moneda</Label>
<Select value={localCurrency} onValueChange={(v) => { setLocalCurrency(v); markDirty() }}>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{CURRENCIES.map((c) => (
<SelectItem key={c.code} value={c.code} className="text-xs">{c.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Overhead */}
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label className="text-xs font-medium">Overhead (costos indirectos)</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3 w-3 text-slate-400 cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-52 text-xs">
Porcentaje adicional sobre los costos directos que representa los costos indirectos: alquiler, administración, infraestructura general.
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-3">
<Slider
value={[localOverhead]}
min={0}
max={100}
step={1}
onValueChange={([v]) => { setLocalOverhead(v); markDirty() }}
className="flex-1"
/>
<span className="text-sm font-mono font-semibold text-slate-700 w-12 text-right">
{formatPercent(localOverhead, 0)}
</span>
</div>
<p className="text-xs text-slate-400">
Típicamente entre 15% y 40% para empresas de servicios
</p>
</div>
{/* Guardar */}
<Button
size="sm"
onClick={handleSave}
disabled={!isDirty}
className="w-full"
>
{isDirty ? 'Guardar configuración' : 'Sin cambios'}
</Button>
</div>
</ScrollArea>
)
}