feat(sprint-4/etapa-2): migración persistencia proceso/grupos/settings a Supabase
- Scripts SQL 002-004: process_groups, processes, app_settings con RLS - supabaseProcessRepo: save/getById/getAll/delete con mapeo camelCase↔snake_case y limpieza Dexie en delete (actividades/gateways/resources/simulations) - supabaseGroupRepo: save/getAll/getById/delete; FK ON DELETE SET NULL en processes - supabaseSettingsRepo: get/update sobre fila singleton - Stores actualizados: library-store y process-store usan repos Supabase para procesos (actividades/gateways/recursos siguen en Dexie hasta Etapa 3-4) - ImportPage y useReportData actualizados a supabaseProcessRepo - userId inyectado internamente en repos via supabase.auth.getSession() — no se threadea por la call stack; stores mantienen interfaces sin cambios - 561/561 tests verdes, build limpio Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,8 @@ import { UploadCloud, FileText, Loader2, FlaskConical } from 'lucide-react'
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import { parseBpmnXml, extractActivityElements, extractGatewayElements, BpmnParseError } from '@/domain/bpmn-parser'
|
import { parseBpmnXml, extractActivityElements, extractGatewayElements, BpmnParseError } from '@/domain/bpmn-parser'
|
||||||
import { processRepo, activityRepo, gatewayRepo } from '@/persistence/repositories'
|
import { activityRepo, gatewayRepo } from '@/persistence/repositories'
|
||||||
|
import { supabaseProcessRepo } from '@/persistence/supabase/process-repo'
|
||||||
import type { Process, Activity, GatewayConfig } from '@/domain/types'
|
import type { Process, Activity, GatewayConfig } from '@/domain/types'
|
||||||
import { bpmnTypeToGatewayType } from '@/domain/types'
|
import { bpmnTypeToGatewayType } from '@/domain/types'
|
||||||
import { useToast } from '@/components/ui/use-toast'
|
import { useToast } from '@/components/ui/use-toast'
|
||||||
@@ -112,7 +113,7 @@ export function ImportPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
processRepo.save(process),
|
supabaseProcessRepo.save(process),
|
||||||
activityRepo.saveMany(activities),
|
activityRepo.saveMany(activities),
|
||||||
gatewayRepo.saveMany(gateways),
|
gatewayRepo.saveMany(gateways),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { processRepo, activityRepo, resourceRepo, simulationRepo } from '@/persistence/repositories'
|
import { activityRepo, resourceRepo, simulationRepo } from '@/persistence/repositories'
|
||||||
|
import { supabaseProcessRepo } from '@/persistence/supabase/process-repo'
|
||||||
import type { Process, Activity, Resource, Simulation } from '@/domain/types'
|
import type { Process, Activity, Resource, Simulation } from '@/domain/types'
|
||||||
|
|
||||||
interface ReportData {
|
interface ReportData {
|
||||||
@@ -27,7 +28,7 @@ export function useReportData(processId: string): ReportData {
|
|||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const [proc, sim, acts, res] = await Promise.all([
|
const [proc, sim, acts, res] = await Promise.all([
|
||||||
processRepo.getById(processId),
|
supabaseProcessRepo.getById(processId),
|
||||||
simulationRepo.getLatestByProcess(processId),
|
simulationRepo.getLatestByProcess(processId),
|
||||||
activityRepo.getByProcess(processId),
|
activityRepo.getByProcess(processId),
|
||||||
resourceRepo.getByProcess(processId),
|
resourceRepo.getByProcess(processId),
|
||||||
|
|||||||
55
src/persistence/supabase/group-repo.ts
Normal file
55
src/persistence/supabase/group-repo.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { supabase } from '@/lib/supabase'
|
||||||
|
import type { ProcessGroup } from '@/domain/types'
|
||||||
|
|
||||||
|
async function getCurrentUserId(): Promise<string> {
|
||||||
|
const { data: { session } } = await supabase.auth.getSession()
|
||||||
|
if (!session?.user) throw new Error('Usuario no autenticado')
|
||||||
|
return session.user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromRow(row: Record<string, unknown>): ProcessGroup {
|
||||||
|
return {
|
||||||
|
id: row.id as string,
|
||||||
|
name: row.name as string,
|
||||||
|
createdAt: new Date(row.created_at as string).getTime(),
|
||||||
|
updatedAt: new Date(row.updated_at as string).getTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabaseGroupRepo = {
|
||||||
|
async save(group: ProcessGroup): Promise<void> {
|
||||||
|
const userId = await getCurrentUserId()
|
||||||
|
const { error } = await supabase.from('process_groups').upsert({
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
created_by: userId,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
if (error) throw error
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAll(): Promise<ProcessGroup[]> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('process_groups')
|
||||||
|
.select('*')
|
||||||
|
.order('name', { ascending: true })
|
||||||
|
if (error) throw error
|
||||||
|
return (data ?? []).map(fromRow)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getById(id: string): Promise<ProcessGroup | undefined> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('process_groups')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', id)
|
||||||
|
.single()
|
||||||
|
if (error) return undefined
|
||||||
|
return fromRow(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
// La FK group_id en processes tiene ON DELETE SET NULL → procesos pasan a "Sin clasificar"
|
||||||
|
const { error } = await supabase.from('process_groups').delete().eq('id', id)
|
||||||
|
if (error) throw error
|
||||||
|
},
|
||||||
|
}
|
||||||
87
src/persistence/supabase/process-repo.ts
Normal file
87
src/persistence/supabase/process-repo.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { supabase } from '@/lib/supabase'
|
||||||
|
import { db } from '@/persistence/db'
|
||||||
|
import type { Process } from '@/domain/types'
|
||||||
|
|
||||||
|
async function getCurrentUserId(): Promise<string> {
|
||||||
|
const { data: { session } } = await supabase.auth.getSession()
|
||||||
|
if (!session?.user) throw new Error('Usuario no autenticado')
|
||||||
|
return session.user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRow(process: Process, userId: string) {
|
||||||
|
return {
|
||||||
|
id: process.id,
|
||||||
|
name: process.name,
|
||||||
|
client: process.clientName,
|
||||||
|
bpmn_xml: process.bpmnXml,
|
||||||
|
currency: process.currency,
|
||||||
|
overhead_percentage: process.overheadPercentage,
|
||||||
|
annual_frequency: process.annualFrequency,
|
||||||
|
analysis_horizon_years: process.analysisHorizonYears,
|
||||||
|
automation_investment: process.automationInvestment,
|
||||||
|
group_id: process.groupId,
|
||||||
|
tags: process.tags,
|
||||||
|
owner_id: userId,
|
||||||
|
created_by: userId,
|
||||||
|
updated_by: userId,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromRow(row: Record<string, unknown>): Process {
|
||||||
|
return {
|
||||||
|
id: row.id as string,
|
||||||
|
name: row.name as string,
|
||||||
|
clientName: (row.client as string) ?? '',
|
||||||
|
bpmnXml: (row.bpmn_xml as string) ?? '',
|
||||||
|
currency: (row.currency as string) ?? 'USD',
|
||||||
|
overheadPercentage: Number(row.overhead_percentage ?? 0),
|
||||||
|
annualFrequency: Number(row.annual_frequency ?? 1000),
|
||||||
|
analysisHorizonYears: Number(row.analysis_horizon_years ?? 3),
|
||||||
|
automationInvestment: Number(row.automation_investment ?? 0),
|
||||||
|
groupId: (row.group_id as string | null) ?? null,
|
||||||
|
tags: Array.isArray(row.tags) ? (row.tags as string[]) : [],
|
||||||
|
createdAt: new Date(row.created_at as string).getTime(),
|
||||||
|
updatedAt: new Date(row.updated_at as string).getTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabaseProcessRepo = {
|
||||||
|
async save(process: Process): Promise<void> {
|
||||||
|
const userId = await getCurrentUserId()
|
||||||
|
const { error } = await supabase.from('processes').upsert(toRow(process, userId))
|
||||||
|
if (error) throw error
|
||||||
|
},
|
||||||
|
|
||||||
|
async getById(id: string): Promise<Process | undefined> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('processes')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', id)
|
||||||
|
.single()
|
||||||
|
if (error) return undefined
|
||||||
|
return fromRow(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAll(): Promise<Process[]> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('processes')
|
||||||
|
.select('*')
|
||||||
|
.order('updated_at', { ascending: false })
|
||||||
|
if (error) throw error
|
||||||
|
return (data ?? []).map(fromRow)
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
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()
|
||||||
|
await db.simulations.where('processId').equals(id).delete()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
27
src/persistence/supabase/settings-repo.ts
Normal file
27
src/persistence/supabase/settings-repo.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { supabase } from '@/lib/supabase'
|
||||||
|
import type { AppSettings } from '@/domain/types'
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: AppSettings = { id: 'singleton', groupLabel: 'Cliente' }
|
||||||
|
|
||||||
|
export const supabaseSettingsRepo = {
|
||||||
|
async get(): Promise<AppSettings> {
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('app_settings')
|
||||||
|
.select('id, group_label')
|
||||||
|
.eq('id', 'singleton')
|
||||||
|
.single()
|
||||||
|
if (!data) return DEFAULT_SETTINGS
|
||||||
|
return { id: 'singleton', groupLabel: (data.group_label as string) ?? 'Cliente' }
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(updates: Partial<Omit<AppSettings, 'id'>>): Promise<void> {
|
||||||
|
const patch: Record<string, unknown> = { updated_at: new Date().toISOString() }
|
||||||
|
if (updates.groupLabel !== undefined) patch.group_label = updates.groupLabel
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('app_settings')
|
||||||
|
.update(patch)
|
||||||
|
.eq('id', 'singleton')
|
||||||
|
if (error) throw error
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { processRepo, groupRepo, settingsRepo, simulationRepo } from '@/persistence/repositories'
|
import { supabaseProcessRepo } from '@/persistence/supabase/process-repo'
|
||||||
|
import { supabaseGroupRepo } from '@/persistence/supabase/group-repo'
|
||||||
|
import { supabaseSettingsRepo } from '@/persistence/supabase/settings-repo'
|
||||||
|
import { simulationRepo } from '@/persistence/repositories'
|
||||||
import type { Process, ProcessGroup, AppSettings, Simulation } from '@/domain/types'
|
import type { Process, ProcessGroup, AppSettings, Simulation } from '@/domain/types'
|
||||||
|
|
||||||
interface LibraryState {
|
interface LibraryState {
|
||||||
@@ -14,6 +17,8 @@ interface LibraryState {
|
|||||||
createGroup: (name: string) => Promise<ProcessGroup>
|
createGroup: (name: string) => Promise<ProcessGroup>
|
||||||
deleteGroup: (id: string) => Promise<void>
|
deleteGroup: (id: string) => Promise<void>
|
||||||
|
|
||||||
|
deleteProcess: (id: string) => Promise<void>
|
||||||
|
|
||||||
assignProcessToGroup: (processId: string, groupId: string | null) => Promise<void>
|
assignProcessToGroup: (processId: string, groupId: string | null) => Promise<void>
|
||||||
|
|
||||||
getProcessesByGroup: (groupId: string | null) => Process[]
|
getProcessesByGroup: (groupId: string | null) => Process[]
|
||||||
@@ -33,42 +38,40 @@ export const useLibraryStore = create<LibraryState>((set, get) => ({
|
|||||||
|
|
||||||
load: async () => {
|
load: async () => {
|
||||||
set({ isLoading: true })
|
set({ isLoading: true })
|
||||||
const [groups, rawProcesses, settings] = await Promise.all([
|
const [groups, processes, settings] = await Promise.all([
|
||||||
groupRepo.getAll(),
|
supabaseGroupRepo.getAll(),
|
||||||
processRepo.getAll(),
|
supabaseProcessRepo.getAll(),
|
||||||
settingsRepo.get(),
|
supabaseSettingsRepo.get(),
|
||||||
])
|
])
|
||||||
// Normalizar datos legacy que puedan llegar sin tags/groupId del IndexedDB
|
|
||||||
const processes = rawProcesses.map((p) => ({
|
|
||||||
...p,
|
|
||||||
groupId: p.groupId ?? null,
|
|
||||||
tags: Array.isArray(p.tags) ? p.tags : [],
|
|
||||||
}))
|
|
||||||
set({ groups, processes, settings, isLoading: false })
|
set({ groups, processes, settings, isLoading: false })
|
||||||
},
|
},
|
||||||
|
|
||||||
createGroup: async (name: string) => {
|
createGroup: async (name: string) => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const group: ProcessGroup = { id: uuidv4(), name, createdAt: now, updatedAt: now }
|
const group: ProcessGroup = { id: uuidv4(), name, createdAt: now, updatedAt: now }
|
||||||
await groupRepo.save(group)
|
await supabaseGroupRepo.save(group)
|
||||||
set((state) => ({ groups: [...state.groups, group].sort((a, b) => a.name.localeCompare(b.name)) }))
|
set((state) => ({ groups: [...state.groups, group].sort((a, b) => a.name.localeCompare(b.name)) }))
|
||||||
return group
|
return group
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteGroup: async (id: string) => {
|
deleteGroup: async (id: string) => {
|
||||||
await groupRepo.delete(id)
|
await supabaseGroupRepo.delete(id)
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
groups: state.groups.filter((g) => g.id !== id),
|
groups: state.groups.filter((g) => g.id !== id),
|
||||||
// Los procesos del grupo pasan a groupId = null
|
|
||||||
processes: state.processes.map((p) => p.groupId === id ? { ...p, groupId: null } : p),
|
processes: state.processes.map((p) => p.groupId === id ? { ...p, groupId: null } : p),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteProcess: async (id: string) => {
|
||||||
|
await supabaseProcessRepo.delete(id)
|
||||||
|
set((state) => ({ processes: state.processes.filter((p) => p.id !== id) }))
|
||||||
|
},
|
||||||
|
|
||||||
assignProcessToGroup: async (processId: string, groupId: string | null) => {
|
assignProcessToGroup: async (processId: string, groupId: string | null) => {
|
||||||
const process = get().processes.find((p) => p.id === processId)
|
const process = get().processes.find((p) => p.id === processId)
|
||||||
if (!process) return
|
if (!process) return
|
||||||
const updated = { ...process, groupId, updatedAt: Date.now() }
|
const updated = { ...process, groupId, updatedAt: Date.now() }
|
||||||
await processRepo.save(updated)
|
await supabaseProcessRepo.save(updated)
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
processes: state.processes.map((p) => p.id === processId ? updated : p),
|
processes: state.processes.map((p) => p.id === processId ? updated : p),
|
||||||
}))
|
}))
|
||||||
@@ -86,7 +89,7 @@ export const useLibraryStore = create<LibraryState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateSettings: async (updates) => {
|
updateSettings: async (updates) => {
|
||||||
await settingsRepo.update(updates)
|
await supabaseSettingsRepo.update(updates)
|
||||||
set((state) => ({ settings: { ...state.settings, ...updates } }))
|
set((state) => ({ settings: { ...state.settings, ...updates } }))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { processRepo, activityRepo, gatewayRepo, resourceRepo } from '@/persistence/repositories'
|
import { supabaseProcessRepo } from '@/persistence/supabase/process-repo'
|
||||||
|
import { activityRepo, gatewayRepo, resourceRepo } from '@/persistence/repositories'
|
||||||
import type { Process, Activity, GatewayConfig, Resource } from '@/domain/types'
|
import type { Process, Activity, GatewayConfig, Resource } from '@/domain/types'
|
||||||
|
|
||||||
interface ProcessState {
|
interface ProcessState {
|
||||||
@@ -10,7 +11,6 @@ interface ProcessState {
|
|||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
error: string | null
|
error: string | null
|
||||||
|
|
||||||
// Acciones
|
|
||||||
loadProcess: (processId: string) => Promise<void>
|
loadProcess: (processId: string) => Promise<void>
|
||||||
setCurrentProcess: (process: Process) => Promise<void>
|
setCurrentProcess: (process: Process) => Promise<void>
|
||||||
updateProcess: (updates: Partial<Process>) => Promise<void>
|
updateProcess: (updates: Partial<Process>) => Promise<void>
|
||||||
@@ -34,7 +34,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
|
|||||||
set({ isLoading: true, error: null })
|
set({ isLoading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const [process, activities, gateways, resources] = await Promise.all([
|
const [process, activities, gateways, resources] = await Promise.all([
|
||||||
processRepo.getById(processId),
|
supabaseProcessRepo.getById(processId),
|
||||||
activityRepo.getByProcess(processId),
|
activityRepo.getByProcess(processId),
|
||||||
gatewayRepo.getByProcess(processId),
|
gatewayRepo.getByProcess(processId),
|
||||||
resourceRepo.getByProcess(processId),
|
resourceRepo.getByProcess(processId),
|
||||||
@@ -47,7 +47,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setCurrentProcess: async (process) => {
|
setCurrentProcess: async (process) => {
|
||||||
await processRepo.save(process)
|
await supabaseProcessRepo.save(process)
|
||||||
const [activities, gateways, resources] = await Promise.all([
|
const [activities, gateways, resources] = await Promise.all([
|
||||||
activityRepo.getByProcess(process.id),
|
activityRepo.getByProcess(process.id),
|
||||||
gatewayRepo.getByProcess(process.id),
|
gatewayRepo.getByProcess(process.id),
|
||||||
@@ -60,7 +60,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
|
|||||||
const current = get().currentProcess
|
const current = get().currentProcess
|
||||||
if (!current) return
|
if (!current) return
|
||||||
const updated: Process = { ...current, ...updates, updatedAt: Date.now() }
|
const updated: Process = { ...current, ...updates, updatedAt: Date.now() }
|
||||||
await processRepo.save(updated)
|
await supabaseProcessRepo.save(updated)
|
||||||
set({ currentProcess: updated })
|
set({ currentProcess: updated })
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
|
|||||||
const current = get().currentProcess
|
const current = get().currentProcess
|
||||||
if (current) {
|
if (current) {
|
||||||
const updatedProcess = { ...current, updatedAt: Date.now() }
|
const updatedProcess = { ...current, updatedAt: Date.now() }
|
||||||
await processRepo.save(updatedProcess)
|
await supabaseProcessRepo.save(updatedProcess)
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
activities: state.activities.map((a) => (a.id === activity.id ? activity : a)),
|
activities: state.activities.map((a) => (a.id === activity.id ? activity : a)),
|
||||||
currentProcess: updatedProcess,
|
currentProcess: updatedProcess,
|
||||||
@@ -86,7 +86,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
|
|||||||
const current = get().currentProcess
|
const current = get().currentProcess
|
||||||
if (current) {
|
if (current) {
|
||||||
const updatedProcess = { ...current, updatedAt: Date.now() }
|
const updatedProcess = { ...current, updatedAt: Date.now() }
|
||||||
await processRepo.save(updatedProcess)
|
await supabaseProcessRepo.save(updatedProcess)
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
gateways: state.gateways.map((g) => (g.id === gateway.id ? gateway : g)),
|
gateways: state.gateways.map((g) => (g.id === gateway.id ? gateway : g)),
|
||||||
currentProcess: updatedProcess,
|
currentProcess: updatedProcess,
|
||||||
@@ -103,7 +103,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
|
|||||||
const current = get().currentProcess
|
const current = get().currentProcess
|
||||||
if (current) {
|
if (current) {
|
||||||
const updatedProcess = { ...current, updatedAt: Date.now() }
|
const updatedProcess = { ...current, updatedAt: Date.now() }
|
||||||
await processRepo.save(updatedProcess)
|
await supabaseProcessRepo.save(updatedProcess)
|
||||||
set((state) => ({ resources: [...state.resources, resource], currentProcess: updatedProcess }))
|
set((state) => ({ resources: [...state.resources, resource], currentProcess: updatedProcess }))
|
||||||
} else {
|
} else {
|
||||||
set((state) => ({ resources: [...state.resources, resource] }))
|
set((state) => ({ resources: [...state.resources, resource] }))
|
||||||
@@ -115,7 +115,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
|
|||||||
const current = get().currentProcess
|
const current = get().currentProcess
|
||||||
if (current) {
|
if (current) {
|
||||||
const updatedProcess = { ...current, updatedAt: Date.now() }
|
const updatedProcess = { ...current, updatedAt: Date.now() }
|
||||||
await processRepo.save(updatedProcess)
|
await supabaseProcessRepo.save(updatedProcess)
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
resources: state.resources.map((r) => (r.id === resource.id ? resource : r)),
|
resources: state.resources.map((r) => (r.id === resource.id ? resource : r)),
|
||||||
currentProcess: updatedProcess,
|
currentProcess: updatedProcess,
|
||||||
@@ -132,7 +132,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
|
|||||||
const current = get().currentProcess
|
const current = get().currentProcess
|
||||||
if (current) {
|
if (current) {
|
||||||
const updatedProcess = { ...current, updatedAt: Date.now() }
|
const updatedProcess = { ...current, updatedAt: Date.now() }
|
||||||
await processRepo.save(updatedProcess)
|
await supabaseProcessRepo.save(updatedProcess)
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
resources: state.resources.filter((r) => r.id !== resourceId),
|
resources: state.resources.filter((r) => r.id !== resourceId),
|
||||||
currentProcess: updatedProcess,
|
currentProcess: updatedProcess,
|
||||||
|
|||||||
13
supabase/migrations/002_create_process_groups.sql
Normal file
13
supabase/migrations/002_create_process_groups.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS public.process_groups (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL,
|
||||||
|
created_by uuid REFERENCES public.users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.process_groups ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "all_process_groups" ON public.process_groups
|
||||||
|
FOR ALL USING (auth.uid() IS NOT NULL)
|
||||||
|
WITH CHECK (auth.uid() IS NOT NULL);
|
||||||
26
supabase/migrations/003_create_processes.sql
Normal file
26
supabase/migrations/003_create_processes.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS public.processes (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL,
|
||||||
|
client text NOT NULL DEFAULT '',
|
||||||
|
bpmn_xml text NOT NULL DEFAULT '',
|
||||||
|
currency text NOT NULL DEFAULT 'USD',
|
||||||
|
overhead_percentage numeric NOT NULL DEFAULT 0,
|
||||||
|
annual_frequency numeric NOT NULL DEFAULT 1000,
|
||||||
|
analysis_horizon_years numeric NOT NULL DEFAULT 3,
|
||||||
|
automation_investment numeric NOT NULL DEFAULT 0,
|
||||||
|
group_id uuid REFERENCES public.process_groups(id) ON DELETE SET NULL,
|
||||||
|
tags text[] NOT NULL DEFAULT '{}',
|
||||||
|
owner_id uuid NOT NULL REFERENCES public.users(id),
|
||||||
|
created_by uuid NOT NULL REFERENCES public.users(id),
|
||||||
|
updated_by uuid NOT NULL REFERENCES public.users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.processes ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- En Sprint 4 todos los usuarios son platform_admin: ven todo.
|
||||||
|
-- Las políticas granulares (process_access) se agregan en Etapa 5.
|
||||||
|
CREATE POLICY "authenticated_all_processes" ON public.processes
|
||||||
|
FOR ALL USING (auth.uid() IS NOT NULL)
|
||||||
|
WITH CHECK (auth.uid() IS NOT NULL);
|
||||||
15
supabase/migrations/004_create_app_settings.sql
Normal file
15
supabase/migrations/004_create_app_settings.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS public.app_settings (
|
||||||
|
id text PRIMARY KEY DEFAULT 'singleton',
|
||||||
|
group_label text NOT NULL DEFAULT 'Cliente',
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.app_settings ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY "all_app_settings" ON public.app_settings
|
||||||
|
FOR ALL USING (auth.uid() IS NOT NULL)
|
||||||
|
WITH CHECK (auth.uid() IS NOT NULL);
|
||||||
|
|
||||||
|
INSERT INTO public.app_settings (id, group_label)
|
||||||
|
VALUES ('singleton', 'Cliente')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
Reference in New Issue
Block a user