Corrección 1 — Delete de proceso restaurado +

This commit is contained in:
2026-05-29 18:45:48 -03:00
parent 0a3c60cd09
commit 2955399ade
19 changed files with 502 additions and 98 deletions

View File

@@ -0,0 +1,183 @@
import { supabase } from '@/lib/supabase'
import type { Activity, ActivityResourceAssignment } from '@/domain/types'
function fromRow(
row: Record<string, unknown>,
assignedResources: ActivityResourceAssignment[],
automatedAssignedResources: ActivityResourceAssignment[],
): Activity {
return {
id: row.id as string,
processId: row.process_id as string,
bpmnElementId: row.bpmn_element_id as string,
name: (row.name as string) ?? '',
type: (row.type as Activity['type']) ?? 'task',
directCostFixed: Number(row.direct_cost_fixed ?? 0),
executionTimeMinutes: Number(row.execution_time_minutes ?? 0),
automatable: Boolean(row.automatable),
automatedCostFixed: Number(row.automated_cost_fixed ?? 0),
automatedTimeMinutes: Number(row.automated_time_minutes ?? 0),
assignedResources,
automatedAssignedResources,
}
}
function assignmentFromRow(row: Record<string, unknown>): ActivityResourceAssignment {
return {
resourceId: row.resource_id as string,
utilizationMinutes: Number(row.utilization_minutes ?? 0),
units: Number(row.units ?? 1),
}
}
// Extrae assignments de la relación anidada que Supabase devuelve con select('*, activity_resource_assignments(*)')
function extractAssignments(row: Record<string, unknown>): {
actual: ActivityResourceAssignment[]
automated: ActivityResourceAssignment[]
} {
const raw = (row.activity_resource_assignments as Array<Record<string, unknown>>) ?? []
return {
actual: raw.filter((a) => a.scenario === 'actual').map(assignmentFromRow),
automated: raw.filter((a) => a.scenario === 'automated').map(assignmentFromRow),
}
}
export const supabaseActivityRepo = {
async save(activity: Activity, userId: string): Promise<void> {
const { error: actError } = await supabase.from('activities').upsert({
id: activity.id,
process_id: activity.processId,
bpmn_element_id: activity.bpmnElementId,
name: activity.name,
type: activity.type,
direct_cost_fixed: activity.directCostFixed,
execution_time_minutes: activity.executionTimeMinutes,
automatable: activity.automatable,
automated_cost_fixed: activity.automatedCostFixed,
automated_time_minutes: activity.automatedTimeMinutes,
updated_by: userId,
updated_at: new Date().toISOString(),
})
if (actError) throw actError
const { error: delError } = await supabase
.from('activity_resource_assignments')
.delete()
.eq('activity_id', activity.id)
if (delError) throw delError
const assignments = [
...activity.assignedResources.map((r) => ({
activity_id: activity.id,
resource_id: r.resourceId,
utilization_minutes: r.utilizationMinutes,
units: r.units,
scenario: 'actual',
})),
...(activity.automatedAssignedResources ?? []).map((r) => ({
activity_id: activity.id,
resource_id: r.resourceId,
utilization_minutes: r.utilizationMinutes,
units: r.units,
scenario: 'automated',
})),
]
if (assignments.length > 0) {
const { error: insError } = await supabase
.from('activity_resource_assignments')
.insert(assignments)
if (insError) throw insError
}
},
// Bulk insert optimizado: 3 round-trips totales sin importar el número de actividades
async saveMany(activities: Activity[], userId: string): Promise<void> {
if (activities.length === 0) return
const now = new Date().toISOString()
const { error: actError } = await supabase.from('activities').upsert(
activities.map((a) => ({
id: a.id,
process_id: a.processId,
bpmn_element_id: a.bpmnElementId,
name: a.name,
type: a.type,
direct_cost_fixed: a.directCostFixed,
execution_time_minutes: a.executionTimeMinutes,
automatable: a.automatable,
automated_cost_fixed: a.automatedCostFixed,
automated_time_minutes: a.automatedTimeMinutes,
updated_by: userId,
updated_at: now,
}))
)
if (actError) throw actError
const activityIds = activities.map((a) => a.id)
const { error: delError } = await supabase
.from('activity_resource_assignments')
.delete()
.in('activity_id', activityIds)
if (delError) throw delError
const assignments = activities.flatMap((a) => [
...a.assignedResources.map((r) => ({
activity_id: a.id,
resource_id: r.resourceId,
utilization_minutes: r.utilizationMinutes,
units: r.units,
scenario: 'actual',
})),
...(a.automatedAssignedResources ?? []).map((r) => ({
activity_id: a.id,
resource_id: r.resourceId,
utilization_minutes: r.utilizationMinutes,
units: r.units,
scenario: 'automated',
})),
])
if (assignments.length > 0) {
const { error: insError } = await supabase
.from('activity_resource_assignments')
.insert(assignments)
if (insError) throw insError
}
},
// Un solo round-trip: activities + assignments anidados en una query
async getByProcess(processId: string): Promise<Activity[]> {
const { data, error } = await supabase
.from('activities')
.select('*, activity_resource_assignments(*)')
.eq('process_id', processId)
if (error) throw error
return (data ?? []).map((row) => {
const { actual, automated } = extractAssignments(row as Record<string, unknown>)
return fromRow(row as Record<string, unknown>, actual, automated)
})
},
// Un solo round-trip: actividad + assignments anidados
async getByBpmnElementId(processId: string, bpmnElementId: string): Promise<Activity | undefined> {
const { data, error } = await supabase
.from('activities')
.select('*, activity_resource_assignments(*)')
.eq('process_id', processId)
.eq('bpmn_element_id', bpmnElementId)
.single()
if (error) return undefined
const { actual, automated } = extractAssignments(data as Record<string, unknown>)
return fromRow(data as Record<string, unknown>, actual, automated)
},
async deleteByProcess(processId: string): Promise<void> {
// CASCADE elimina activity_resource_assignments automáticamente
const { error } = await supabase.from('activities').delete().eq('process_id', processId)
if (error) throw error
},
}

View File

@@ -0,0 +1,55 @@
import { supabase } from '@/lib/supabase'
import type { GatewayConfig } from '@/domain/types'
function fromRow(row: Record<string, unknown>): GatewayConfig {
return {
id: row.id as string,
processId: row.process_id as string,
bpmnElementId: row.bpmn_element_id as string,
gatewayType: row.gateway_type as GatewayConfig['gatewayType'],
branches: Array.isArray(row.branches) ? row.branches as GatewayConfig['branches'] : [],
}
}
export const supabaseGatewayRepo = {
async save(gateway: GatewayConfig): Promise<void> {
const { error } = await supabase.from('gateways').upsert({
id: gateway.id,
process_id: gateway.processId,
bpmn_element_id: gateway.bpmnElementId,
gateway_type: gateway.gatewayType,
branches: gateway.branches,
updated_at: new Date().toISOString(),
})
if (error) throw error
},
async saveMany(gateways: GatewayConfig[]): Promise<void> {
if (gateways.length === 0) return
const { error } = await supabase.from('gateways').upsert(
gateways.map((g) => ({
id: g.id,
process_id: g.processId,
bpmn_element_id: g.bpmnElementId,
gateway_type: g.gatewayType,
branches: g.branches,
updated_at: new Date().toISOString(),
}))
)
if (error) throw error
},
async getByProcess(processId: string): Promise<GatewayConfig[]> {
const { data, error } = await supabase
.from('gateways')
.select('*')
.eq('process_id', processId)
if (error) throw error
return (data ?? []).map(fromRow)
},
async deleteByProcess(processId: string): Promise<void> {
const { error } = await supabase.from('gateways').delete().eq('process_id', processId)
if (error) throw error
},
}

View File

@@ -66,14 +66,12 @@ export const supabaseProcessRepo = {
},
async delete(id: string): Promise<void> {
// CASCADE en Supabase elimina: activities, gateways, activity_resource_assignments
const { error } = await supabase.from('processes').delete().eq('id', id)
if (error) throw error
// TODO Sprint 4 Etapa 3-4: eliminar limpieza Dexie cuando actividades migren a Supabase
await db.transaction('rw', [db.activities, db.gateways, db.resources, db.simulations], async () => {
await db.activities.where('processId').equals(id).delete()
await db.gateways.where('processId').equals(id).delete()
await db.resources.where('processId').equals(id).delete()
// TODO Sprint 4 Etapa 4: eliminar cuando simulaciones migren a Supabase
await db.transaction('rw', [db.simulations], async () => {
await db.simulations.where('processId').equals(id).delete()
})
},

View File

@@ -0,0 +1,40 @@
import { supabase } from '@/lib/supabase'
import type { Resource } from '@/domain/types'
function fromRow(row: Record<string, unknown>): Resource {
return {
id: row.id as string,
name: row.name as string,
type: row.type as Resource['type'],
costPerHour: Number(row.cost_per_hour ?? 0),
}
}
export const supabaseResourceRepo = {
async save(resource: Resource, userId: string): Promise<void> {
const { error } = await supabase.from('resources').upsert({
id: resource.id,
name: resource.name,
type: resource.type,
cost_per_hour: resource.costPerHour,
created_by: userId,
updated_by: userId,
updated_at: new Date().toISOString(),
})
if (error) throw error
},
async getAll(): Promise<Resource[]> {
const { data, error } = await supabase
.from('resources')
.select('*')
.order('name', { ascending: true })
if (error) throw error
return (data ?? []).map(fromRow)
},
async delete(resourceId: string): Promise<void> {
const { error } = await supabase.from('resources').delete().eq('id', resourceId)
if (error) throw error
},
}