feat: agregar control de visibilidad del total tarifado
This commit is contained in:
+142
-2
@@ -4,6 +4,9 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
AlertCircle,
|
||||
DollarSign,
|
||||
Eye,
|
||||
EyeOff,
|
||||
LayoutGrid,
|
||||
LogOut,
|
||||
Plus,
|
||||
@@ -25,7 +28,8 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { ProjectCard } from "@/components/board/ProjectCard";
|
||||
import { ProjectDialog } from "@/components/board/ProjectDialog";
|
||||
import { SearchableSelect } from "@/components/board/SearchableSelect";
|
||||
import { useProjects, type Project } from "@/lib/store";
|
||||
import { useProjectPricingSummary, useProjects, type Project } from "@/lib/store";
|
||||
import { usePricingSummaryVisibility } from "@/lib/pricingSummaryVisibility";
|
||||
import { useAppLists } from "@/lib/appLists";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { dedupeOptions } from "@/lib/optionUtils";
|
||||
@@ -82,7 +86,30 @@ export default function BoardPage() {
|
||||
client: advancedFilters.client,
|
||||
cm: advancedFilters.cm,
|
||||
});
|
||||
const { user, isGerardo, logout } = useAuth();
|
||||
const { user, isGerardo, canControlPricingSummary, logout } = useAuth();
|
||||
const pricingSummaryParams = {
|
||||
tab: filter,
|
||||
search: query,
|
||||
page: 1,
|
||||
pageSize: PROJECTS_PER_PAGE,
|
||||
country: advancedFilters.country,
|
||||
brand: advancedFilters.brand,
|
||||
client: advancedFilters.client,
|
||||
cm: advancedFilters.cm,
|
||||
} as const;
|
||||
const {
|
||||
isPublic: isPricingSummaryPublic,
|
||||
loading: pricingSummaryVisibilityLoading,
|
||||
saving: pricingSummaryVisibilitySaving,
|
||||
error: pricingSummaryVisibilityError,
|
||||
setPublicVisibility: setPricingSummaryPublicVisibility,
|
||||
} = usePricingSummaryVisibility(Boolean(user));
|
||||
const canViewPricingSummary = canControlPricingSummary || isPricingSummaryPublic;
|
||||
const {
|
||||
summary: pricingSummary,
|
||||
loading: pricingSummaryLoading,
|
||||
error: pricingSummaryError,
|
||||
} = useProjectPricingSummary(pricingSummaryParams, canViewPricingSummary);
|
||||
|
||||
const cmListOptions = Object.values(lists.buCm);
|
||||
const countryFilterOptions = dedupeOptions([
|
||||
@@ -321,6 +348,24 @@ export default function BoardPage() {
|
||||
onFilterChange={setAdvancedFilter}
|
||||
onClear={clearAdvancedFilters}
|
||||
/>
|
||||
|
||||
{canViewPricingSummary && (
|
||||
<PricingSummaryCard
|
||||
totalAmount={pricingSummary.totalAmount}
|
||||
projectCount={pricingSummary.projectCount}
|
||||
loading={pricingSummaryLoading || pricingSummaryVisibilityLoading}
|
||||
error={pricingSummaryError || pricingSummaryVisibilityError}
|
||||
hasFilters={
|
||||
filter !== "todos" || query.trim().length > 0 || activeAdvancedFilterCount > 0
|
||||
}
|
||||
canControlVisibility={canControlPricingSummary}
|
||||
isPublic={isPricingSummaryPublic}
|
||||
visibilitySaving={pricingSummaryVisibilitySaving}
|
||||
onToggleVisibility={() =>
|
||||
setPricingSummaryPublicVisibility(!isPricingSummaryPublic).catch(() => undefined)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
@@ -385,6 +430,101 @@ export default function BoardPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value || 0);
|
||||
}
|
||||
|
||||
function PricingSummaryCard({
|
||||
totalAmount,
|
||||
projectCount,
|
||||
loading,
|
||||
error,
|
||||
hasFilters,
|
||||
canControlVisibility,
|
||||
isPublic,
|
||||
visibilitySaving,
|
||||
onToggleVisibility,
|
||||
}: {
|
||||
totalAmount: number;
|
||||
projectCount: number;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
hasFilters: boolean;
|
||||
canControlVisibility: boolean;
|
||||
isPublic: boolean;
|
||||
visibilitySaving: boolean;
|
||||
onToggleVisibility: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-primary/15 bg-primary/5 px-4 py-3 shadow-sm">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="mt-0.5 flex h-9 w-9 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
||||
<DollarSign className="h-4 w-4" />
|
||||
</span>
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{hasFilters ? "Total tarifado filtrado" : "Total tarifado global"}
|
||||
</p>
|
||||
<span
|
||||
className={`rounded-full border px-2 py-0.5 text-[10px] font-medium ${
|
||||
isPublic
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
||||
: "border-primary/20 bg-card text-primary"
|
||||
}`}
|
||||
>
|
||||
{isPublic ? "Visible para todos" : "Privado"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 text-2xl font-semibold tracking-tight text-foreground">
|
||||
{loading ? "Calculando…" : formatCurrency(totalAmount)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{error
|
||||
? `No se pudo calcular el total: ${error}`
|
||||
: `${projectCount} proyecto(s) incluidos según los filtros actuales.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center lg:justify-end">
|
||||
<div className="rounded-full border border-primary/20 bg-card px-3 py-1 text-xs font-medium text-primary">
|
||||
{hasFilters ? "Según filtros activos" : "Sin filtros aplicados"}
|
||||
</div>
|
||||
|
||||
{canControlVisibility && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={isPublic ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
className="gap-1.5 rounded-full"
|
||||
disabled={visibilitySaving}
|
||||
onClick={onToggleVisibility}
|
||||
title={
|
||||
isPublic
|
||||
? "Ocultar este total para los demás usuarios"
|
||||
: "Mostrar este total temporalmente a todos los usuarios"
|
||||
}
|
||||
>
|
||||
{isPublic ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
{visibilitySaving
|
||||
? "Actualizando…"
|
||||
: isPublic
|
||||
? "Ocultar a todos"
|
||||
: "Mostrar a todos"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdvancedProjectFilters({
|
||||
filters,
|
||||
activeCount,
|
||||
|
||||
Reference in New Issue
Block a user