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">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" fill="transparent"/>
|
||||
<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>
|
||||
</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>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tablero CDC</title>
|
||||
<meta name="description" content="Herramienta interna del Departamento de Diseño Creativo." />
|
||||
<meta name="author" content="CDC" />
|
||||
<link rel="icon" type="image/svg+xml" href="/tablero-cdc/favicon.svg" />
|
||||
<script type="module" crossorigin src="/tablero-cdc/assets/index-BrJEDdGr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/tablero-cdc/assets/index-CF6PF3ek.css">
|
||||
<title>Tablero CDC | GLM</title>
|
||||
<meta name="description" content="Herramienta interna de GomezLee Marketing para gestión de proyectos CDC." />
|
||||
<meta name="author" content="GomezLee Marketing" />
|
||||
<link rel="icon" type="image/png" href="/tablero-cdc/glm-favicon.png?v=glm-logo-final-20260626" />
|
||||
<link rel="shortcut icon" type="image/png" href="/tablero-cdc/glm-favicon.png?v=glm-logo-final-20260626" />
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tablero CDC</title>
|
||||
<meta name="description" content="Herramienta interna del Departamento de Diseño Creativo." />
|
||||
<meta name="author" content="CDC" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Tablero CDC | GLM</title>
|
||||
<meta name="description" content="Herramienta interna de GomezLee Marketing para gestión de proyectos CDC." />
|
||||
<meta name="author" content="GomezLee Marketing" />
|
||||
<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>
|
||||
<body>
|
||||
<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">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" fill="transparent"/>
|
||||
<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>
|
||||
</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 { AuthProvider, useAuth } from "@/context/AuthContext";
|
||||
import { LoginScreen } from "@/components/LoginScreen";
|
||||
import { GLMLogo } from "@/components/GLMLogo";
|
||||
import BoardPage from "./pages/BoardPage";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
@@ -62,13 +63,8 @@ function AuthGate() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<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">
|
||||
<svg className="w-4 h-4 text-primary-foreground" fill="none" viewBox="0 0 24 24">
|
||||
<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 className="animate-pulse">
|
||||
<GLMLogo showWordmark={false} />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Cargando tablero…</p>
|
||||
</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 { useAuth } from "@/context/AuthContext";
|
||||
import { GLMLogo } from "@/components/GLMLogo";
|
||||
|
||||
export function LoginScreen() {
|
||||
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="w-full max-w-sm">
|
||||
{/* Logo / marca */}
|
||||
<div className="flex flex-col items-center mb-8 gap-3">
|
||||
<div className="w-14 h-14 rounded-2xl bg-primary flex items-center justify-center shadow-lg">
|
||||
<LayoutGrid className="w-7 h-7 text-primary-foreground" />
|
||||
</div>
|
||||
<div className="text-center leading-tight">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Tablero CDC</h1>
|
||||
</div>
|
||||
<div className="flex flex-col items-center mb-8 gap-5">
|
||||
<GLMLogo size="lg" />
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[#4F758B]">Tablero CDC</h1>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
@@ -43,7 +39,7 @@ export function LoginScreen() {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,11 +63,7 @@ export function ProjectPricingPanel({ items, onChange, loading, error }: Props)
|
||||
const [manualName, setManualName] = useState<string>("");
|
||||
const [manualDescription, setManualDescription] = useState<string>("");
|
||||
const [manualAmount, setManualAmount] = useState<string>("");
|
||||
const {
|
||||
catalog: tariffCatalog,
|
||||
loading: tariffLoading,
|
||||
error: tariffError,
|
||||
} = useTariffCatalog();
|
||||
const { catalog: tariffCatalog, loading: tariffLoading, error: tariffError } = useTariffCatalog();
|
||||
|
||||
const sectionOptions = TARIFF_SECTIONS.map((option) => option.label);
|
||||
const selectedSectionLabel = TARIFF_SECTIONS.find((option) => option.id === section)?.label || "";
|
||||
@@ -237,7 +233,6 @@ export function ProjectPricingPanel({ items, onChange, loading, error }: Props)
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{(loading || tariffLoading) && (
|
||||
<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…"}
|
||||
|
||||
@@ -9,19 +9,7 @@ import React, {
|
||||
} from "react";
|
||||
import type { User as SupabaseUser } from "@supabase/supabase-js";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
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}`);
|
||||
}
|
||||
import { getActiveTableroCdcAccess, type TableroCdcUserAccess } from "@/lib/accessControl";
|
||||
|
||||
function getRedirectTo(): string {
|
||||
if (typeof window === "undefined") return "/tablero-cdc/";
|
||||
@@ -75,23 +63,44 @@ const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [access, setAccess] = useState<TableroCdcUserAccess | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const applyUser = useCallback(async (supabaseUser: SupabaseUser | null) => {
|
||||
if (!supabaseUser) {
|
||||
setUser(null);
|
||||
setAccess(null);
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
setUser(normalizeSupabaseUser(supabaseUser));
|
||||
setAccess(nextAccess);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
@@ -123,8 +132,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
});
|
||||
|
||||
const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
void applyUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
void applyUser(session?.user ?? null).finally(() => {
|
||||
if (mounted) setLoading(false);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -142,7 +153,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
redirectTo: getRedirectTo(),
|
||||
queryParams: {
|
||||
prompt: "select_account",
|
||||
hd: ALLOWED_DOMAIN,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -163,22 +173,22 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
setUser(null);
|
||||
setAccess(null);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const normalizedEmail = user?.email?.toLowerCase() || "";
|
||||
const isGerardo = useMemo(() => normalizedEmail === GERARDO_EMAIL, [normalizedEmail]);
|
||||
const isGerardo = useMemo(() => access?.role === "director_creativo", [access?.role]);
|
||||
const canManageInternalPricing = useMemo(
|
||||
() => INTERNAL_PRICING_ALLOWED_EMAILS.includes(normalizedEmail),
|
||||
[normalizedEmail],
|
||||
() => access?.canManageInternalPricing === true,
|
||||
[access?.canManageInternalPricing],
|
||||
);
|
||||
const canDeleteProjects = useMemo(
|
||||
() => DELETE_ALLOWED_EMAILS.includes(normalizedEmail),
|
||||
[normalizedEmail],
|
||||
() => access?.canDeleteProjects === true,
|
||||
[access?.canDeleteProjects],
|
||||
);
|
||||
const canControlPricingSummary = useMemo(
|
||||
() => PRICING_SUMMARY_CONTROLLER_EMAILS.includes(normalizedEmail),
|
||||
[normalizedEmail],
|
||||
() => access?.canControlPricingSummary === true,
|
||||
[access?.canControlPricingSummary],
|
||||
);
|
||||
|
||||
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) {
|
||||
console.error("No se pudo leer la visibilidad del total tarifado:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "No se pudo leer la visibilidad del total tarifado.",
|
||||
err instanceof Error ? err.message : "No se pudo leer la visibilidad del total tarifado.",
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
@@ -63,8 +61,7 @@ export function usePricingSummaryVisibility(enabled = true) {
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
const setPublicVisibility = useCallback(
|
||||
async (nextIsPublic: boolean) => {
|
||||
const setPublicVisibility = useCallback(async (nextIsPublic: boolean) => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
@@ -83,9 +80,7 @@ export function usePricingSummaryVisibility(enabled = true) {
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getCurrentTableroCdcAccess } from "@/lib/accessControl";
|
||||
import type { ColorId, Status } from "@/data/lists";
|
||||
|
||||
export interface Project {
|
||||
@@ -178,9 +179,6 @@ type DirectProjectSummaryRow = {
|
||||
|
||||
const DEFAULT_COLOR: ColorId = "blue";
|
||||
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 = {
|
||||
tab: "todos",
|
||||
search: "",
|
||||
@@ -478,18 +476,18 @@ async function getCurrentUserId(): Promise<string> {
|
||||
}
|
||||
|
||||
async function currentUserIsGerardo(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
return user.email?.toLowerCase() === GERARDO_EMAIL;
|
||||
const access = await getCurrentTableroCdcAccess();
|
||||
return access.role === "director_creativo";
|
||||
}
|
||||
|
||||
async function currentUserCanManageInternalPricing(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
return INTERNAL_PRICING_ALLOWED_EMAILS.includes(user.email?.toLowerCase() || "");
|
||||
const access = await getCurrentTableroCdcAccess();
|
||||
return access.canManageInternalPricing;
|
||||
}
|
||||
|
||||
async function currentUserCanDeleteProjects(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
return DELETE_ALLOWED_EMAILS.includes(user.email?.toLowerCase() || "");
|
||||
const access = await getCurrentTableroCdcAccess();
|
||||
return access.canDeleteProjects;
|
||||
}
|
||||
|
||||
function toProjectRow(project: Omit<Project, "id" | "createdAt"> | Partial<Project>) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import { usePricingSummaryVisibility } from "@/lib/pricingSummaryVisibility";
|
||||
import { useAppLists } from "@/lib/appLists";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { dedupeOptions } from "@/lib/optionUtils";
|
||||
import { GLMLogo } from "@/components/GLMLogo";
|
||||
|
||||
/** Extract up-to-2-letter initials from a display name or email */
|
||||
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">
|
||||
<div className="max-w-[1400px] mx-auto px-6 py-3.5 flex items-center gap-4">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center">
|
||||
<LayoutGrid className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<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 className="flex items-center gap-4">
|
||||
<GLMLogo size="sm" />
|
||||
<div className="h-7 w-px bg-border" />
|
||||
<h1 className="text-[15px] font-semibold tracking-tight text-[#4F758B]">Tablero CDC</h1>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
|
||||
@@ -64,48 +64,57 @@
|
||||
:root {
|
||||
--radius: 0.875rem;
|
||||
|
||||
/* Google-like clean palette + soft purple accent */
|
||||
--background: oklch(0.99 0.003 280);
|
||||
--foreground: oklch(0.25 0.02 280);
|
||||
/* GLM palette — soft, clean and low-noise for the existing CDC UI */
|
||||
--glm-blue: #4f758b;
|
||||
--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-foreground: oklch(0.25 0.02 280);
|
||||
--card-foreground: oklch(0.4091 0 89.9);
|
||||
--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-foreground: oklch(0.99 0.005 280);
|
||||
--primary: oklch(0.5427 0.055 234.6);
|
||||
--primary-foreground: oklch(1 0 89.9);
|
||||
|
||||
--secondary: oklch(0.97 0.005 280);
|
||||
--secondary-foreground: oklch(0.3 0.02 280);
|
||||
--muted: oklch(0.965 0.004 280);
|
||||
--muted-foreground: oklch(0.5 0.015 280);
|
||||
--accent: oklch(0.96 0.02 290);
|
||||
--accent-foreground: oklch(0.35 0.05 290);
|
||||
--secondary: oklch(0.9702 0 89.9);
|
||||
--secondary-foreground: oklch(0.4091 0 89.9);
|
||||
--muted: oklch(0.9702 0 89.9);
|
||||
--muted-foreground: oklch(0.6304 0.0501 232.9);
|
||||
--accent: oklch(0.9633 0.0203 131.5);
|
||||
--accent-foreground: oklch(0.4968 0.0663 196.6);
|
||||
|
||||
--destructive: oklch(0.6 0.22 27);
|
||||
--destructive-foreground: oklch(0.99 0 0);
|
||||
|
||||
--border: oklch(0.925 0.005 280);
|
||||
--input: oklch(0.93 0.005 280);
|
||||
--ring: oklch(0.65 0.15 290);
|
||||
--border: oklch(0.9 0.01 235);
|
||||
--input: oklch(0.91 0.01 235);
|
||||
--ring: oklch(0.7344 0.1772 137.9);
|
||||
|
||||
--chart-1: oklch(0.65 0.18 250);
|
||||
--chart-2: oklch(0.65 0.18 150);
|
||||
--chart-3: oklch(0.7 0.16 80);
|
||||
--chart-4: oklch(0.6 0.2 20);
|
||||
--chart-5: oklch(0.55 0.18 290);
|
||||
--chart-1: oklch(0.5427 0.055 234.6);
|
||||
--chart-2: oklch(0.7344 0.1772 137.9);
|
||||
--chart-3: oklch(0.5778 0.0529 235.7);
|
||||
--chart-4: oklch(0.7013 0.1996 43.7);
|
||||
--chart-5: oklch(0.787 0.1232 161.2);
|
||||
|
||||
--sidebar: oklch(0.985 0.003 280);
|
||||
--sidebar-foreground: oklch(0.25 0.02 280);
|
||||
--sidebar-primary: oklch(0.55 0.18 290);
|
||||
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||
--sidebar-accent: oklch(0.96 0.02 290);
|
||||
--sidebar-accent-foreground: oklch(0.35 0.05 290);
|
||||
--sidebar-border: oklch(0.925 0.005 280);
|
||||
--sidebar-ring: oklch(0.65 0.15 290);
|
||||
--sidebar: oklch(0.985 0.004 131);
|
||||
--sidebar-foreground: oklch(0.4091 0 89.9);
|
||||
--sidebar-primary: oklch(0.5427 0.055 234.6);
|
||||
--sidebar-primary-foreground: oklch(1 0 89.9);
|
||||
--sidebar-accent: oklch(0.9633 0.0203 131.5);
|
||||
--sidebar-accent-foreground: oklch(0.4968 0.0663 196.6);
|
||||
--sidebar-border: oklch(0.9 0.01 235);
|
||||
--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-hover: 0 4px 8px oklch(0.5 0.02 280 / 0.08), 0 12px 28px oklch(0.5 0.02 280 / 0.10);
|
||||
--shadow-soft:
|
||||
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 {
|
||||
|
||||
@@ -1,14 +1,243 @@
|
||||
-- =========================================================
|
||||
-- TABLERO CDC - Visibilidad del Total Tarifado
|
||||
-- Ejecutar en Supabase SQL Editor antes de desplegar esta versión.
|
||||
-- TABLERO CDC - Acceso autorizado por Supabase
|
||||
-- Ejecutar en Supabase SQL Editor ANTES de desplegar el ZIP.
|
||||
--
|
||||
-- Objetivo:
|
||||
-- - Por defecto, el Total Tarifado solo lo ven:
|
||||
-- * gmarrero@gomezleemarketing.com
|
||||
-- * jgomez@gomezleemarketing.com
|
||||
-- * iaracena@gomezleemarketing.com
|
||||
-- - Ellos pueden activar/desactivar un botón para mostrarlo temporalmente a todos.
|
||||
-- - No expone montos internos por proyecto; solo habilita/oculta el resumen global/filtrado.
|
||||
-- - 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 (
|
||||
@@ -25,15 +254,16 @@ 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_authenticated"
|
||||
create policy "tablero_cdc_app_settings_select_allowed_users"
|
||||
on public.tablero_cdc_app_settings
|
||||
for select
|
||||
to authenticated
|
||||
using (true);
|
||||
using (public.tablero_cdc_current_user_is_allowed());
|
||||
|
||||
create policy "tablero_cdc_app_settings_insert_summary_controllers"
|
||||
on public.tablero_cdc_app_settings
|
||||
@@ -41,11 +271,7 @@ create policy "tablero_cdc_app_settings_insert_summary_controllers"
|
||||
to authenticated
|
||||
with check (
|
||||
key = 'pricing_summary_public'
|
||||
and lower(coalesce(auth.jwt() ->> 'email', '')) in (
|
||||
'gmarrero@gomezleemarketing.com',
|
||||
'jgomez@gomezleemarketing.com',
|
||||
'iaracena@gomezleemarketing.com'
|
||||
)
|
||||
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||
);
|
||||
|
||||
create policy "tablero_cdc_app_settings_update_summary_controllers"
|
||||
@@ -54,19 +280,11 @@ create policy "tablero_cdc_app_settings_update_summary_controllers"
|
||||
to authenticated
|
||||
using (
|
||||
key = 'pricing_summary_public'
|
||||
and lower(coalesce(auth.jwt() ->> 'email', '')) in (
|
||||
'gmarrero@gomezleemarketing.com',
|
||||
'jgomez@gomezleemarketing.com',
|
||||
'iaracena@gomezleemarketing.com'
|
||||
)
|
||||
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||
)
|
||||
with check (
|
||||
key = 'pricing_summary_public'
|
||||
and lower(coalesce(auth.jwt() ->> 'email', '')) in (
|
||||
'gmarrero@gomezleemarketing.com',
|
||||
'jgomez@gomezleemarketing.com',
|
||||
'iaracena@gomezleemarketing.com'
|
||||
)
|
||||
and public.tablero_cdc_current_user_can_control_pricing_summary()
|
||||
);
|
||||
|
||||
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
|
||||
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
|
||||
@@ -100,14 +320,9 @@ security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
v_email text := lower(coalesce(auth.jwt() ->> 'email', ''));
|
||||
v_enabled boolean := coalesce(p_enabled, false);
|
||||
begin
|
||||
if v_email not in (
|
||||
'gmarrero@gomezleemarketing.com',
|
||||
'jgomez@gomezleemarketing.com',
|
||||
'iaracena@gomezleemarketing.com'
|
||||
) then
|
||||
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;
|
||||
|
||||
@@ -128,10 +343,11 @@ begin
|
||||
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;
|
||||
|
||||
-- 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(
|
||||
p_tab text default 'todos',
|
||||
p_search text default '',
|
||||
@@ -153,12 +369,11 @@ as $$
|
||||
),
|
||||
allowed as (
|
||||
select
|
||||
lower(coalesce(auth.jwt() ->> 'email', '')) in (
|
||||
'gmarrero@gomezleemarketing.com',
|
||||
'jgomez@gomezleemarketing.com',
|
||||
'iaracena@gomezleemarketing.com'
|
||||
)
|
||||
or coalesce((select is_public from visibility), false) as can_view_pricing
|
||||
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
|
||||
@@ -234,7 +449,6 @@ $$;
|
||||
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.
|
||||
-- Si la publicación ya existe o la tabla ya está agregada, no hace nada.
|
||||
do $$
|
||||
begin
|
||||
alter publication supabase_realtime add table public.tablero_cdc_app_settings;
|
||||
@@ -243,6 +457,12 @@ exception
|
||||
when undefined_object then null;
|
||||
end $$;
|
||||
|
||||
commit;
|
||||
|
||||
select
|
||||
'Visibilidad del Total Tarifado lista' as status,
|
||||
(select value from public.tablero_cdc_app_settings where key = 'pricing_summary_public') as pricing_summary_public;
|
||||
'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;
|
||||
|
||||
@@ -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;
|
||||