feat: agregar control de visibilidad del total tarifado

This commit is contained in:
2026-06-22 10:05:21 -04:00
parent 5adf487bd0
commit 5f5b05e8fd
13 changed files with 1593 additions and 432 deletions
+228 -15
View File
@@ -76,6 +76,12 @@ export type ProjectFilterOptions = {
cms: string[];
};
export type ProjectPricingSummary = {
totalAmount: number;
projectCount: number;
loadedAt: number | null;
};
export type ProjectQueryParams = {
tab: ProjectTab;
search: string;
@@ -150,6 +156,11 @@ type RpcProjectsResponse = {
filter_options?: Partial<ProjectFilterOptions> | null;
};
type RpcPricingSummaryResponse = {
total_amount?: number | string | null;
project_count?: number | string | null;
};
type DirectProjectQuery = {
or: (filters: string) => DirectProjectQuery;
not: (column: string, operator: string, value: unknown) => DirectProjectQuery;
@@ -160,19 +171,16 @@ type DirectProjectQuery = {
) => Promise<{ data: unknown[] | null; error: { message: string } | null; count: number | null }>;
};
type DirectProjectSummaryRow = {
id?: string | null;
internal_amount?: number | string | null;
};
const DEFAULT_COLOR: ColorId = "blue";
const OPEN_STATUS = "Activo";
const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com";
const INTERNAL_PRICING_ALLOWED_EMAILS = [
GERARDO_EMAIL,
"areyes@gomezleemarketing.com",
"iaracena@gomezleemarketing.com",
];
const DELETE_ALLOWED_EMAILS = [
GERARDO_EMAIL,
"areyes@gomezleemarketing.com",
"iaracena@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: "",
@@ -715,6 +723,25 @@ async function replaceProjectLinks(
projectId: string,
project: Pick<Project, "propuestaLinks" | "afLinks">,
) {
const linkRows = toLinkRows(projectId, project);
try {
const { error: rpcError } = await supabase.rpc("tablero_cdc_replace_project_links", {
p_project_id: projectId,
p_links: linkRows.map(({ link_type, url, label }) => ({ link_type, url, label })),
});
if (rpcError) throw rpcError;
return;
} catch (error) {
if (!isMissingReplaceLinksRpc(error)) throw error;
console.warn(
"RPC tablero_cdc_replace_project_links no existe todavía. Usando reemplazo de links por delete+insert.",
error,
);
}
const { error: deleteError } = await supabase
.from("tablero_cdc_project_links")
.delete()
@@ -722,8 +749,6 @@ async function replaceProjectLinks(
if (deleteError) throw deleteError;
const linkRows = toLinkRows(projectId, project);
if (linkRows.length === 0) return;
const { error: insertError } = await supabase.from("tablero_cdc_project_links").insert(linkRows);
@@ -962,14 +987,40 @@ function getSelectColumns(includeInternalAmount: boolean) {
`;
}
function getErrorMessage(error: unknown) {
if (error instanceof Error) return error.message;
if (error && typeof error === "object" && "message" in error) {
return String((error as { message?: unknown }).message || "");
}
return String(error || "");
}
function isMissingRpcFunction(error: unknown) {
const message = error instanceof Error ? error.message : String(error || "");
const message = getErrorMessage(error);
return (
message.includes("tablero_cdc_get_projects_paginated") ||
message.includes("Could not find the function")
);
}
function isMissingReplaceLinksRpc(error: unknown) {
const message = getErrorMessage(error);
return (
message.includes("tablero_cdc_replace_project_links") ||
message.includes("Could not find the function")
);
}
function isMissingPricingSummaryRpc(error: unknown) {
const message = getErrorMessage(error);
return (
message.includes("tablero_cdc_get_pricing_summary") ||
message.includes("Could not find the function")
);
}
function escapePostgrestFilterValue(value: string) {
return value.replace(/[%_]/g, "\\$&").replace(/,/g, " ");
}
@@ -1084,7 +1135,7 @@ async function loadProjects(params?: Partial<ProjectQueryParams>) {
notify();
try {
const isGerardo = await currentUserIsGerardo();
const canManageInternalPricing = await currentUserCanManageInternalPricing();
let result: ProjectLoadResult;
try {
@@ -1096,7 +1147,7 @@ async function loadProjects(params?: Partial<ProjectQueryParams>) {
"RPC tablero_cdc_get_projects_paginated no existe todavía. Usando consulta directa como respaldo temporal.",
rpcError,
);
result = await loadProjectsDirect(nextParams, isGerardo);
result = await loadProjectsDirect(nextParams, canManageInternalPricing);
}
if (requestId !== loadRequestSeq) return;
@@ -1307,6 +1358,168 @@ async function deleteProject(id: string) {
}
}
async function loadProjectPricingSummaryFromRpc(
params: ProjectQueryParams,
): Promise<ProjectPricingSummary> {
const { data, error } = await supabase.rpc("tablero_cdc_get_pricing_summary", {
p_tab: params.tab,
p_search: params.search,
p_country: params.country || "",
p_brand: params.brand || "",
p_client: params.client || "",
p_cm: params.cm || "",
});
if (error) throw error;
const result = (data || {}) as RpcPricingSummaryResponse;
const totalAmount = Number(result.total_amount || 0);
const projectCount = Number(result.project_count || 0);
return {
totalAmount: Number.isFinite(totalAmount) ? totalAmount : 0,
projectCount: Number.isFinite(projectCount) ? projectCount : 0,
loadedAt: Date.now(),
};
}
async function loadProjectPricingSummaryDirect(
params: ProjectQueryParams,
): Promise<ProjectPricingSummary> {
const pageSize = 1000;
let from = 0;
let totalCount: number | null = null;
let totalAmount = 0;
while (totalCount === null || from < totalCount) {
let query = supabase
.from("tablero_cdc_projects")
.select("id, internal_amount", { count: "exact" })
.order("created_at", { ascending: false }) as unknown as DirectProjectQuery;
query = applyDirectFilters(query, params);
const { data, error, count } = await query.range(from, from + pageSize - 1);
if (error) throw error;
if (totalCount === null) {
totalCount = count ?? (data || []).length;
}
const rows = (data || []) as DirectProjectSummaryRow[];
rows.forEach((row) => {
const amount = Number(row.internal_amount ?? 0);
if (Number.isFinite(amount) && amount > 0) {
totalAmount += amount;
}
});
if (rows.length < pageSize) break;
from += pageSize;
}
return {
totalAmount,
projectCount: totalCount ?? 0,
loadedAt: Date.now(),
};
}
async function loadProjectPricingSummary(
params?: Partial<ProjectQueryParams>,
): Promise<ProjectPricingSummary> {
const nextParams = normalizeProjectParams({
...params,
page: 1,
pageSize: 1000,
});
try {
return await loadProjectPricingSummaryFromRpc(nextParams);
} catch (rpcError) {
if (!isMissingPricingSummaryRpc(rpcError)) throw rpcError;
console.warn(
"RPC tablero_cdc_get_pricing_summary no existe todavía. Usando suma directa como respaldo temporal.",
rpcError,
);
return loadProjectPricingSummaryDirect(nextParams);
}
}
export function useProjectPricingSummary(params: ProjectQueryParams, enabled: boolean) {
const [summary, setSummary] = useState<ProjectPricingSummary>({
totalAmount: 0,
projectCount: 0,
loadedAt: null,
});
const [summaryLoading, setSummaryLoading] = useState(false);
const [summaryError, setSummaryError] = useState<string | null>(null);
const paramsTab = params.tab;
const paramsSearch = params.search;
const paramsCountry = params.country;
const paramsBrand = params.brand;
const paramsClient = params.client;
const paramsCm = params.cm;
useEffect(() => {
if (!enabled) {
setSummary({ totalAmount: 0, projectCount: 0, loadedAt: null });
setSummaryError(null);
setSummaryLoading(false);
return;
}
let cancelled = false;
setSummaryLoading(true);
setSummaryError(null);
loadProjectPricingSummary({
tab: paramsTab,
search: paramsSearch,
country: paramsCountry,
brand: paramsBrand,
client: paramsClient,
cm: paramsCm,
page: 1,
pageSize: 1000,
})
.then((result) => {
if (!cancelled) setSummary(result);
})
.catch((error) => {
if (cancelled) return;
console.error("No se pudo calcular el total tarifado filtrado:", error);
setSummaryError(
error instanceof Error
? error.message
: "No se pudo calcular el total tarifado filtrado.",
);
})
.finally(() => {
if (!cancelled) setSummaryLoading(false);
});
return () => {
cancelled = true;
};
}, [enabled, paramsTab, paramsSearch, paramsCountry, paramsBrand, paramsClient, paramsCm]);
return {
summary,
loading: summaryLoading,
error: summaryError,
refresh: () =>
loadProjectPricingSummary(params).then((result) => {
setSummary(result);
return result;
}),
};
}
export function useProjects(params?: ProjectQueryParams) {
const [, force] = useState(0);
const paramsTab = params?.tab;