feat: actualizar Tablero CDC con administracion de listas
This commit is contained in:
+303
-51
@@ -4,10 +4,14 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
AlertCircle,
|
||||
BadgeDollarSign,
|
||||
DollarSign,
|
||||
Eye,
|
||||
EyeOff,
|
||||
LayoutGrid,
|
||||
ListChecks,
|
||||
Inbox,
|
||||
FileX2,
|
||||
LogOut,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -27,17 +31,30 @@ import {
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { ProjectCard } from "@/components/board/ProjectCard";
|
||||
import { ProjectDialog } from "@/components/board/ProjectDialog";
|
||||
import { ProjectTimeDialog } from "@/components/board/ProjectTimeDialog";
|
||||
import { BriefReviewCard } from "@/components/board/BriefReviewCard";
|
||||
import { SearchableSelect } from "@/components/board/SearchableSelect";
|
||||
import { useProjectPricingSummary, useProjects, type Project } from "@/lib/store";
|
||||
import {
|
||||
useProjectPricingSummary,
|
||||
useProjects,
|
||||
type Project,
|
||||
type ProjectTab,
|
||||
} from "@/lib/store";
|
||||
import { useBriefInbox, useBriefUnreadCount, type BriefReviewStatus } from "@/lib/briefInbox";
|
||||
import { usePricingSummaryVisibility } from "@/lib/pricingSummaryVisibility";
|
||||
import { useAppLists } from "@/lib/appLists";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { dedupeOptions } from "@/lib/optionUtils";
|
||||
import { expandCountryOptions, expandMultiValueOptions } from "@/lib/filterNormalization";
|
||||
import { GLMLogo } from "@/components/GLMLogo";
|
||||
import { TariffManagerDialog } from "@/components/tariff/TariffManagerDialog";
|
||||
import { ListManagerDialog } from "@/components/lists/ListManagerDialog";
|
||||
|
||||
/** Extract up-to-2-letter initials from a display name or email */
|
||||
const PROJECTS_PER_PAGE = 24;
|
||||
|
||||
type BoardView = "nuevos" | ProjectTab | "no_aprobados";
|
||||
|
||||
type AdvancedFilters = {
|
||||
country: string;
|
||||
brand: string;
|
||||
@@ -66,37 +83,61 @@ export default function BoardPage() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Project | null>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const [filter, setFilter] = useState<"todos" | "abiertos" | "cerrados">("todos");
|
||||
const [filter, setFilter] = useState<BoardView>("todos");
|
||||
const [advancedFilters, setAdvancedFilters] = useState<AdvancedFilters>(EMPTY_ADVANCED_FILTERS);
|
||||
const [pageByFilter, setPageByFilter] = useState({
|
||||
const [pageByFilter, setPageByFilter] = useState<Record<BoardView, number>>({
|
||||
nuevos: 1,
|
||||
todos: 1,
|
||||
abiertos: 1,
|
||||
cerrados: 1,
|
||||
no_aprobados: 1,
|
||||
});
|
||||
const [deletingProjectId, setDeletingProjectId] = useState<string | null>(null);
|
||||
const [tariffManagerOpen, setTariffManagerOpen] = useState(false);
|
||||
const [listManagerOpen, setListManagerOpen] = useState(false);
|
||||
const [timeProject, setTimeProject] = useState<Project | null>(null);
|
||||
const [reviewingBriefId, setReviewingBriefId] = useState<string | null>(null);
|
||||
|
||||
const isBriefReviewView = filter === "nuevos" || filter === "no_aprobados";
|
||||
const briefStatus: BriefReviewStatus = filter === "no_aprobados" ? "rejected" : "new";
|
||||
const projectFilter: ProjectTab = isBriefReviewView ? "todos" : filter;
|
||||
const currentPage = pageByFilter[filter];
|
||||
const { lists } = useAppLists();
|
||||
const { projects, totalProjects, filterOptions, loading, error, refresh, remove } = useProjects({
|
||||
tab: filter,
|
||||
tab: projectFilter,
|
||||
search: isBriefReviewView ? "" : query,
|
||||
page: isBriefReviewView ? pageByFilter[projectFilter] : currentPage,
|
||||
pageSize: PROJECTS_PER_PAGE,
|
||||
country: isBriefReviewView ? "" : advancedFilters.country,
|
||||
brand: isBriefReviewView ? "" : advancedFilters.brand,
|
||||
client: isBriefReviewView ? "" : advancedFilters.client,
|
||||
cm: isBriefReviewView ? "" : advancedFilters.cm,
|
||||
});
|
||||
const briefInbox = useBriefInbox({
|
||||
status: briefStatus,
|
||||
search: query,
|
||||
page: currentPage,
|
||||
pageSize: PROJECTS_PER_PAGE,
|
||||
country: advancedFilters.country,
|
||||
brand: advancedFilters.brand,
|
||||
client: advancedFilters.client,
|
||||
cm: advancedFilters.cm,
|
||||
enabled: isBriefReviewView,
|
||||
});
|
||||
const { user, isGerardo, canControlPricingSummary, logout } = useAuth();
|
||||
const {
|
||||
user,
|
||||
isGerardo,
|
||||
canManageInternalPricing,
|
||||
canControlPricingSummary,
|
||||
canManageTariffCatalog,
|
||||
logout,
|
||||
} = useAuth();
|
||||
const briefUnread = useBriefUnreadCount(Boolean(user));
|
||||
const pricingSummaryParams = {
|
||||
tab: filter,
|
||||
search: query,
|
||||
tab: projectFilter,
|
||||
search: isBriefReviewView ? "" : query,
|
||||
page: 1,
|
||||
pageSize: PROJECTS_PER_PAGE,
|
||||
country: advancedFilters.country,
|
||||
brand: advancedFilters.brand,
|
||||
client: advancedFilters.client,
|
||||
cm: advancedFilters.cm,
|
||||
country: isBriefReviewView ? "" : advancedFilters.country,
|
||||
brand: isBriefReviewView ? "" : advancedFilters.brand,
|
||||
client: isBriefReviewView ? "" : advancedFilters.client,
|
||||
cm: isBriefReviewView ? "" : advancedFilters.cm,
|
||||
} as const;
|
||||
const {
|
||||
isPublic: isPricingSummaryPublic,
|
||||
@@ -105,7 +146,8 @@ export default function BoardPage() {
|
||||
error: pricingSummaryVisibilityError,
|
||||
setPublicVisibility: setPricingSummaryPublicVisibility,
|
||||
} = usePricingSummaryVisibility(Boolean(user));
|
||||
const canViewPricingSummary = canControlPricingSummary || isPricingSummaryPublic;
|
||||
const canViewPricingSummary =
|
||||
!isBriefReviewView && (canControlPricingSummary || isPricingSummaryPublic);
|
||||
const {
|
||||
summary: pricingSummary,
|
||||
loading: pricingSummaryLoading,
|
||||
@@ -115,31 +157,32 @@ export default function BoardPage() {
|
||||
const cmListOptions = Object.values(lists.buCm);
|
||||
const countryFilterOptions = dedupeOptions([
|
||||
...lists.bus,
|
||||
...filterOptions.countries,
|
||||
...expandCountryOptions(filterOptions.countries, lists.bus),
|
||||
advancedFilters.country,
|
||||
]);
|
||||
const brandFilterOptions = dedupeOptions([
|
||||
...lists.marcas,
|
||||
...filterOptions.brands,
|
||||
...expandMultiValueOptions(filterOptions.brands, lists.marcas),
|
||||
advancedFilters.brand,
|
||||
]);
|
||||
const clientFilterOptions = dedupeOptions([
|
||||
...lists.clientes,
|
||||
...filterOptions.clients,
|
||||
...expandMultiValueOptions(filterOptions.clients, lists.clientes),
|
||||
advancedFilters.client,
|
||||
]);
|
||||
const cmFilterOptions = dedupeOptions([
|
||||
...cmListOptions,
|
||||
...filterOptions.cms,
|
||||
...expandMultiValueOptions(filterOptions.cms, cmListOptions),
|
||||
advancedFilters.cm,
|
||||
]);
|
||||
const activeAdvancedFilterCount = Object.values(advancedFilters).filter(Boolean).length;
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(totalProjects / PROJECTS_PER_PAGE));
|
||||
const currentTotalItems = isBriefReviewView ? briefInbox.totalItems : totalProjects;
|
||||
const totalPages = Math.max(1, Math.ceil(currentTotalItems / PROJECTS_PER_PAGE));
|
||||
const safePage = Math.min(currentPage, totalPages);
|
||||
const pageStart = (safePage - 1) * PROJECTS_PER_PAGE;
|
||||
const visibleStart = totalProjects === 0 ? 0 : pageStart + 1;
|
||||
const visibleEnd = Math.min(pageStart + PROJECTS_PER_PAGE, totalProjects);
|
||||
const visibleStart = currentTotalItems === 0 ? 0 : pageStart + 1;
|
||||
const visibleEnd = Math.min(pageStart + PROJECTS_PER_PAGE, currentTotalItems);
|
||||
|
||||
useEffect(() => {
|
||||
setPageByFilter((prev) => ({ ...prev, [filter]: 1 }));
|
||||
@@ -158,6 +201,18 @@ export default function BoardPage() {
|
||||
}
|
||||
}, [currentPage, filter, totalPages]);
|
||||
|
||||
// Entering "Nuevos" clears only this user's pending notification. If a new
|
||||
// brief arrives while this view is open, Realtime increments unreadCount and
|
||||
// this effect immediately marks that currently visible queue as seen again.
|
||||
// Other teammates keep their own independent unread state.
|
||||
useEffect(() => {
|
||||
if (filter !== "nuevos" || !user || briefUnread.unreadCount <= 0) return;
|
||||
|
||||
void briefUnread.markAllSeen().catch((markError) => {
|
||||
console.error("Error marcando briefs como vistos:", markError);
|
||||
});
|
||||
}, [filter, user?.id, briefUnread.unreadCount, briefUnread.markAllSeen]);
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
const nextPage = Math.min(Math.max(page, 1), totalPages);
|
||||
|
||||
@@ -212,6 +267,61 @@ export default function BoardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const approveBrief = async (briefId: string, title: string, fromRejected = false) => {
|
||||
const confirmed = window.confirm(
|
||||
fromRejected
|
||||
? `¿Aprobar el brief rechazado "${title || "Sin título"}"?\n\nSe recuperará de Rechazados, se creará como proyecto Activo y aparecerá en Todos y Activos.`
|
||||
: `¿Aprobar el brief "${title || "Sin título"}"?\n\nSe creará como proyecto Activo y aparecerá en Todos y Activos.`,
|
||||
);
|
||||
|
||||
if (!confirmed || reviewingBriefId) return;
|
||||
|
||||
setReviewingBriefId(briefId);
|
||||
|
||||
try {
|
||||
const result = await briefInbox.approve(briefId);
|
||||
await refresh();
|
||||
|
||||
if (result.sheetSyncStatus === "synced") {
|
||||
window.alert("Brief aprobado. El proyecto quedó Activo y sincronizado con Google Sheet.");
|
||||
} else {
|
||||
window.alert(result.sheetSyncMessage);
|
||||
}
|
||||
} catch (reviewError) {
|
||||
console.error("Error aprobando brief:", reviewError);
|
||||
window.alert(
|
||||
reviewError instanceof Error
|
||||
? reviewError.message
|
||||
: "No se pudo aprobar el brief. Revisa Supabase e intenta de nuevo.",
|
||||
);
|
||||
} finally {
|
||||
setReviewingBriefId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const rejectBrief = async (briefId: string, title: string) => {
|
||||
const confirmed = window.confirm(
|
||||
`¿Mover el brief "${title || "Sin título"}" a Rechazados?\n\nNo se creará ningún proyecto en el tablero general.`,
|
||||
);
|
||||
|
||||
if (!confirmed || reviewingBriefId) return;
|
||||
|
||||
setReviewingBriefId(briefId);
|
||||
|
||||
try {
|
||||
await briefInbox.reject(briefId);
|
||||
} catch (reviewError) {
|
||||
console.error("Error marcando brief como rechazado:", reviewError);
|
||||
window.alert(
|
||||
reviewError instanceof Error
|
||||
? reviewError.message
|
||||
: "No se pudo mover el brief a Rechazados. Revisa Supabase e intenta de nuevo.",
|
||||
);
|
||||
} finally {
|
||||
setReviewingBriefId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const displayName = user?.displayName ?? user?.email ?? "Usuario";
|
||||
const userRole = isGerardo ? "Jefe CDC" : "Usuario CDC";
|
||||
const userInitials = initials(user?.displayName, user?.email);
|
||||
@@ -225,7 +335,7 @@ export default function BoardPage() {
|
||||
<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>
|
||||
<h1 className="text-[15px] font-semibold tracking-tight text-[#4F758B]">CDC Project Management</h1>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
@@ -234,7 +344,7 @@ export default function BoardPage() {
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Buscar proyecto, cliente, marca…"
|
||||
placeholder={isBriefReviewView ? "Buscar brief, cliente, marca…" : "Buscar proyecto, cliente, marca…"}
|
||||
className="pl-9 h-9 bg-muted/40 border-transparent focus-visible:bg-card focus-visible:border-border"
|
||||
/>
|
||||
</div>
|
||||
@@ -286,6 +396,30 @@ export default function BoardPage() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Tariff administration — controlled by Supabase permission */}
|
||||
{canManageTariffCatalog && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setTariffManagerOpen(true)}
|
||||
className="gap-1.5 h-9 rounded-full px-4 shadow-sm"
|
||||
>
|
||||
<BadgeDollarSign className="w-4 h-4" /> Tarifario
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* App lists administration — same trusted admin permission as Tarifario */}
|
||||
{canManageTariffCatalog && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setListManagerOpen(true)}
|
||||
className="gap-1.5 h-9 rounded-full px-4 shadow-sm"
|
||||
>
|
||||
<ListChecks className="w-4 h-4" /> Listas
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* New project button */}
|
||||
<Button onClick={openNew} className="gap-1.5 h-9 rounded-full px-4 shadow-sm">
|
||||
<Plus className="w-4 h-4" /> Nuevo
|
||||
@@ -297,28 +431,54 @@ export default function BoardPage() {
|
||||
<div className="max-w-[1400px] mx-auto px-6 pt-6 pb-3 space-y-3">
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h2 className="pl-3 text-2xl font-semibold tracking-tight">Proyectos</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
{filter === "nuevos"
|
||||
? "Briefs por revisar"
|
||||
: filter === "no_aprobados"
|
||||
? "Briefs rechazados"
|
||||
: "Proyectos"}
|
||||
</h2>
|
||||
{filter === "nuevos" && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Los briefs de CDC Brief llegan aquí antes de convertirse en proyectos Activos.
|
||||
</p>
|
||||
)}
|
||||
{filter === "no_aprobados" && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Briefs revisados que no se incorporaron al tablero general.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="inline-flex bg-muted/60 rounded-full p-1 text-xs">
|
||||
<div className="inline-flex flex-wrap bg-muted/60 rounded-full p-1 text-xs">
|
||||
{(
|
||||
[
|
||||
["todos", "Todos"],
|
||||
["abiertos", "Activos"],
|
||||
["cerrados", "Cerrados"],
|
||||
["nuevos", "Nuevos"],
|
||||
["no_aprobados", "Rechazados"],
|
||||
] as const
|
||||
).map(([k, l]) => (
|
||||
<button
|
||||
key={k}
|
||||
onClick={() => setFilter(k)}
|
||||
className={`px-3.5 py-1.5 rounded-full transition-colors font-medium ${
|
||||
className={`inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full transition-colors font-medium ${
|
||||
filter === k
|
||||
? "bg-card text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{l}
|
||||
<span>{l}</span>
|
||||
{k === "nuevos" && briefUnread.unreadCount > 0 && (
|
||||
<span
|
||||
className="inline-flex min-w-[18px] h-[18px] items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-bold leading-none text-white shadow-sm"
|
||||
aria-label={`${briefUnread.unreadCount} briefs nuevos sin revisar`}
|
||||
>
|
||||
{briefUnread.unreadCount > 99 ? "99+" : briefUnread.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -330,21 +490,23 @@ export default function BoardPage() {
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Buscar proyecto, cliente, marca…"
|
||||
placeholder={isBriefReviewView ? "Buscar brief, cliente, marca…" : "Buscar proyecto, cliente, marca…"}
|
||||
className="pl-9 h-9 bg-muted/40 border-transparent focus-visible:bg-card focus-visible:border-border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdvancedProjectFilters
|
||||
filters={advancedFilters}
|
||||
activeCount={activeAdvancedFilterCount}
|
||||
countryOptions={countryFilterOptions}
|
||||
brandOptions={brandFilterOptions}
|
||||
clientOptions={clientFilterOptions}
|
||||
cmOptions={cmFilterOptions}
|
||||
onFilterChange={setAdvancedFilter}
|
||||
onClear={clearAdvancedFilters}
|
||||
/>
|
||||
{!isBriefReviewView && (
|
||||
<AdvancedProjectFilters
|
||||
filters={advancedFilters}
|
||||
activeCount={activeAdvancedFilterCount}
|
||||
countryOptions={countryFilterOptions}
|
||||
brandOptions={brandFilterOptions}
|
||||
clientOptions={clientFilterOptions}
|
||||
cmOptions={cmFilterOptions}
|
||||
onFilterChange={setAdvancedFilter}
|
||||
onClear={clearAdvancedFilters}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canViewPricingSummary && (
|
||||
<PricingSummaryCard
|
||||
@@ -367,50 +529,86 @@ export default function BoardPage() {
|
||||
|
||||
{/* Grid */}
|
||||
<main className="max-w-[1400px] mx-auto px-6 pb-16">
|
||||
{error && (
|
||||
{(isBriefReviewView ? briefInbox.error : error) && (
|
||||
<div className="mb-4 flex flex-col gap-3 rounded-2xl border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive sm:flex-row sm:items-center sm:justify-between">
|
||||
<span className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<span className="font-medium">No se pudieron cargar los proyectos.</span> {error}
|
||||
<span className="font-medium">
|
||||
{isBriefReviewView
|
||||
? "No se pudieron cargar los briefs."
|
||||
: "No se pudieron cargar los proyectos."}
|
||||
</span>{" "}
|
||||
{isBriefReviewView ? briefInbox.error : error}
|
||||
</span>
|
||||
</span>
|
||||
<Button variant="outline" size="sm" onClick={() => void refresh()} className="gap-1.5">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void (isBriefReviewView ? briefInbox.refresh() : refresh())}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" /> Reintentar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && projects.length === 0 ? (
|
||||
{isBriefReviewView ? (
|
||||
briefInbox.loading && briefInbox.items.length === 0 ? (
|
||||
<LoadingState />
|
||||
) : briefInbox.items.length === 0 ? (
|
||||
<BriefEmptyState mode={briefStatus} query={query} />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{briefInbox.items.map((item) => (
|
||||
<BriefReviewCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
mode={briefStatus}
|
||||
busy={reviewingBriefId === item.id}
|
||||
onApprove={() => void approveBrief(item.id, item.title, briefStatus === "rejected")}
|
||||
onReject={() => void rejectBrief(item.id, item.title)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : loading && projects.length === 0 ? (
|
||||
<LoadingState />
|
||||
) : projects.length === 0 ? (
|
||||
<EmptyState
|
||||
onNew={openNew}
|
||||
hasProjects={
|
||||
totalProjects > 0 ||
|
||||
filter !== "todos" ||
|
||||
projectFilter !== "todos" ||
|
||||
query.trim().length > 0 ||
|
||||
activeAdvancedFilterCount > 0
|
||||
}
|
||||
filter={filter}
|
||||
filter={projectFilter}
|
||||
query={query}
|
||||
activeAdvancedFilterCount={activeAdvancedFilterCount}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{projects.map((p) => (
|
||||
<ProjectCard key={p.id} project={p} onClick={() => openEdit(p)} />
|
||||
<ProjectCard
|
||||
key={p.id}
|
||||
project={p}
|
||||
onClick={() => openEdit(p)}
|
||||
onTimeClick={() => setTimeProject(p)}
|
||||
showInternalAmount={canManageInternalPricing}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalProjects > PROJECTS_PER_PAGE && (
|
||||
{currentTotalItems > PROJECTS_PER_PAGE && (
|
||||
<PaginationControls
|
||||
currentPage={safePage}
|
||||
totalPages={totalPages}
|
||||
visibleStart={visibleStart}
|
||||
visibleEnd={visibleEnd}
|
||||
totalItems={totalProjects}
|
||||
totalItems={currentTotalItems}
|
||||
itemLabel={isBriefReviewView ? "briefs" : "proyectos"}
|
||||
onPageChange={goToPage}
|
||||
/>
|
||||
)}
|
||||
@@ -423,6 +621,27 @@ export default function BoardPage() {
|
||||
onDelete={deleteProject}
|
||||
deleting={!!editing && deletingProjectId === editing.id}
|
||||
/>
|
||||
|
||||
<ProjectTimeDialog
|
||||
open={!!timeProject}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) setTimeProject(null);
|
||||
}}
|
||||
project={timeProject}
|
||||
/>
|
||||
|
||||
{canManageTariffCatalog && (
|
||||
<>
|
||||
<TariffManagerDialog
|
||||
open={tariffManagerOpen}
|
||||
onOpenChange={setTariffManagerOpen}
|
||||
/>
|
||||
<ListManagerDialog
|
||||
open={listManagerOpen}
|
||||
onOpenChange={setListManagerOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -632,6 +851,7 @@ function PaginationControls({
|
||||
visibleStart,
|
||||
visibleEnd,
|
||||
totalItems,
|
||||
itemLabel = "proyectos",
|
||||
onPageChange,
|
||||
}: {
|
||||
currentPage: number;
|
||||
@@ -639,6 +859,7 @@ function PaginationControls({
|
||||
visibleStart: number;
|
||||
visibleEnd: number;
|
||||
totalItems: number;
|
||||
itemLabel?: string;
|
||||
onPageChange: (page: number) => void;
|
||||
}) {
|
||||
const pages = getVisiblePages(currentPage, totalPages);
|
||||
@@ -649,7 +870,7 @@ function PaginationControls({
|
||||
<span className="font-medium text-foreground">
|
||||
{visibleStart}-{visibleEnd}
|
||||
</span>{" "}
|
||||
de <span className="font-medium text-foreground">{totalItems}</span> proyectos
|
||||
de <span className="font-medium text-foreground">{totalItems}</span> {itemLabel}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -757,6 +978,37 @@ function LoadingState() {
|
||||
);
|
||||
}
|
||||
|
||||
function BriefEmptyState({ mode, query }: { mode: BriefReviewStatus; query: string }) {
|
||||
const hasSearch = query.trim().length > 0;
|
||||
const isRejected = mode === "rejected";
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-border bg-card/40 py-24 text-center">
|
||||
<div
|
||||
className={`mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl ${
|
||||
isRejected ? "bg-red-50 text-red-600" : "bg-amber-50 text-amber-700"
|
||||
}`}
|
||||
>
|
||||
{isRejected ? <FileX2 className="h-6 w-6" /> : <Inbox className="h-6 w-6" />}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{hasSearch
|
||||
? "No hay briefs que coincidan con la búsqueda"
|
||||
: isRejected
|
||||
? "No hay briefs rechazados"
|
||||
: "No hay briefs pendientes"}
|
||||
</h3>
|
||||
<p className="mx-auto mt-1 max-w-md text-sm text-muted-foreground">
|
||||
{hasSearch
|
||||
? "Prueba con otro nombre, cliente, marca, país o solicitante."
|
||||
: isRejected
|
||||
? "Los briefs que el equipo decida no convertir en proyecto aparecerán aquí."
|
||||
: "Cuando CDC Brief genere un nuevo brief, aparecerá aquí automáticamente para revisión."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({
|
||||
onNew,
|
||||
hasProjects,
|
||||
@@ -766,7 +1018,7 @@ function EmptyState({
|
||||
}: {
|
||||
onNew: () => void;
|
||||
hasProjects: boolean;
|
||||
filter: "todos" | "abiertos" | "cerrados";
|
||||
filter: ProjectTab;
|
||||
query: string;
|
||||
activeAdvancedFilterCount: number;
|
||||
}) {
|
||||
|
||||
Reference in New Issue
Block a user