fix: Actualizar con branding de GLM y restringir acceso con Supabase.
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 436 B |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -1,13 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
<defs>
|
<rect width="512" height="512" fill="transparent"/>
|
||||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
<text x="256" y="310" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="142" font-weight="700" fill="#6B8294">G<tspan fill="#61B72A">L</tspan>M</text>
|
||||||
<stop offset="0%" stop-color="#3b82f6" />
|
|
||||||
<stop offset="100%" stop-color="#8b5cf6" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="100" height="100" rx="24" fill="url(#grad)" />
|
|
||||||
<rect x="25" y="25" width="20" height="20" rx="4" fill="#ffffff" />
|
|
||||||
<rect x="55" y="25" width="20" height="20" rx="4" fill="#ffffff" opacity="0.6" />
|
|
||||||
<rect x="25" y="55" width="20" height="20" rx="4" fill="#ffffff" opacity="0.6" />
|
|
||||||
<rect x="55" y="55" width="20" height="20" rx="4" fill="#ffffff" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 302 B |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
@@ -3,12 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tablero CDC</title>
|
<title>Tablero CDC | GLM</title>
|
||||||
<meta name="description" content="Herramienta interna del Departamento de Diseño Creativo." />
|
<meta name="description" content="Herramienta interna de GomezLee Marketing para gestión de proyectos CDC." />
|
||||||
<meta name="author" content="CDC" />
|
<meta name="author" content="GomezLee Marketing" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/tablero-cdc/favicon.svg" />
|
<link rel="icon" type="image/png" href="/tablero-cdc/glm-favicon.png?v=glm-logo-final-20260626" />
|
||||||
<script type="module" crossorigin src="/tablero-cdc/assets/index-BrJEDdGr.js"></script>
|
<link rel="shortcut icon" type="image/png" href="/tablero-cdc/glm-favicon.png?v=glm-logo-final-20260626" />
|
||||||
<link rel="stylesheet" crossorigin href="/tablero-cdc/assets/index-CF6PF3ek.css">
|
<link rel="apple-touch-icon" href="/tablero-cdc/apple-touch-icon.png?v=glm-logo-final-20260626" />
|
||||||
|
<script type="module" crossorigin src="/tablero-cdc/assets/index-CYpFBqnj.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/tablero-cdc/assets/index-DrD5IvZG.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tablero CDC</title>
|
<title>Tablero CDC | GLM</title>
|
||||||
<meta name="description" content="Herramienta interna del Departamento de Diseño Creativo." />
|
<meta name="description" content="Herramienta interna de GomezLee Marketing para gestión de proyectos CDC." />
|
||||||
<meta name="author" content="CDC" />
|
<meta name="author" content="GomezLee Marketing" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/png" href="%BASE_URL%glm-favicon.png?v=glm-logo-final-20260626" />
|
||||||
|
<link rel="shortcut icon" type="image/png" href="%BASE_URL%glm-favicon.png?v=glm-logo-final-20260626" />
|
||||||
|
<link rel="apple-touch-icon" href="%BASE_URL%apple-touch-icon.png?v=glm-logo-final-20260626" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 436 B |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -1,13 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
<defs>
|
<rect width="512" height="512" fill="transparent"/>
|
||||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
<text x="256" y="310" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="142" font-weight="700" fill="#6B8294">G<tspan fill="#61B72A">L</tspan>M</text>
|
||||||
<stop offset="0%" stop-color="#3b82f6" />
|
|
||||||
<stop offset="100%" stop-color="#8b5cf6" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="100" height="100" rx="24" fill="url(#grad)" />
|
|
||||||
<rect x="25" y="25" width="20" height="20" rx="4" fill="#ffffff" />
|
|
||||||
<rect x="55" y="25" width="20" height="20" rx="4" fill="#ffffff" opacity="0.6" />
|
|
||||||
<rect x="25" y="55" width="20" height="20" rx="4" fill="#ffffff" opacity="0.6" />
|
|
||||||
<rect x="55" y="55" width="20" height="20" rx="4" fill="#ffffff" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 302 B |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
@@ -2,6 +2,7 @@ import React, { Component, ReactNode } from "react";
|
|||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import { AuthProvider, useAuth } from "@/context/AuthContext";
|
import { AuthProvider, useAuth } from "@/context/AuthContext";
|
||||||
import { LoginScreen } from "@/components/LoginScreen";
|
import { LoginScreen } from "@/components/LoginScreen";
|
||||||
|
import { GLMLogo } from "@/components/GLMLogo";
|
||||||
import BoardPage from "./pages/BoardPage";
|
import BoardPage from "./pages/BoardPage";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
@@ -62,13 +63,8 @@ function AuthGate() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center animate-pulse">
|
<div className="animate-pulse">
|
||||||
<svg className="w-4 h-4 text-primary-foreground" fill="none" viewBox="0 0 24 24">
|
<GLMLogo showWordmark={false} />
|
||||||
<rect x="3" y="3" width="7" height="7" rx="1" fill="currentColor" />
|
|
||||||
<rect x="14" y="3" width="7" height="7" rx="1" fill="currentColor" opacity="0.5" />
|
|
||||||
<rect x="3" y="14" width="7" height="7" rx="1" fill="currentColor" opacity="0.5" />
|
|
||||||
<rect x="14" y="14" width="7" height="7" rx="1" fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">Cargando tablero…</p>
|
<p className="text-sm text-muted-foreground">Cargando tablero…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type GLMLogoProps = {
|
||||||
|
size?: "sm" | "lg";
|
||||||
|
showWordmark?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GLMLogo({ size = "sm", className }: GLMLogoProps) {
|
||||||
|
const imageSize = size === "lg" ? "h-auto w-[168px]" : "h-auto w-[104px]";
|
||||||
|
const logoNudge = size === "lg" ? "-translate-x-1" : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("inline-flex items-center", className)} aria-label="GLM">
|
||||||
|
<img
|
||||||
|
src={`${import.meta.env.BASE_URL}glm-logo.png`}
|
||||||
|
alt="GLM"
|
||||||
|
className={cn(imageSize, logoNudge)}
|
||||||
|
loading="eager"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LayoutGrid } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
|
import { GLMLogo } from "@/components/GLMLogo";
|
||||||
|
|
||||||
export function LoginScreen() {
|
export function LoginScreen() {
|
||||||
const { loginWithGoogle, error, loading } = useAuth();
|
const { loginWithGoogle, error, loading } = useAuth();
|
||||||
@@ -9,13 +9,9 @@ export function LoginScreen() {
|
|||||||
<div className="min-h-screen bg-background flex items-center justify-center px-4">
|
<div className="min-h-screen bg-background flex items-center justify-center px-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm">
|
||||||
{/* Logo / marca */}
|
{/* Logo / marca */}
|
||||||
<div className="flex flex-col items-center mb-8 gap-3">
|
<div className="flex flex-col items-center mb-8 gap-5">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-primary flex items-center justify-center shadow-lg">
|
<GLMLogo size="lg" />
|
||||||
<LayoutGrid className="w-7 h-7 text-primary-foreground" />
|
<h1 className="text-2xl font-bold tracking-tight text-[#4F758B]">Tablero CDC</h1>
|
||||||
</div>
|
|
||||||
<div className="text-center leading-tight">
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Tablero CDC</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card */}
|
{/* Card */}
|
||||||
@@ -43,7 +39,7 @@ export function LoginScreen() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-center text-xs text-muted-foreground mt-6">
|
<p className="text-center text-xs text-muted-foreground mt-6">
|
||||||
Solo acceso para usuarios <span className="font-medium">@gomezleemarketing.com</span>
|
Acceso restringido a usuarios autorizados
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,11 +63,7 @@ export function ProjectPricingPanel({ items, onChange, loading, error }: Props)
|
|||||||
const [manualName, setManualName] = useState<string>("");
|
const [manualName, setManualName] = useState<string>("");
|
||||||
const [manualDescription, setManualDescription] = useState<string>("");
|
const [manualDescription, setManualDescription] = useState<string>("");
|
||||||
const [manualAmount, setManualAmount] = useState<string>("");
|
const [manualAmount, setManualAmount] = useState<string>("");
|
||||||
const {
|
const { catalog: tariffCatalog, loading: tariffLoading, error: tariffError } = useTariffCatalog();
|
||||||
catalog: tariffCatalog,
|
|
||||||
loading: tariffLoading,
|
|
||||||
error: tariffError,
|
|
||||||
} = useTariffCatalog();
|
|
||||||
|
|
||||||
const sectionOptions = TARIFF_SECTIONS.map((option) => option.label);
|
const sectionOptions = TARIFF_SECTIONS.map((option) => option.label);
|
||||||
const selectedSectionLabel = TARIFF_SECTIONS.find((option) => option.id === section)?.label || "";
|
const selectedSectionLabel = TARIFF_SECTIONS.find((option) => option.id === section)?.label || "";
|
||||||
@@ -237,7 +233,6 @@ export function ProjectPricingPanel({ items, onChange, loading, error }: Props)
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{(loading || tariffLoading) && (
|
{(loading || tariffLoading) && (
|
||||||
<div className="mb-3 rounded-xl border border-border bg-card px-3 py-2 text-xs text-muted-foreground">
|
<div className="mb-3 rounded-xl border border-border bg-card px-3 py-2 text-xs text-muted-foreground">
|
||||||
{loading ? "Cargando costos guardados…" : "Cargando tarifario…"}
|
{loading ? "Cargando costos guardados…" : "Cargando tarifario…"}
|
||||||
|
|||||||
@@ -9,19 +9,7 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import type { User as SupabaseUser } from "@supabase/supabase-js";
|
import type { User as SupabaseUser } from "@supabase/supabase-js";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
|
import { getActiveTableroCdcAccess, type TableroCdcUserAccess } from "@/lib/accessControl";
|
||||||
const ALLOWED_DOMAIN = "gomezleemarketing.com";
|
|
||||||
const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com";
|
|
||||||
const JOSE_LEOPOLDO_EMAIL = "jgomez@gomezleemarketing.com";
|
|
||||||
const ISAAC_EMAIL = "iaracena@gomezleemarketing.com";
|
|
||||||
const INTERNAL_PRICING_ALLOWED_EMAILS = [GERARDO_EMAIL, ISAAC_EMAIL];
|
|
||||||
const PRICING_SUMMARY_CONTROLLER_EMAILS = [GERARDO_EMAIL, JOSE_LEOPOLDO_EMAIL, ISAAC_EMAIL];
|
|
||||||
const DELETE_ALLOWED_EMAILS = [GERARDO_EMAIL, "iaracena@gomezleemarketing.com"];
|
|
||||||
|
|
||||||
function isAllowedEmail(email: string | null | undefined): boolean {
|
|
||||||
if (!email) return false;
|
|
||||||
return email.toLowerCase().endsWith(`@${ALLOWED_DOMAIN}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRedirectTo(): string {
|
function getRedirectTo(): string {
|
||||||
if (typeof window === "undefined") return "/tablero-cdc/";
|
if (typeof window === "undefined") return "/tablero-cdc/";
|
||||||
@@ -75,23 +63,44 @@ const AuthContext = createContext<AuthContextValue | null>(null);
|
|||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<AuthUser | null>(null);
|
const [user, setUser] = useState<AuthUser | null>(null);
|
||||||
|
const [access, setAccess] = useState<TableroCdcUserAccess | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const applyUser = useCallback(async (supabaseUser: SupabaseUser | null) => {
|
const applyUser = useCallback(async (supabaseUser: SupabaseUser | null) => {
|
||||||
if (!supabaseUser) {
|
if (!supabaseUser) {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
setAccess(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAllowedEmail(supabaseUser.email)) {
|
let nextAccess: TableroCdcUserAccess | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
nextAccess = await getActiveTableroCdcAccess(supabaseUser.email);
|
||||||
|
} catch (accessError) {
|
||||||
|
console.error("Error al validar acceso al Tablero CDC:", accessError);
|
||||||
await supabase.auth.signOut();
|
await supabase.auth.signOut();
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setError("Solo se permite acceso con correos corporativos de GomezLee Marketing.");
|
setAccess(null);
|
||||||
|
setError(
|
||||||
|
"No se pudo validar tu acceso al Tablero CDC. Verifica que el SQL de accesos esté ejecutado o contacta a IT.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextAccess) {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
setUser(null);
|
||||||
|
setAccess(null);
|
||||||
|
setError(
|
||||||
|
"Tu correo no está autorizado para acceder al Tablero CDC. Solicita acceso a IT o al equipo CDC.",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(normalizeSupabaseUser(supabaseUser));
|
setUser(normalizeSupabaseUser(supabaseUser));
|
||||||
|
setAccess(nextAccess);
|
||||||
setError(null);
|
setError(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -123,8 +132,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
|
const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
void applyUser(session?.user ?? null);
|
setLoading(true);
|
||||||
setLoading(false);
|
void applyUser(session?.user ?? null).finally(() => {
|
||||||
|
if (mounted) setLoading(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -142,7 +153,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
redirectTo: getRedirectTo(),
|
redirectTo: getRedirectTo(),
|
||||||
queryParams: {
|
queryParams: {
|
||||||
prompt: "select_account",
|
prompt: "select_account",
|
||||||
hd: ALLOWED_DOMAIN,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -163,22 +173,22 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
setAccess(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const normalizedEmail = user?.email?.toLowerCase() || "";
|
const isGerardo = useMemo(() => access?.role === "director_creativo", [access?.role]);
|
||||||
const isGerardo = useMemo(() => normalizedEmail === GERARDO_EMAIL, [normalizedEmail]);
|
|
||||||
const canManageInternalPricing = useMemo(
|
const canManageInternalPricing = useMemo(
|
||||||
() => INTERNAL_PRICING_ALLOWED_EMAILS.includes(normalizedEmail),
|
() => access?.canManageInternalPricing === true,
|
||||||
[normalizedEmail],
|
[access?.canManageInternalPricing],
|
||||||
);
|
);
|
||||||
const canDeleteProjects = useMemo(
|
const canDeleteProjects = useMemo(
|
||||||
() => DELETE_ALLOWED_EMAILS.includes(normalizedEmail),
|
() => access?.canDeleteProjects === true,
|
||||||
[normalizedEmail],
|
[access?.canDeleteProjects],
|
||||||
);
|
);
|
||||||
const canControlPricingSummary = useMemo(
|
const canControlPricingSummary = useMemo(
|
||||||
() => PRICING_SUMMARY_CONTROLLER_EMAILS.includes(normalizedEmail),
|
() => access?.canControlPricingSummary === true,
|
||||||
[normalizedEmail],
|
[access?.canControlPricingSummary],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { supabase } from "@/lib/supabase";
|
||||||
|
|
||||||
|
export type TableroCdcUserAccess = {
|
||||||
|
email: string;
|
||||||
|
fullName: string | null;
|
||||||
|
role: string;
|
||||||
|
isActive: boolean;
|
||||||
|
canDeleteProjects: boolean;
|
||||||
|
canManageInternalPricing: boolean;
|
||||||
|
canControlPricingSummary: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AccessRow = {
|
||||||
|
email?: string | null;
|
||||||
|
full_name?: string | null;
|
||||||
|
role?: string | null;
|
||||||
|
is_active?: boolean | null;
|
||||||
|
can_delete_projects?: boolean | null;
|
||||||
|
can_manage_internal_pricing?: boolean | null;
|
||||||
|
can_control_pricing_summary?: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeEmail(email: string | null | undefined): string {
|
||||||
|
return String(email ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAccessRow(row: AccessRow | null | undefined): TableroCdcUserAccess | null {
|
||||||
|
if (!row?.email || row.is_active !== true) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
email: normalizeEmail(row.email),
|
||||||
|
fullName: row.full_name ?? null,
|
||||||
|
role: String(row.role || "user").trim() || "user",
|
||||||
|
isActive: true,
|
||||||
|
canDeleteProjects: row.can_delete_projects === true,
|
||||||
|
canManageInternalPricing: row.can_manage_internal_pricing === true,
|
||||||
|
canControlPricingSummary: row.can_control_pricing_summary === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveTableroCdcAccess(
|
||||||
|
email: string | null | undefined,
|
||||||
|
): Promise<TableroCdcUserAccess | null> {
|
||||||
|
const normalizedEmail = normalizeEmail(email);
|
||||||
|
|
||||||
|
if (!normalizedEmail) return null;
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("tablero_cdc_allowed_users")
|
||||||
|
.select(
|
||||||
|
"email, full_name, role, is_active, can_delete_projects, can_manage_internal_pricing, can_control_pricing_summary",
|
||||||
|
)
|
||||||
|
.eq("email", normalizedEmail)
|
||||||
|
.eq("is_active", true)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeAccessRow(data as AccessRow | null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentTableroCdcAccess(): Promise<TableroCdcUserAccess> {
|
||||||
|
const { data, error } = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (error || !data.user) {
|
||||||
|
throw new Error("No hay una sesión activa de Supabase.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const access = await getActiveTableroCdcAccess(data.user.email);
|
||||||
|
|
||||||
|
if (!access) {
|
||||||
|
throw new Error("Tu correo no está autorizado para acceder al Tablero CDC.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return access;
|
||||||
|
}
|
||||||
@@ -53,9 +53,7 @@ export function usePricingSummaryVisibility(enabled = true) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("No se pudo leer la visibilidad del total tarifado:", err);
|
console.error("No se pudo leer la visibilidad del total tarifado:", err);
|
||||||
setError(
|
setError(
|
||||||
err instanceof Error
|
err instanceof Error ? err.message : "No se pudo leer la visibilidad del total tarifado.",
|
||||||
? err.message
|
|
||||||
: "No se pudo leer la visibilidad del total tarifado.",
|
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -63,29 +61,26 @@ export function usePricingSummaryVisibility(enabled = true) {
|
|||||||
}
|
}
|
||||||
}, [enabled]);
|
}, [enabled]);
|
||||||
|
|
||||||
const setPublicVisibility = useCallback(
|
const setPublicVisibility = useCallback(async (nextIsPublic: boolean) => {
|
||||||
async (nextIsPublic: boolean) => {
|
setSaving(true);
|
||||||
setSaving(true);
|
setError(null);
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await savePricingSummaryPublicVisibility(nextIsPublic);
|
await savePricingSummaryPublicVisibility(nextIsPublic);
|
||||||
setIsPublic(nextIsPublic);
|
setIsPublic(nextIsPublic);
|
||||||
return nextIsPublic;
|
return nextIsPublic;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("No se pudo actualizar la visibilidad del total tarifado:", err);
|
console.error("No se pudo actualizar la visibilidad del total tarifado:", err);
|
||||||
setError(
|
setError(
|
||||||
err instanceof Error
|
err instanceof Error
|
||||||
? err.message
|
? err.message
|
||||||
: "No se pudo actualizar la visibilidad del total tarifado.",
|
: "No se pudo actualizar la visibilidad del total tarifado.",
|
||||||
);
|
);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh();
|
void refresh();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
|
import { getCurrentTableroCdcAccess } from "@/lib/accessControl";
|
||||||
import type { ColorId, Status } from "@/data/lists";
|
import type { ColorId, Status } from "@/data/lists";
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
@@ -178,9 +179,6 @@ type DirectProjectSummaryRow = {
|
|||||||
|
|
||||||
const DEFAULT_COLOR: ColorId = "blue";
|
const DEFAULT_COLOR: ColorId = "blue";
|
||||||
const OPEN_STATUS = "Activo";
|
const OPEN_STATUS = "Activo";
|
||||||
const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com";
|
|
||||||
const INTERNAL_PRICING_ALLOWED_EMAILS = [GERARDO_EMAIL, "iaracena@gomezleemarketing.com"];
|
|
||||||
const DELETE_ALLOWED_EMAILS = [GERARDO_EMAIL, "iaracena@gomezleemarketing.com"];
|
|
||||||
const DEFAULT_PROJECT_PARAMS: ProjectQueryParams = {
|
const DEFAULT_PROJECT_PARAMS: ProjectQueryParams = {
|
||||||
tab: "todos",
|
tab: "todos",
|
||||||
search: "",
|
search: "",
|
||||||
@@ -478,18 +476,18 @@ async function getCurrentUserId(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function currentUserIsGerardo(): Promise<boolean> {
|
async function currentUserIsGerardo(): Promise<boolean> {
|
||||||
const user = await getCurrentUser();
|
const access = await getCurrentTableroCdcAccess();
|
||||||
return user.email?.toLowerCase() === GERARDO_EMAIL;
|
return access.role === "director_creativo";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function currentUserCanManageInternalPricing(): Promise<boolean> {
|
async function currentUserCanManageInternalPricing(): Promise<boolean> {
|
||||||
const user = await getCurrentUser();
|
const access = await getCurrentTableroCdcAccess();
|
||||||
return INTERNAL_PRICING_ALLOWED_EMAILS.includes(user.email?.toLowerCase() || "");
|
return access.canManageInternalPricing;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function currentUserCanDeleteProjects(): Promise<boolean> {
|
async function currentUserCanDeleteProjects(): Promise<boolean> {
|
||||||
const user = await getCurrentUser();
|
const access = await getCurrentTableroCdcAccess();
|
||||||
return DELETE_ALLOWED_EMAILS.includes(user.email?.toLowerCase() || "");
|
return access.canDeleteProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toProjectRow(project: Omit<Project, "id" | "createdAt"> | Partial<Project>) {
|
function toProjectRow(project: Omit<Project, "id" | "createdAt"> | Partial<Project>) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { usePricingSummaryVisibility } from "@/lib/pricingSummaryVisibility";
|
|||||||
import { useAppLists } from "@/lib/appLists";
|
import { useAppLists } from "@/lib/appLists";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
import { dedupeOptions } from "@/lib/optionUtils";
|
import { dedupeOptions } from "@/lib/optionUtils";
|
||||||
|
import { GLMLogo } from "@/components/GLMLogo";
|
||||||
|
|
||||||
/** Extract up-to-2-letter initials from a display name or email */
|
/** Extract up-to-2-letter initials from a display name or email */
|
||||||
const PROJECTS_PER_PAGE = 24;
|
const PROJECTS_PER_PAGE = 24;
|
||||||
@@ -221,14 +222,10 @@ export default function BoardPage() {
|
|||||||
<header className="sticky top-0 z-30 bg-background/85 backdrop-blur-md border-b border-border">
|
<header className="sticky top-0 z-30 bg-background/85 backdrop-blur-md border-b border-border">
|
||||||
<div className="max-w-[1400px] mx-auto px-6 py-3.5 flex items-center gap-4">
|
<div className="max-w-[1400px] mx-auto px-6 py-3.5 flex items-center gap-4">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center">
|
<GLMLogo size="sm" />
|
||||||
<LayoutGrid className="w-4 h-4 text-primary-foreground" />
|
<div className="h-7 w-px bg-border" />
|
||||||
</div>
|
<h1 className="text-[15px] font-semibold tracking-tight text-[#4F758B]">Tablero CDC</h1>
|
||||||
<div className="leading-tight">
|
|
||||||
<h1 className="text-[15px] font-semibold tracking-tight">Tablero CDC</h1>
|
|
||||||
<p className="text-[11px] text-muted-foreground">Flujo de Proyectos</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
|
|||||||
@@ -64,48 +64,57 @@
|
|||||||
:root {
|
:root {
|
||||||
--radius: 0.875rem;
|
--radius: 0.875rem;
|
||||||
|
|
||||||
/* Google-like clean palette + soft purple accent */
|
/* GLM palette — soft, clean and low-noise for the existing CDC UI */
|
||||||
--background: oklch(0.99 0.003 280);
|
--glm-blue: #4f758b;
|
||||||
--foreground: oklch(0.25 0.02 280);
|
--glm-blue-medium: #5b7f95;
|
||||||
|
--glm-steel: #6b8fa3;
|
||||||
|
--glm-teal: #2c6e6f;
|
||||||
|
--glm-green: #6cc24a;
|
||||||
|
--glm-green-soft: #eef6e8;
|
||||||
|
|
||||||
|
--background: oklch(0.99 0.004 131);
|
||||||
|
--foreground: oklch(0.4091 0 89.9);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.25 0.02 280);
|
--card-foreground: oklch(0.4091 0 89.9);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.25 0.02 280);
|
--popover-foreground: oklch(0.4091 0 89.9);
|
||||||
|
|
||||||
--primary: oklch(0.55 0.18 290);
|
--primary: oklch(0.5427 0.055 234.6);
|
||||||
--primary-foreground: oklch(0.99 0.005 280);
|
--primary-foreground: oklch(1 0 89.9);
|
||||||
|
|
||||||
--secondary: oklch(0.97 0.005 280);
|
--secondary: oklch(0.9702 0 89.9);
|
||||||
--secondary-foreground: oklch(0.3 0.02 280);
|
--secondary-foreground: oklch(0.4091 0 89.9);
|
||||||
--muted: oklch(0.965 0.004 280);
|
--muted: oklch(0.9702 0 89.9);
|
||||||
--muted-foreground: oklch(0.5 0.015 280);
|
--muted-foreground: oklch(0.6304 0.0501 232.9);
|
||||||
--accent: oklch(0.96 0.02 290);
|
--accent: oklch(0.9633 0.0203 131.5);
|
||||||
--accent-foreground: oklch(0.35 0.05 290);
|
--accent-foreground: oklch(0.4968 0.0663 196.6);
|
||||||
|
|
||||||
--destructive: oklch(0.6 0.22 27);
|
--destructive: oklch(0.6 0.22 27);
|
||||||
--destructive-foreground: oklch(0.99 0 0);
|
--destructive-foreground: oklch(0.99 0 0);
|
||||||
|
|
||||||
--border: oklch(0.925 0.005 280);
|
--border: oklch(0.9 0.01 235);
|
||||||
--input: oklch(0.93 0.005 280);
|
--input: oklch(0.91 0.01 235);
|
||||||
--ring: oklch(0.65 0.15 290);
|
--ring: oklch(0.7344 0.1772 137.9);
|
||||||
|
|
||||||
--chart-1: oklch(0.65 0.18 250);
|
--chart-1: oklch(0.5427 0.055 234.6);
|
||||||
--chart-2: oklch(0.65 0.18 150);
|
--chart-2: oklch(0.7344 0.1772 137.9);
|
||||||
--chart-3: oklch(0.7 0.16 80);
|
--chart-3: oklch(0.5778 0.0529 235.7);
|
||||||
--chart-4: oklch(0.6 0.2 20);
|
--chart-4: oklch(0.7013 0.1996 43.7);
|
||||||
--chart-5: oklch(0.55 0.18 290);
|
--chart-5: oklch(0.787 0.1232 161.2);
|
||||||
|
|
||||||
--sidebar: oklch(0.985 0.003 280);
|
--sidebar: oklch(0.985 0.004 131);
|
||||||
--sidebar-foreground: oklch(0.25 0.02 280);
|
--sidebar-foreground: oklch(0.4091 0 89.9);
|
||||||
--sidebar-primary: oklch(0.55 0.18 290);
|
--sidebar-primary: oklch(0.5427 0.055 234.6);
|
||||||
--sidebar-primary-foreground: oklch(0.99 0 0);
|
--sidebar-primary-foreground: oklch(1 0 89.9);
|
||||||
--sidebar-accent: oklch(0.96 0.02 290);
|
--sidebar-accent: oklch(0.9633 0.0203 131.5);
|
||||||
--sidebar-accent-foreground: oklch(0.35 0.05 290);
|
--sidebar-accent-foreground: oklch(0.4968 0.0663 196.6);
|
||||||
--sidebar-border: oklch(0.925 0.005 280);
|
--sidebar-border: oklch(0.9 0.01 235);
|
||||||
--sidebar-ring: oklch(0.65 0.15 290);
|
--sidebar-ring: oklch(0.7344 0.1772 137.9);
|
||||||
|
|
||||||
--shadow-soft: 0 1px 2px oklch(0.5 0.02 280 / 0.06), 0 4px 12px oklch(0.5 0.02 280 / 0.05);
|
--shadow-soft:
|
||||||
--shadow-hover: 0 4px 8px oklch(0.5 0.02 280 / 0.08), 0 12px 28px oklch(0.5 0.02 280 / 0.10);
|
0 1px 2px oklch(0.5427 0.055 234.6 / 0.06), 0 4px 14px oklch(0.5427 0.055 234.6 / 0.06);
|
||||||
|
--shadow-hover:
|
||||||
|
0 4px 10px oklch(0.5427 0.055 234.6 / 0.1), 0 14px 30px oklch(0.5427 0.055 234.6 / 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|||||||
@@ -1,14 +1,243 @@
|
|||||||
-- =========================================================
|
-- =========================================================
|
||||||
-- TABLERO CDC - Visibilidad del Total Tarifado
|
-- TABLERO CDC - Acceso autorizado por Supabase
|
||||||
-- Ejecutar en Supabase SQL Editor antes de desplegar esta versión.
|
-- Ejecutar en Supabase SQL Editor ANTES de desplegar el ZIP.
|
||||||
--
|
--
|
||||||
-- Objetivo:
|
-- Objetivo:
|
||||||
-- - Por defecto, el Total Tarifado solo lo ven:
|
-- - El acceso a la app ya no depende de una lista fija en código.
|
||||||
-- * gmarrero@gomezleemarketing.com
|
-- - Los usuarios permitidos viven en public.tablero_cdc_allowed_users.
|
||||||
-- * jgomez@gomezleemarketing.com
|
-- - Para agregar/quitar acceso luego, se actualiza esta tabla en Supabase.
|
||||||
-- * iaracena@gomezleemarketing.com
|
-- - También centraliza permisos especiales usados por la app:
|
||||||
-- - Ellos pueden activar/desactivar un botón para mostrarlo temporalmente a todos.
|
-- * can_manage_internal_pricing
|
||||||
-- - No expone montos internos por proyecto; solo habilita/oculta el resumen global/filtrado.
|
-- * can_delete_projects
|
||||||
|
-- * can_control_pricing_summary
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create table if not exists public.tablero_cdc_allowed_users (
|
||||||
|
email text primary key,
|
||||||
|
full_name text,
|
||||||
|
role text not null default 'user',
|
||||||
|
is_active boolean not null default true,
|
||||||
|
can_delete_projects boolean not null default false,
|
||||||
|
can_manage_internal_pricing boolean not null default false,
|
||||||
|
can_control_pricing_summary boolean not null default false,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
comment on table public.tablero_cdc_allowed_users is
|
||||||
|
'Lista central de usuarios autorizados para entrar al Tablero CDC.';
|
||||||
|
|
||||||
|
comment on column public.tablero_cdc_allowed_users.role is
|
||||||
|
'Rol lógico de la app. director_creativo habilita comportamiento especial de Director Creativo.';
|
||||||
|
|
||||||
|
create or replace function public.set_tablero_cdc_allowed_users_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.email = lower(trim(new.email));
|
||||||
|
new.updated_at = now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_tablero_cdc_allowed_users_updated_at on public.tablero_cdc_allowed_users;
|
||||||
|
|
||||||
|
create trigger trg_tablero_cdc_allowed_users_updated_at
|
||||||
|
before insert or update on public.tablero_cdc_allowed_users
|
||||||
|
for each row
|
||||||
|
execute function public.set_tablero_cdc_allowed_users_updated_at();
|
||||||
|
|
||||||
|
-- Usuarios autorizados iniciales.
|
||||||
|
-- Nota: Re-ejecutar este SQL mantiene estos usuarios activos y actualiza sus permisos base.
|
||||||
|
insert into public.tablero_cdc_allowed_users (
|
||||||
|
email,
|
||||||
|
full_name,
|
||||||
|
role,
|
||||||
|
is_active,
|
||||||
|
can_delete_projects,
|
||||||
|
can_manage_internal_pricing,
|
||||||
|
can_control_pricing_summary
|
||||||
|
)
|
||||||
|
values
|
||||||
|
(
|
||||||
|
'iaracena@gomezleemarketing.com',
|
||||||
|
'Isaac Daniel Aracena Toribio',
|
||||||
|
'admin_it',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'jgomez@gomezleemarketing.com',
|
||||||
|
'José Leopoldo Gomez',
|
||||||
|
'leadership',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'gmarrero@gomezleemarketing.com',
|
||||||
|
'Gerardo Marrero',
|
||||||
|
'director_creativo',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'areyes@gomezleemarketing.com',
|
||||||
|
'Alicia Thaía Reyes',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'boliva@gomezleemarketing.com',
|
||||||
|
'Bonnie Oliva',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'obrisceno@gomezleemarketing.com',
|
||||||
|
'Oscar Brisceño',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'lavila01@gomezlee.marketing',
|
||||||
|
'Lady Avila Faura',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'rmarcano@gomezleemarketing.com',
|
||||||
|
'Ramon Marcano',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'lmatos@gomezleemarketing.com',
|
||||||
|
'Luis Matos',
|
||||||
|
'it',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'mgomez@gomezleemarketing.com',
|
||||||
|
'Máximo Gomez',
|
||||||
|
'leadership',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
on conflict (email)
|
||||||
|
do update set
|
||||||
|
full_name = excluded.full_name,
|
||||||
|
role = excluded.role,
|
||||||
|
is_active = excluded.is_active,
|
||||||
|
can_delete_projects = excluded.can_delete_projects,
|
||||||
|
can_manage_internal_pricing = excluded.can_manage_internal_pricing,
|
||||||
|
can_control_pricing_summary = excluded.can_control_pricing_summary,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
alter table public.tablero_cdc_allowed_users enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "tablero_cdc_allowed_users_select_own_active" on public.tablero_cdc_allowed_users;
|
||||||
|
drop policy if exists "tablero_cdc_allowed_users_no_insert_from_app" on public.tablero_cdc_allowed_users;
|
||||||
|
drop policy if exists "tablero_cdc_allowed_users_no_update_from_app" on public.tablero_cdc_allowed_users;
|
||||||
|
drop policy if exists "tablero_cdc_allowed_users_no_delete_from_app" on public.tablero_cdc_allowed_users;
|
||||||
|
|
||||||
|
-- La app solo puede leer la fila activa del usuario autenticado.
|
||||||
|
-- La administración real se hace desde Supabase SQL Editor.
|
||||||
|
create policy "tablero_cdc_allowed_users_select_own_active"
|
||||||
|
on public.tablero_cdc_allowed_users
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
is_active = true
|
||||||
|
and lower(email) = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
create policy "tablero_cdc_allowed_users_no_insert_from_app"
|
||||||
|
on public.tablero_cdc_allowed_users
|
||||||
|
for insert
|
||||||
|
to authenticated
|
||||||
|
with check (false);
|
||||||
|
|
||||||
|
create policy "tablero_cdc_allowed_users_no_update_from_app"
|
||||||
|
on public.tablero_cdc_allowed_users
|
||||||
|
for update
|
||||||
|
to authenticated
|
||||||
|
using (false)
|
||||||
|
with check (false);
|
||||||
|
|
||||||
|
create policy "tablero_cdc_allowed_users_no_delete_from_app"
|
||||||
|
on public.tablero_cdc_allowed_users
|
||||||
|
for delete
|
||||||
|
to authenticated
|
||||||
|
using (false);
|
||||||
|
|
||||||
|
grant select on public.tablero_cdc_allowed_users to authenticated;
|
||||||
|
|
||||||
|
-- Funciones auxiliares para políticas/RPC.
|
||||||
|
create or replace function public.tablero_cdc_current_user_is_allowed()
|
||||||
|
returns boolean
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from public.tablero_cdc_allowed_users u
|
||||||
|
where u.email = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||||
|
and u.is_active = true
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
|
returns boolean
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from public.tablero_cdc_allowed_users u
|
||||||
|
where u.email = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||||
|
and u.is_active = true
|
||||||
|
and u.can_control_pricing_summary = true
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant execute on function public.tablero_cdc_current_user_is_allowed() to authenticated;
|
||||||
|
grant execute on function public.tablero_cdc_current_user_can_control_pricing_summary() to authenticated;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- Total Tarifado: configuración pública/privada usando tabla de accesos
|
||||||
-- =========================================================
|
-- =========================================================
|
||||||
|
|
||||||
create table if not exists public.tablero_cdc_app_settings (
|
create table if not exists public.tablero_cdc_app_settings (
|
||||||
@@ -25,15 +254,16 @@ on conflict (key) do nothing;
|
|||||||
alter table public.tablero_cdc_app_settings enable row level security;
|
alter table public.tablero_cdc_app_settings enable row level security;
|
||||||
|
|
||||||
drop policy if exists "tablero_cdc_app_settings_select_authenticated" on public.tablero_cdc_app_settings;
|
drop policy if exists "tablero_cdc_app_settings_select_authenticated" on public.tablero_cdc_app_settings;
|
||||||
|
drop policy if exists "tablero_cdc_app_settings_select_allowed_users" on public.tablero_cdc_app_settings;
|
||||||
drop policy if exists "tablero_cdc_app_settings_insert_summary_controllers" on public.tablero_cdc_app_settings;
|
drop policy if exists "tablero_cdc_app_settings_insert_summary_controllers" on public.tablero_cdc_app_settings;
|
||||||
drop policy if exists "tablero_cdc_app_settings_update_summary_controllers" on public.tablero_cdc_app_settings;
|
drop policy if exists "tablero_cdc_app_settings_update_summary_controllers" on public.tablero_cdc_app_settings;
|
||||||
drop policy if exists "tablero_cdc_app_settings_delete_none" on public.tablero_cdc_app_settings;
|
drop policy if exists "tablero_cdc_app_settings_delete_none" on public.tablero_cdc_app_settings;
|
||||||
|
|
||||||
create policy "tablero_cdc_app_settings_select_authenticated"
|
create policy "tablero_cdc_app_settings_select_allowed_users"
|
||||||
on public.tablero_cdc_app_settings
|
on public.tablero_cdc_app_settings
|
||||||
for select
|
for select
|
||||||
to authenticated
|
to authenticated
|
||||||
using (true);
|
using (public.tablero_cdc_current_user_is_allowed());
|
||||||
|
|
||||||
create policy "tablero_cdc_app_settings_insert_summary_controllers"
|
create policy "tablero_cdc_app_settings_insert_summary_controllers"
|
||||||
on public.tablero_cdc_app_settings
|
on public.tablero_cdc_app_settings
|
||||||
@@ -41,11 +271,7 @@ create policy "tablero_cdc_app_settings_insert_summary_controllers"
|
|||||||
to authenticated
|
to authenticated
|
||||||
with check (
|
with check (
|
||||||
key = 'pricing_summary_public'
|
key = 'pricing_summary_public'
|
||||||
and lower(coalesce(auth.jwt() ->> 'email', '')) in (
|
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
'gmarrero@gomezleemarketing.com',
|
|
||||||
'jgomez@gomezleemarketing.com',
|
|
||||||
'iaracena@gomezleemarketing.com'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
create policy "tablero_cdc_app_settings_update_summary_controllers"
|
create policy "tablero_cdc_app_settings_update_summary_controllers"
|
||||||
@@ -54,19 +280,11 @@ create policy "tablero_cdc_app_settings_update_summary_controllers"
|
|||||||
to authenticated
|
to authenticated
|
||||||
using (
|
using (
|
||||||
key = 'pricing_summary_public'
|
key = 'pricing_summary_public'
|
||||||
and lower(coalesce(auth.jwt() ->> 'email', '')) in (
|
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
'gmarrero@gomezleemarketing.com',
|
|
||||||
'jgomez@gomezleemarketing.com',
|
|
||||||
'iaracena@gomezleemarketing.com'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
with check (
|
with check (
|
||||||
key = 'pricing_summary_public'
|
key = 'pricing_summary_public'
|
||||||
and lower(coalesce(auth.jwt() ->> 'email', '')) in (
|
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
'gmarrero@gomezleemarketing.com',
|
|
||||||
'jgomez@gomezleemarketing.com',
|
|
||||||
'iaracena@gomezleemarketing.com'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
create policy "tablero_cdc_app_settings_delete_none"
|
create policy "tablero_cdc_app_settings_delete_none"
|
||||||
@@ -93,6 +311,8 @@ before insert or update on public.tablero_cdc_app_settings
|
|||||||
for each row
|
for each row
|
||||||
execute function public.set_tablero_cdc_app_settings_updated_at();
|
execute function public.set_tablero_cdc_app_settings_updated_at();
|
||||||
|
|
||||||
|
grant select on public.tablero_cdc_app_settings to authenticated;
|
||||||
|
|
||||||
create or replace function public.tablero_cdc_set_pricing_summary_public(p_enabled boolean)
|
create or replace function public.tablero_cdc_set_pricing_summary_public(p_enabled boolean)
|
||||||
returns jsonb
|
returns jsonb
|
||||||
language plpgsql
|
language plpgsql
|
||||||
@@ -100,14 +320,9 @@ security definer
|
|||||||
set search_path = public
|
set search_path = public
|
||||||
as $$
|
as $$
|
||||||
declare
|
declare
|
||||||
v_email text := lower(coalesce(auth.jwt() ->> 'email', ''));
|
|
||||||
v_enabled boolean := coalesce(p_enabled, false);
|
v_enabled boolean := coalesce(p_enabled, false);
|
||||||
begin
|
begin
|
||||||
if v_email not in (
|
if not public.tablero_cdc_current_user_can_control_pricing_summary() then
|
||||||
'gmarrero@gomezleemarketing.com',
|
|
||||||
'jgomez@gomezleemarketing.com',
|
|
||||||
'iaracena@gomezleemarketing.com'
|
|
||||||
) then
|
|
||||||
raise exception 'No tienes permiso para cambiar la visibilidad del total tarifado.';
|
raise exception 'No tienes permiso para cambiar la visibilidad del total tarifado.';
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
@@ -128,10 +343,11 @@ begin
|
|||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
grant select on public.tablero_cdc_app_settings to authenticated;
|
|
||||||
grant execute on function public.tablero_cdc_set_pricing_summary_public(boolean) to authenticated;
|
grant execute on function public.tablero_cdc_set_pricing_summary_public(boolean) to authenticated;
|
||||||
|
|
||||||
-- Reemplaza la RPC de resumen para respetar visibilidad pública temporal.
|
-- Reemplaza la RPC de resumen para respetar:
|
||||||
|
-- 1) que el usuario esté autorizado para la app;
|
||||||
|
-- 2) que sea controlador del Total Tarifado o que el total esté público.
|
||||||
create or replace function public.tablero_cdc_get_pricing_summary(
|
create or replace function public.tablero_cdc_get_pricing_summary(
|
||||||
p_tab text default 'todos',
|
p_tab text default 'todos',
|
||||||
p_search text default '',
|
p_search text default '',
|
||||||
@@ -153,12 +369,11 @@ as $$
|
|||||||
),
|
),
|
||||||
allowed as (
|
allowed as (
|
||||||
select
|
select
|
||||||
lower(coalesce(auth.jwt() ->> 'email', '')) in (
|
public.tablero_cdc_current_user_is_allowed()
|
||||||
'gmarrero@gomezleemarketing.com',
|
and (
|
||||||
'jgomez@gomezleemarketing.com',
|
public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
'iaracena@gomezleemarketing.com'
|
or coalesce((select is_public from visibility), false)
|
||||||
)
|
) as can_view_pricing
|
||||||
or coalesce((select is_public from visibility), false) as can_view_pricing
|
|
||||||
),
|
),
|
||||||
params as (
|
params as (
|
||||||
select
|
select
|
||||||
@@ -234,7 +449,6 @@ $$;
|
|||||||
grant execute on function public.tablero_cdc_get_pricing_summary(text, text, text, text, text, text) to authenticated;
|
grant execute on function public.tablero_cdc_get_pricing_summary(text, text, text, text, text, text) to authenticated;
|
||||||
|
|
||||||
-- Intenta habilitar realtime para que el cambio de visible/oculto se refleje sin recargar.
|
-- Intenta habilitar realtime para que el cambio de visible/oculto se refleje sin recargar.
|
||||||
-- Si la publicación ya existe o la tabla ya está agregada, no hace nada.
|
|
||||||
do $$
|
do $$
|
||||||
begin
|
begin
|
||||||
alter publication supabase_realtime add table public.tablero_cdc_app_settings;
|
alter publication supabase_realtime add table public.tablero_cdc_app_settings;
|
||||||
@@ -243,6 +457,12 @@ exception
|
|||||||
when undefined_object then null;
|
when undefined_object then null;
|
||||||
end $$;
|
end $$;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
select
|
select
|
||||||
'Visibilidad del Total Tarifado lista' as status,
|
'Acceso Tablero CDC listo' as status,
|
||||||
(select value from public.tablero_cdc_app_settings where key = 'pricing_summary_public') as pricing_summary_public;
|
count(*) filter (where is_active) as usuarios_activos,
|
||||||
|
count(*) filter (where can_control_pricing_summary and is_active) as controlan_total_tarifado,
|
||||||
|
count(*) filter (where can_manage_internal_pricing and is_active) as ven_interno_cargado,
|
||||||
|
count(*) filter (where can_delete_projects and is_active) as pueden_eliminar
|
||||||
|
from public.tablero_cdc_allowed_users;
|
||||||
|
|||||||
@@ -0,0 +1,468 @@
|
|||||||
|
-- =========================================================
|
||||||
|
-- TABLERO CDC - Acceso autorizado por Supabase
|
||||||
|
-- Ejecutar en Supabase SQL Editor ANTES de desplegar el ZIP.
|
||||||
|
--
|
||||||
|
-- Objetivo:
|
||||||
|
-- - El acceso a la app ya no depende de una lista fija en código.
|
||||||
|
-- - Los usuarios permitidos viven en public.tablero_cdc_allowed_users.
|
||||||
|
-- - Para agregar/quitar acceso luego, se actualiza esta tabla en Supabase.
|
||||||
|
-- - También centraliza permisos especiales usados por la app:
|
||||||
|
-- * can_manage_internal_pricing
|
||||||
|
-- * can_delete_projects
|
||||||
|
-- * can_control_pricing_summary
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create table if not exists public.tablero_cdc_allowed_users (
|
||||||
|
email text primary key,
|
||||||
|
full_name text,
|
||||||
|
role text not null default 'user',
|
||||||
|
is_active boolean not null default true,
|
||||||
|
can_delete_projects boolean not null default false,
|
||||||
|
can_manage_internal_pricing boolean not null default false,
|
||||||
|
can_control_pricing_summary boolean not null default false,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
comment on table public.tablero_cdc_allowed_users is
|
||||||
|
'Lista central de usuarios autorizados para entrar al Tablero CDC.';
|
||||||
|
|
||||||
|
comment on column public.tablero_cdc_allowed_users.role is
|
||||||
|
'Rol lógico de la app. director_creativo habilita comportamiento especial de Director Creativo.';
|
||||||
|
|
||||||
|
create or replace function public.set_tablero_cdc_allowed_users_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.email = lower(trim(new.email));
|
||||||
|
new.updated_at = now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_tablero_cdc_allowed_users_updated_at on public.tablero_cdc_allowed_users;
|
||||||
|
|
||||||
|
create trigger trg_tablero_cdc_allowed_users_updated_at
|
||||||
|
before insert or update on public.tablero_cdc_allowed_users
|
||||||
|
for each row
|
||||||
|
execute function public.set_tablero_cdc_allowed_users_updated_at();
|
||||||
|
|
||||||
|
-- Usuarios autorizados iniciales.
|
||||||
|
-- Nota: Re-ejecutar este SQL mantiene estos usuarios activos y actualiza sus permisos base.
|
||||||
|
insert into public.tablero_cdc_allowed_users (
|
||||||
|
email,
|
||||||
|
full_name,
|
||||||
|
role,
|
||||||
|
is_active,
|
||||||
|
can_delete_projects,
|
||||||
|
can_manage_internal_pricing,
|
||||||
|
can_control_pricing_summary
|
||||||
|
)
|
||||||
|
values
|
||||||
|
(
|
||||||
|
'iaracena@gomezleemarketing.com',
|
||||||
|
'Isaac Daniel Aracena Toribio',
|
||||||
|
'admin_it',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'jgomez@gomezleemarketing.com',
|
||||||
|
'José Leopoldo Gomez',
|
||||||
|
'leadership',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'gmarrero@gomezleemarketing.com',
|
||||||
|
'Gerardo Marrero',
|
||||||
|
'director_creativo',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'areyes@gomezleemarketing.com',
|
||||||
|
'Alicia Thaía Reyes',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'boliva@gomezleemarketing.com',
|
||||||
|
'Bonnie Oliva',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'obrisceno@gomezleemarketing.com',
|
||||||
|
'Oscar Brisceño',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'lavila01@gomezlee.marketing',
|
||||||
|
'Lady Avila Faura',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'rmarcano@gomezleemarketing.com',
|
||||||
|
'Ramon Marcano',
|
||||||
|
'user',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'lmatos@gomezleemarketing.com',
|
||||||
|
'Luis Matos',
|
||||||
|
'it',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'mgomez@gomezleemarketing.com',
|
||||||
|
'Máximo Gomez',
|
||||||
|
'leadership',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
on conflict (email)
|
||||||
|
do update set
|
||||||
|
full_name = excluded.full_name,
|
||||||
|
role = excluded.role,
|
||||||
|
is_active = excluded.is_active,
|
||||||
|
can_delete_projects = excluded.can_delete_projects,
|
||||||
|
can_manage_internal_pricing = excluded.can_manage_internal_pricing,
|
||||||
|
can_control_pricing_summary = excluded.can_control_pricing_summary,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
alter table public.tablero_cdc_allowed_users enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "tablero_cdc_allowed_users_select_own_active" on public.tablero_cdc_allowed_users;
|
||||||
|
drop policy if exists "tablero_cdc_allowed_users_no_insert_from_app" on public.tablero_cdc_allowed_users;
|
||||||
|
drop policy if exists "tablero_cdc_allowed_users_no_update_from_app" on public.tablero_cdc_allowed_users;
|
||||||
|
drop policy if exists "tablero_cdc_allowed_users_no_delete_from_app" on public.tablero_cdc_allowed_users;
|
||||||
|
|
||||||
|
-- La app solo puede leer la fila activa del usuario autenticado.
|
||||||
|
-- La administración real se hace desde Supabase SQL Editor.
|
||||||
|
create policy "tablero_cdc_allowed_users_select_own_active"
|
||||||
|
on public.tablero_cdc_allowed_users
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
is_active = true
|
||||||
|
and lower(email) = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
create policy "tablero_cdc_allowed_users_no_insert_from_app"
|
||||||
|
on public.tablero_cdc_allowed_users
|
||||||
|
for insert
|
||||||
|
to authenticated
|
||||||
|
with check (false);
|
||||||
|
|
||||||
|
create policy "tablero_cdc_allowed_users_no_update_from_app"
|
||||||
|
on public.tablero_cdc_allowed_users
|
||||||
|
for update
|
||||||
|
to authenticated
|
||||||
|
using (false)
|
||||||
|
with check (false);
|
||||||
|
|
||||||
|
create policy "tablero_cdc_allowed_users_no_delete_from_app"
|
||||||
|
on public.tablero_cdc_allowed_users
|
||||||
|
for delete
|
||||||
|
to authenticated
|
||||||
|
using (false);
|
||||||
|
|
||||||
|
grant select on public.tablero_cdc_allowed_users to authenticated;
|
||||||
|
|
||||||
|
-- Funciones auxiliares para políticas/RPC.
|
||||||
|
create or replace function public.tablero_cdc_current_user_is_allowed()
|
||||||
|
returns boolean
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from public.tablero_cdc_allowed_users u
|
||||||
|
where u.email = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||||
|
and u.is_active = true
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create or replace function public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
|
returns boolean
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from public.tablero_cdc_allowed_users u
|
||||||
|
where u.email = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||||
|
and u.is_active = true
|
||||||
|
and u.can_control_pricing_summary = true
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant execute on function public.tablero_cdc_current_user_is_allowed() to authenticated;
|
||||||
|
grant execute on function public.tablero_cdc_current_user_can_control_pricing_summary() to authenticated;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- Total Tarifado: configuración pública/privada usando tabla de accesos
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
create table if not exists public.tablero_cdc_app_settings (
|
||||||
|
key text primary key,
|
||||||
|
value jsonb not null default '{}'::jsonb,
|
||||||
|
updated_by uuid references auth.users(id),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into public.tablero_cdc_app_settings (key, value)
|
||||||
|
values ('pricing_summary_public', jsonb_build_object('enabled', false))
|
||||||
|
on conflict (key) do nothing;
|
||||||
|
|
||||||
|
alter table public.tablero_cdc_app_settings enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "tablero_cdc_app_settings_select_authenticated" on public.tablero_cdc_app_settings;
|
||||||
|
drop policy if exists "tablero_cdc_app_settings_select_allowed_users" on public.tablero_cdc_app_settings;
|
||||||
|
drop policy if exists "tablero_cdc_app_settings_insert_summary_controllers" on public.tablero_cdc_app_settings;
|
||||||
|
drop policy if exists "tablero_cdc_app_settings_update_summary_controllers" on public.tablero_cdc_app_settings;
|
||||||
|
drop policy if exists "tablero_cdc_app_settings_delete_none" on public.tablero_cdc_app_settings;
|
||||||
|
|
||||||
|
create policy "tablero_cdc_app_settings_select_allowed_users"
|
||||||
|
on public.tablero_cdc_app_settings
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.tablero_cdc_current_user_is_allowed());
|
||||||
|
|
||||||
|
create policy "tablero_cdc_app_settings_insert_summary_controllers"
|
||||||
|
on public.tablero_cdc_app_settings
|
||||||
|
for insert
|
||||||
|
to authenticated
|
||||||
|
with check (
|
||||||
|
key = 'pricing_summary_public'
|
||||||
|
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
|
);
|
||||||
|
|
||||||
|
create policy "tablero_cdc_app_settings_update_summary_controllers"
|
||||||
|
on public.tablero_cdc_app_settings
|
||||||
|
for update
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
key = 'pricing_summary_public'
|
||||||
|
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
|
)
|
||||||
|
with check (
|
||||||
|
key = 'pricing_summary_public'
|
||||||
|
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
|
);
|
||||||
|
|
||||||
|
create policy "tablero_cdc_app_settings_delete_none"
|
||||||
|
on public.tablero_cdc_app_settings
|
||||||
|
for delete
|
||||||
|
to authenticated
|
||||||
|
using (false);
|
||||||
|
|
||||||
|
create or replace function public.set_tablero_cdc_app_settings_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.updated_at = now();
|
||||||
|
new.updated_by = auth.uid();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_tablero_cdc_app_settings_updated_at on public.tablero_cdc_app_settings;
|
||||||
|
|
||||||
|
create trigger trg_tablero_cdc_app_settings_updated_at
|
||||||
|
before insert or update on public.tablero_cdc_app_settings
|
||||||
|
for each row
|
||||||
|
execute function public.set_tablero_cdc_app_settings_updated_at();
|
||||||
|
|
||||||
|
grant select on public.tablero_cdc_app_settings to authenticated;
|
||||||
|
|
||||||
|
create or replace function public.tablero_cdc_set_pricing_summary_public(p_enabled boolean)
|
||||||
|
returns jsonb
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_enabled boolean := coalesce(p_enabled, false);
|
||||||
|
begin
|
||||||
|
if not public.tablero_cdc_current_user_can_control_pricing_summary() then
|
||||||
|
raise exception 'No tienes permiso para cambiar la visibilidad del total tarifado.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into public.tablero_cdc_app_settings (key, value, updated_by, updated_at)
|
||||||
|
values (
|
||||||
|
'pricing_summary_public',
|
||||||
|
jsonb_build_object('enabled', v_enabled),
|
||||||
|
auth.uid(),
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
on conflict (key)
|
||||||
|
do update set
|
||||||
|
value = excluded.value,
|
||||||
|
updated_by = auth.uid(),
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
return jsonb_build_object('enabled', v_enabled);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant execute on function public.tablero_cdc_set_pricing_summary_public(boolean) to authenticated;
|
||||||
|
|
||||||
|
-- Reemplaza la RPC de resumen para respetar:
|
||||||
|
-- 1) que el usuario esté autorizado para la app;
|
||||||
|
-- 2) que sea controlador del Total Tarifado o que el total esté público.
|
||||||
|
create or replace function public.tablero_cdc_get_pricing_summary(
|
||||||
|
p_tab text default 'todos',
|
||||||
|
p_search text default '',
|
||||||
|
p_country text default '',
|
||||||
|
p_brand text default '',
|
||||||
|
p_client text default '',
|
||||||
|
p_cm text default ''
|
||||||
|
)
|
||||||
|
returns jsonb
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security invoker
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
with visibility as (
|
||||||
|
select coalesce((value ->> 'enabled')::boolean, false) as is_public
|
||||||
|
from public.tablero_cdc_app_settings
|
||||||
|
where key = 'pricing_summary_public'
|
||||||
|
),
|
||||||
|
allowed as (
|
||||||
|
select
|
||||||
|
public.tablero_cdc_current_user_is_allowed()
|
||||||
|
and (
|
||||||
|
public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||||
|
or coalesce((select is_public from visibility), false)
|
||||||
|
) as can_view_pricing
|
||||||
|
),
|
||||||
|
params as (
|
||||||
|
select
|
||||||
|
lower(coalesce(nullif(trim(p_tab), ''), 'todos')) as tab,
|
||||||
|
trim(coalesce(p_search, '')) as search_text,
|
||||||
|
trim(coalesce(p_country, '')) as country_filter,
|
||||||
|
trim(coalesce(p_brand, '')) as brand_filter,
|
||||||
|
trim(coalesce(p_client, '')) as client_filter,
|
||||||
|
trim(coalesce(p_cm, '')) as cm_filter
|
||||||
|
),
|
||||||
|
base_filtered as (
|
||||||
|
select p.*
|
||||||
|
from public.tablero_cdc_projects p
|
||||||
|
cross join params prm
|
||||||
|
where
|
||||||
|
case
|
||||||
|
when prm.tab in ('abiertos', 'activos', 'active', 'open') then
|
||||||
|
coalesce(nullif(trim(p.status), ''), 'Activo') = 'Activo'
|
||||||
|
when prm.tab in ('cerrados', 'closed') then
|
||||||
|
coalesce(nullif(trim(p.status), ''), 'Activo') <> 'Activo'
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
and (
|
||||||
|
prm.search_text = ''
|
||||||
|
or coalesce(p.title, '') ilike '%' || prm.search_text || '%'
|
||||||
|
or coalesce(p.client, '') ilike '%' || prm.search_text || '%'
|
||||||
|
or coalesce(p.brand, '') ilike '%' || prm.search_text || '%'
|
||||||
|
or coalesce(p.country, '') ilike '%' || prm.search_text || '%'
|
||||||
|
or coalesce(p.requested_by, '') ilike '%' || prm.search_text || '%'
|
||||||
|
or coalesce(p.country_manager, '') ilike '%' || prm.search_text || '%'
|
||||||
|
or coalesce(p.description, '') ilike '%' || prm.search_text || '%'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
filtered as (
|
||||||
|
select bf.*
|
||||||
|
from base_filtered bf
|
||||||
|
cross join params prm
|
||||||
|
where
|
||||||
|
(prm.country_filter = '' or coalesce(bf.country, '') ilike '%' || prm.country_filter || '%')
|
||||||
|
and (prm.brand_filter = '' or coalesce(bf.brand, '') ilike '%' || prm.brand_filter || '%')
|
||||||
|
and (prm.client_filter = '' or coalesce(bf.client, '') ilike '%' || prm.client_filter || '%')
|
||||||
|
and (prm.cm_filter = '' or coalesce(bf.country_manager, '') ilike '%' || prm.cm_filter || '%')
|
||||||
|
),
|
||||||
|
totals as (
|
||||||
|
select
|
||||||
|
count(*)::integer as project_count,
|
||||||
|
coalesce(
|
||||||
|
sum(
|
||||||
|
greatest(
|
||||||
|
coalesce(nullif(trim(internal_amount::text), '')::numeric, 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)::numeric as total_amount
|
||||||
|
from filtered
|
||||||
|
)
|
||||||
|
select case
|
||||||
|
when (select can_view_pricing from allowed) then
|
||||||
|
jsonb_build_object(
|
||||||
|
'total_amount', (select total_amount from totals),
|
||||||
|
'project_count', (select project_count from totals)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
jsonb_build_object(
|
||||||
|
'total_amount', 0,
|
||||||
|
'project_count', 0
|
||||||
|
)
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant execute on function public.tablero_cdc_get_pricing_summary(text, text, text, text, text, text) to authenticated;
|
||||||
|
|
||||||
|
-- Intenta habilitar realtime para que el cambio de visible/oculto se refleje sin recargar.
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
alter publication supabase_realtime add table public.tablero_cdc_app_settings;
|
||||||
|
exception
|
||||||
|
when duplicate_object then null;
|
||||||
|
when undefined_object then null;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
select
|
||||||
|
'Acceso Tablero CDC listo' as status,
|
||||||
|
count(*) filter (where is_active) as usuarios_activos,
|
||||||
|
count(*) filter (where can_control_pricing_summary and is_active) as controlan_total_tarifado,
|
||||||
|
count(*) filter (where can_manage_internal_pricing and is_active) as ven_interno_cargado,
|
||||||
|
count(*) filter (where can_delete_projects and is_active) as pueden_eliminar
|
||||||
|
from public.tablero_cdc_allowed_users;
|
||||||