feat: agregar control de visibilidad del total tarifado
This commit is contained in:
+228
-15
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user