diff --git a/src/domain/types.ts b/src/domain/types.ts
index 927c270..ca47ad5 100644
--- a/src/domain/types.ts
+++ b/src/domain/types.ts
@@ -18,6 +18,7 @@ export interface Process {
updatedAt: number
groupId: UUID | null // π Sprint 3 β null = proceso sin grupo asignado ("Sin clasificar")
tags: string[] // π Sprint 3 β tags libres para clasificaciΓ³n y bΓΊsqueda
+ ownerId: string // π Sprint 4 β owner del proceso (auth.uid() al crear)
}
export type ActivityType = 'task' | 'subprocess'
diff --git a/src/features/import/ImportPage.tsx b/src/features/import/ImportPage.tsx
index d7493e1..0f7f374 100644
--- a/src/features/import/ImportPage.tsx
+++ b/src/features/import/ImportPage.tsx
@@ -80,6 +80,7 @@ export function ImportPage() {
updatedAt: now,
groupId: targetGroupId,
tags: [],
+ ownerId: user!.id,
}
const activityElements = extractActivityElements(graph)
diff --git a/src/features/library/LibraryPage.tsx b/src/features/library/LibraryPage.tsx
index b713a5f..3680a43 100644
--- a/src/features/library/LibraryPage.tsx
+++ b/src/features/library/LibraryPage.tsx
@@ -3,10 +3,11 @@ import { useNavigate } from '@tanstack/react-router'
import {
Upload, Search, Plus, Building2, FolderX, GitBranch,
Clock, TrendingUp, BarChart2, FileX2, X, Settings,
- MoreVertical, Trash2,
+ MoreVertical, Trash2, Share2, Lock, User,
} from 'lucide-react'
import { useLibraryStore } from '@/store/library-store'
import { useAuth } from '@/auth/AuthContext'
+import { supabaseProcessRepo } from '@/persistence/supabase/process-repo'
import { formatCurrency } from '@/lib/format'
import type { Process, ProcessGroup, Simulation } from '@/domain/types'
import { AppHeader } from '@/components/AppHeader'
@@ -43,6 +44,48 @@ function matchesTags(p: Process, activeTags: string[]): boolean {
return activeTags.every((t) => p.tags.includes(t))
}
+// βββ Ownership Badge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function OwnershipBadge({ process, currentUserId }: { process: Process; currentUserId: string }) {
+ const isOwn = process.ownerId === currentUserId
+ return (
+
+ {isOwn
+ ?
+ :
+ }
+
+ )
+}
+
+// βββ Toast ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function useToast() {
+ const [message, setMessage] = useState(null)
+
+ function show(msg: string) {
+ setMessage(msg)
+ setTimeout(() => setMessage(null), 2800)
+ }
+
+ return { message, show }
+}
+
+function Toast({ message }: { message: string | null }) {
+ if (!message) return null
+ return (
+
+ {message}
+
+ )
+}
+
// βββ KPI Badge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function KpiBadge({ process, simulation }: { process: Process; simulation: Simulation | null | undefined }) {
@@ -80,15 +123,18 @@ function KpiBadge({ process, simulation }: { process: Process; simulation: Simul
// βββ Process Card βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-function ProcessCard({ process, simulation, showGroupChip, groupName, onDelete }: {
+function ProcessCard({ process, simulation, showGroupChip, groupName, onDelete, currentUserId, onShared }: {
process: Process
simulation: Simulation | null | undefined
showGroupChip?: boolean
groupName?: string
onDelete?: () => void
+ currentUserId: string
+ onShared?: () => void
}) {
const navigate = useNavigate()
const [menuState, setMenuState] = useState(null)
+ const isOwn = process.ownerId === currentUserId
// Cierra el menΓΊ al hacer click fuera de Γ©l
useEffect(() => {
@@ -114,7 +160,10 @@ function ProcessCard({ process, simulation, showGroupChip, groupName, onDelete }
{/* Info central */}
-
{process.name || 'Sin nombre'}
+
+
{process.name || 'Sin nombre'}
+
+
Modificado {relativeDate(process.updatedAt)}
@@ -156,9 +205,23 @@ function ProcessCard({ process, simulation, showGroupChip, groupName, onDelete }
{menuState === 'menu' && (
e.stopPropagation()}
>
+ {isOwn && (
+
+ )}
+ {/* Toggle Todos / MΓos */}
+
+
+
+
+
+
+
)
}
diff --git a/src/persistence/supabase/process-repo.ts b/src/persistence/supabase/process-repo.ts
index 0534aee..ecdc5d4 100644
--- a/src/persistence/supabase/process-repo.ts
+++ b/src/persistence/supabase/process-repo.ts
@@ -14,7 +14,8 @@ function toRow(process: Process, userId: string) {
automation_investment: process.automationInvestment,
group_id: process.groupId,
tags: process.tags,
- owner_id: userId,
+ // owner_id se preserva del valor existente en el proceso; solo se inicializa con userId en creates
+ owner_id: process.ownerId || userId,
created_by: userId,
updated_by: userId,
updated_at: new Date().toISOString(),
@@ -36,6 +37,7 @@ function fromRow(row: Record): Process {
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(),
+ ownerId: (row.owner_id as string) ?? '',
}
}
@@ -69,4 +71,10 @@ export const supabaseProcessRepo = {
const { error } = await supabase.from('processes').delete().eq('id', id)
if (error) throw error
},
+
+ // No-op en Sprint 4: la implementaciΓ³n real (insertar en process_access) llega cuando existan guests.
+ // El mΓ©todo existe para que la UI de "Compartir con equipo" funcione sin escritura a DB.
+ async setShared(_processId: string, _shared: boolean): Promise {
+ return
+ },
}
diff --git a/supabase/migrations/010_create_user_groups.sql b/supabase/migrations/010_create_user_groups.sql
new file mode 100644
index 0000000..5f7b8b7
--- /dev/null
+++ b/supabase/migrations/010_create_user_groups.sql
@@ -0,0 +1,38 @@
+CREATE TABLE IF NOT EXISTS public.user_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()
+);
+
+ALTER TABLE public.user_groups ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "read_groups" ON public.user_groups
+ FOR SELECT USING (auth.uid() IS NOT NULL);
+
+CREATE POLICY "manage_groups" ON public.user_groups
+ FOR ALL USING (
+ EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND platform_role = 'platform_admin')
+ OR created_by = auth.uid()
+ );
+
+CREATE TABLE IF NOT EXISTS public.group_memberships (
+ user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
+ group_id uuid NOT NULL REFERENCES public.user_groups(id) ON DELETE CASCADE,
+ role text NOT NULL DEFAULT 'member' CHECK (role IN ('owner','member')),
+ PRIMARY KEY (user_id, group_id)
+);
+
+ALTER TABLE public.group_memberships ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "read_memberships" ON public.group_memberships
+ FOR SELECT USING (auth.uid() IS NOT NULL);
+
+CREATE POLICY "manage_memberships" ON public.group_memberships
+ FOR ALL USING (
+ EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND platform_role = 'platform_admin')
+ OR EXISTS (
+ SELECT 1 FROM public.user_groups
+ WHERE id = group_memberships.group_id AND created_by = auth.uid()
+ )
+ );
diff --git a/supabase/migrations/011_create_process_access.sql b/supabase/migrations/011_create_process_access.sql
new file mode 100644
index 0000000..bf96fda
--- /dev/null
+++ b/supabase/migrations/011_create_process_access.sql
@@ -0,0 +1,21 @@
+CREATE TABLE IF NOT EXISTS public.process_access (
+ process_id uuid NOT NULL REFERENCES public.processes(id) ON DELETE CASCADE,
+ grantee_type text NOT NULL CHECK (grantee_type IN ('user','group')),
+ grantee_id uuid NOT NULL,
+ permission text NOT NULL DEFAULT 'view' CHECK (permission IN ('view','edit')),
+ PRIMARY KEY (process_id, grantee_type, grantee_id)
+);
+
+ALTER TABLE public.process_access ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "read_process_access" ON public.process_access
+ FOR SELECT USING (auth.uid() IS NOT NULL);
+
+CREATE POLICY "manage_process_access" ON public.process_access
+ FOR ALL USING (
+ EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND platform_role = 'platform_admin')
+ OR EXISTS (
+ SELECT 1 FROM public.processes
+ WHERE id = process_access.process_id AND owner_id = auth.uid()
+ )
+ );
diff --git a/supabase/migrations/012_update_processes_rls.sql b/supabase/migrations/012_update_processes_rls.sql
new file mode 100644
index 0000000..f07ce79
--- /dev/null
+++ b/supabase/migrations/012_update_processes_rls.sql
@@ -0,0 +1,53 @@
+-- Helper: retorna true si el usuario actual es platform_admin
+CREATE OR REPLACE FUNCTION public.is_platform_admin()
+RETURNS boolean
+LANGUAGE sql SECURITY DEFINER
+AS $$
+ SELECT EXISTS (
+ SELECT 1 FROM public.users
+ WHERE id = auth.uid() AND platform_role = 'platform_admin'
+ );
+$$;
+
+-- Eliminar polΓtica amplia de Sprint 4 Etapa 2
+DROP POLICY IF EXISTS "authenticated_all_processes" ON public.processes;
+
+-- PolΓticas granulares
+CREATE POLICY "select_processes" ON public.processes
+ FOR SELECT USING (
+ public.is_platform_admin()
+ OR owner_id = auth.uid()
+ OR EXISTS (
+ SELECT 1 FROM public.process_access
+ WHERE process_id = processes.id
+ AND (
+ (grantee_type = 'user' AND grantee_id = auth.uid())
+ OR (grantee_type = 'group' AND grantee_id IN (
+ SELECT group_id FROM public.group_memberships WHERE user_id = auth.uid()
+ ))
+ )
+ )
+ );
+
+CREATE POLICY "insert_processes" ON public.processes
+ FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
+
+CREATE POLICY "update_processes" ON public.processes
+ FOR UPDATE USING (
+ public.is_platform_admin()
+ OR owner_id = auth.uid()
+ OR EXISTS (
+ SELECT 1 FROM public.process_access
+ WHERE process_id = processes.id
+ AND permission = 'edit'
+ AND (
+ (grantee_type = 'user' AND grantee_id = auth.uid())
+ OR (grantee_type = 'group' AND grantee_id IN (
+ SELECT group_id FROM public.group_memberships WHERE user_id = auth.uid()
+ ))
+ )
+ )
+ );
+
+CREATE POLICY "delete_processes" ON public.processes
+ FOR DELETE USING (public.is_platform_admin() OR owner_id = auth.uid());
diff --git a/tests/features/report/components.test.tsx b/tests/features/report/components.test.tsx
index 79842ab..22304b1 100644
--- a/tests/features/report/components.test.tsx
+++ b/tests/features/report/components.test.tsx
@@ -229,7 +229,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proceso Test', clientName: '',
bpmnXml: '', currency: 'USD',
overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
- createdAt: 0, updatedAt: 0, groupId: null, tags: [],
+ createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user',
}
expect(() => render()).not.toThrow()
})
@@ -239,7 +239,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proc', clientName: '',
bpmnXml: '', currency: 'USD',
overheadPercentage: 0.25, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
- createdAt: 0, updatedAt: 0, groupId: null, tags: [],
+ createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user',
}
render()
const text = document.body.textContent ?? ''
@@ -251,7 +251,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proc', clientName: '',
bpmnXml: '', currency: 'USD',
overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
- createdAt: 0, updatedAt: 0, groupId: null, tags: [],
+ createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user',
}
render()
const text = document.body.textContent ?? ''
@@ -338,6 +338,7 @@ describe('ReportPage β con datos completos', () => {
updatedAt: Date.now() - 1000 * 60 * 30,
groupId: null,
tags: [],
+ ownerId: 'test-user',
}
const mockSimulation = {
diff --git a/tests/features/report/roi-components.test.tsx b/tests/features/report/roi-components.test.tsx
index e24662b..2972f29 100644
--- a/tests/features/report/roi-components.test.tsx
+++ b/tests/features/report/roi-components.test.tsx
@@ -590,7 +590,7 @@ const mockProcessBase = {
currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: Date.now() - 3600_000, updatedAt: Date.now() - 3600_000,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
const mockSimulationBase = {
diff --git a/tests/lib/export/csv-export-roi.test.ts b/tests/lib/export/csv-export-roi.test.ts
index 9998024..b830918 100644
--- a/tests/lib/export/csv-export-roi.test.ts
+++ b/tests/lib/export/csv-export-roi.test.ts
@@ -14,7 +14,7 @@ function makeProcess(extras: Partial = {}): Process {
id: 'p1', name: 'Proceso de CrΓ©dito', clientName: 'Banco XYZ',
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
- createdAt: 0, updatedAt: 0, groupId: null, tags: [], ...extras,
+ createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', ...extras,
}
}
diff --git a/tests/lib/export/csv-export.test.ts b/tests/lib/export/csv-export.test.ts
index ddfbd06..08b2f51 100644
--- a/tests/lib/export/csv-export.test.ts
+++ b/tests/lib/export/csv-export.test.ts
@@ -9,7 +9,7 @@ function makeProcess(currency: string, extras: Partial = {}): Process {
id: 'p1', name: 'Proceso de Ventas', clientName: 'Empresa ABC',
bpmnXml: '', currency, overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
- createdAt: 0, updatedAt: 0, groupId: null, tags: [], ...extras,
+ createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', ...extras,
}
}
diff --git a/tests/lib/export/measure-sizes.test.ts b/tests/lib/export/measure-sizes.test.ts
index 89a1855..b86061c 100644
--- a/tests/lib/export/measure-sizes.test.ts
+++ b/tests/lib/export/measure-sizes.test.ts
@@ -66,7 +66,7 @@ describe('MediciΓ³n de tamaΓ±os de CSV β 3 sample BPMNs', () => {
bpmnXml: xml, currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
const simulation: Simulation = {
diff --git a/tests/lib/export/pdf-export-roi.test.ts b/tests/lib/export/pdf-export-roi.test.ts
index e262ada..838c175 100644
--- a/tests/lib/export/pdf-export-roi.test.ts
+++ b/tests/lib/export/pdf-export-roi.test.ts
@@ -51,7 +51,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: 0, updatedAt: 0,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
const perActivityActual = [
diff --git a/tests/lib/export/pdf-export-sprint2.test.ts b/tests/lib/export/pdf-export-sprint2.test.ts
index 7055155..2d93d39 100644
--- a/tests/lib/export/pdf-export-sprint2.test.ts
+++ b/tests/lib/export/pdf-export-sprint2.test.ts
@@ -43,7 +43,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
// SimulaciΓ³n SIN recursos en ninguna actividad
diff --git a/tests/lib/export/pdf-export.test.ts b/tests/lib/export/pdf-export.test.ts
index a42523f..4e7eaf5 100644
--- a/tests/lib/export/pdf-export.test.ts
+++ b/tests/lib/export/pdf-export.test.ts
@@ -54,7 +54,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
const mockSimulation: Simulation = {
diff --git a/tests/lib/export/pdf-orientation.test.ts b/tests/lib/export/pdf-orientation.test.ts
index 59cc7bb..68321c6 100644
--- a/tests/lib/export/pdf-orientation.test.ts
+++ b/tests/lib/export/pdf-orientation.test.ts
@@ -59,7 +59,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 50000,
createdAt: 0, updatedAt: 0,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
const mockSimulation: Simulation = {
diff --git a/tests/lib/export/pdf-roi-page1.test.ts b/tests/lib/export/pdf-roi-page1.test.ts
index c2a838e..9aa0665 100644
--- a/tests/lib/export/pdf-roi-page1.test.ts
+++ b/tests/lib/export/pdf-roi-page1.test.ts
@@ -56,7 +56,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
const mockSimulation: Simulation = {
diff --git a/tests/lib/export/pdf-roi-pages2-5.test.ts b/tests/lib/export/pdf-roi-pages2-5.test.ts
index 448dcb4..47e0855 100644
--- a/tests/lib/export/pdf-roi-pages2-5.test.ts
+++ b/tests/lib/export/pdf-roi-pages2-5.test.ts
@@ -52,7 +52,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
const mockRoi: RoiResult = {
diff --git a/tests/lib/export/pdf-scenario-page1.test.ts b/tests/lib/export/pdf-scenario-page1.test.ts
index 20cba98..003befc 100644
--- a/tests/lib/export/pdf-scenario-page1.test.ts
+++ b/tests/lib/export/pdf-scenario-page1.test.ts
@@ -52,7 +52,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0,
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
const mockSimulation: Simulation = {
diff --git a/tests/store/simulation-store-invalidation.test.ts b/tests/store/simulation-store-invalidation.test.ts
index f7b3553..c778183 100644
--- a/tests/store/simulation-store-invalidation.test.ts
+++ b/tests/store/simulation-store-invalidation.test.ts
@@ -94,7 +94,7 @@ describe('simulation store β invalidaciΓ³n por cambio en process store', () =>
currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 500, analysisHorizonYears: 2, automationInvestment: 10_000,
createdAt: Date.now(), updatedAt: Date.now(),
- groupId: null, tags: [],
+ groupId: null, tags: [], ownerId: 'test-user',
}
useProcessStore.setState({ currentProcess: proc })