Sprint 4 — Etapa 7 completada. Build limpio, 521/521 tests verdes. Resumen:
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

Tipos: Resource.processId? eliminado (vestigial); ResourceType ya tenía los 5 valores correctos, sin cambios.

resource-repo.ts: agregado getAllWithCreator() con JOIN a users!created_by (mismo patrón que Etapa 6) y tipo ResourceWithCreator. delete() ahora valida que no haya asignaciones activas en activity_resource_assignments antes de borrar — lanza error explicativo si las hay. Verifiqué ambos contra Supabase real: el JOIN funciona (FK reconocida por PostgREST) y el conteo de guarda también.

Nuevo — src/features/resources/:

resource-display.tsx — labels, colores/íconos por tipo (Lucide: User/Briefcase/Server/Wrench/Package) y ResourceTypeAvatar compartido
ResourceFormSheet.tsx — Sheet reutilizable para crear/editar, usado tanto en el catálogo como en el panel del workspace
ResourceCatalogPage.tsx — página /recursos: búsqueda, filtro por tipo, tabla con badge de tipo, costo, "creado por", editar/eliminar con confirmación
Nuevo — src/components/ui/sheet.tsx: componente shadcn Sheet (no existía), construido sobre @radix-ui/react-dialog que ya estaba instalado — no agregué dependencias nuevas.

ResourcesPanel.tsx rediseñado: ahora solo muestra recursos usados en el proceso actual (derivado de assignedResources), con conteo de actividades por recurso. CRUD completo se mudó a /recursos; el panel solo permite "Nuevo" (catálogo global) y tiene el link "Ver catálogo completo".

Router: ruta /recursos registrada.

ActivityPanel no se tocó, como indicaba el scope.
This commit is contained in:
2026-06-19 00:01:46 -03:00
parent 4f51c74717
commit 892b8b3bc7
14 changed files with 789 additions and 189 deletions

View File

@@ -0,0 +1,112 @@
import { useEffect, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { Check } from 'lucide-react'
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { RESOURCE_TYPE_LABELS } from './resource-display'
import type { Resource, ResourceType } from '@/domain/types'
interface ResourceFormState {
name: string
type: ResourceType
costPerHour: string
}
const EMPTY_FORM: ResourceFormState = { name: '', type: 'role', costPerHour: '' }
interface ResourceFormSheetProps {
open: boolean
onOpenChange: (open: boolean) => void
resource?: Resource
onSave: (resource: Resource) => Promise<void>
}
export function ResourceFormSheet({ open, onOpenChange, resource, onSave }: ResourceFormSheetProps) {
const [form, setForm] = useState<ResourceFormState>(EMPTY_FORM)
const [saving, setSaving] = useState(false)
useEffect(() => {
if (open) {
setForm(
resource
? { name: resource.name, type: resource.type, costPerHour: resource.costPerHour.toString() }
: EMPTY_FORM
)
}
}, [open, resource])
async function handleSubmit() {
if (!form.name.trim()) return
setSaving(true)
try {
await onSave({
id: resource?.id ?? uuidv4(),
name: form.name.trim(),
type: form.type,
costPerHour: parseFloat(form.costPerHour) || 0,
})
onOpenChange(false)
} finally {
setSaving(false)
}
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent>
<SheetHeader>
<SheetTitle>{resource ? 'Editar recurso' : 'Nuevo recurso'}</SheetTitle>
</SheetHeader>
<div className="space-y-4 mt-6">
<div className="space-y-1.5">
<Label className="text-xs">Nombre</Label>
<Input
autoFocus
placeholder="ej. Analista Senior"
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Tipo</Label>
<Select value={form.type} onValueChange={(v) => setForm((f) => ({ ...f, type: v as ResourceType }))}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{(Object.entries(RESOURCE_TYPE_LABELS) as [ResourceType, string][]).map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Costo por hora</Label>
<Input
type="number"
min={0}
step={5}
placeholder="0.00"
value={form.costPerHour}
onChange={(e) => setForm((f) => ({ ...f, costPerHour: e.target.value }))}
className="font-mono"
/>
</div>
</div>
<SheetFooter className="mt-6">
<Button onClick={handleSubmit} disabled={!form.name.trim() || saving} className="gap-1.5">
<Check className="h-4 w-4" />
{resource ? 'Actualizar' : 'Crear'}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}