novas atualizações na exportacao planilha, tela de listar fechamento, filtros
This commit is contained in:
+10
-4
@@ -11,13 +11,18 @@ import React from "react";
|
|||||||
import IntelligenceIAApp from "./modules/intelligence-ia/App";
|
import IntelligenceIAApp from "./modules/intelligence-ia/App";
|
||||||
import FechamentoHgtxApp from "./modules/fechamento-hgtx/App";
|
import FechamentoHgtxApp from "./modules/fechamento-hgtx/App";
|
||||||
|
|
||||||
|
const INTELLIGENCE_SCORE_PREFIX = "/intelligence-score";
|
||||||
const FECHAMENTO_LEGACY_PREFIX = "/fechamento-hgtx";
|
const FECHAMENTO_LEGACY_PREFIX = "/fechamento-hgtx";
|
||||||
|
const FECHAMENTO_PREFIX = "/fechamento";
|
||||||
|
|
||||||
/** Redireciona URLs antigas `/fechamento-hgtx/...` para `/fechamento/...` (mesmo sufixo, query e hash). */
|
/** Redireciona URLs antigas para `/intelligence-score/...` (mesmo sufixo, query e hash). */
|
||||||
function FechamentoLegacyRedirect() {
|
function FechamentoLegacyRedirect() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const tail = location.pathname.slice(FECHAMENTO_LEGACY_PREFIX.length);
|
const fromPrefix = location.pathname.startsWith(FECHAMENTO_LEGACY_PREFIX)
|
||||||
const to = `/fechamento${tail}`;
|
? FECHAMENTO_LEGACY_PREFIX
|
||||||
|
: FECHAMENTO_PREFIX;
|
||||||
|
const tail = location.pathname.slice(fromPrefix.length);
|
||||||
|
const to = `${INTELLIGENCE_SCORE_PREFIX}${tail}`;
|
||||||
return <Navigate to={`${to}${location.search}${location.hash}`} replace />;
|
return <Navigate to={`${to}${location.search}${location.hash}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +49,8 @@ const App = () => (
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/intelligence-ia/*" element={<IntelligenceIAApp />} />
|
<Route path="/intelligence-ia/*" element={<IntelligenceIAApp />} />
|
||||||
<Route path="/fechamento/*" element={<FechamentoHgtxApp />} />
|
<Route path="/intelligence-score/*" element={<FechamentoHgtxApp />} />
|
||||||
|
<Route path="/fechamento/*" element={<FechamentoLegacyRedirect />} />
|
||||||
<Route path="/fechamento-hgtx/*" element={<FechamentoLegacyRedirect />} />
|
<Route path="/fechamento-hgtx/*" element={<FechamentoLegacyRedirect />} />
|
||||||
|
|
||||||
<Route path="/*" element={<Index />} />
|
<Route path="/*" element={<Index />} />
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function UnidadeGate({ children }: UnidadeGateProps) {
|
|||||||
<CardContent className="space-y-4 text-center text-muted-foreground">
|
<CardContent className="space-y-4 text-center text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
O código do estabelecimento não foi enviado pelo ambiente (TransferArea). Abra o módulo
|
O código do estabelecimento não foi enviado pelo ambiente (TransferArea). Abra o módulo
|
||||||
Fechamento HGTX a partir do Codex com o estabelecimento carregado no transfer.
|
HGTX Intelligence Score a partir do Codex com o estabelecimento carregado no transfer.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -134,7 +134,7 @@ export function UnidadeGate({ children }: UnidadeGateProps) {
|
|||||||
Cadastre o nome da unidade em Configurações (integração com o código atual do transfer).
|
Cadastre o nome da unidade em Configurações (integração com o código atual do transfer).
|
||||||
</p>
|
</p>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to="/fechamento/configuracoes#unidade">Ir para Configurações</Link>
|
<Link to="/intelligence-score/configuracoes#unidade">Ir para Configurações</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function RequireAdminRoute({ children }: { children: JSX.Element }) {
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Navigate to="/fechamento/meu-fechamento" replace />;
|
return <Navigate to="/intelligence-score/meu-fechamento" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequireFechamentoDetalheRoute({ children }: { children: JSX.Element }) {
|
function RequireFechamentoDetalheRoute({ children }: { children: JSX.Element }) {
|
||||||
@@ -39,7 +39,7 @@ function RequireFechamentoDetalheRoute({ children }: { children: JSX.Element })
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Navigate to="/fechamento/meu-fechamento" replace />;
|
return <Navigate to="/intelligence-score/meu-fechamento" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FechamentoHgtxApp = () => {
|
const FechamentoHgtxApp = () => {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function AppSidebar() {
|
|||||||
<ClipboardList className="h-5 w-5 text-primary" />
|
<ClipboardList className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 animate-fade-in">
|
<div className="min-w-0 animate-fade-in">
|
||||||
<h1 className="truncate text-lg font-semibold text-sidebar-foreground">Fechamento HGTX</h1>
|
<h1 className="truncate text-lg font-semibold text-sidebar-foreground">HGTX Intelligence Score</h1>
|
||||||
<p className="text-xs text-muted-foreground">{isAdmin ? "Painel Admin" : "Painel Parceiro"}</p>
|
<p className="text-xs text-muted-foreground">{isAdmin ? "Painel Admin" : "Painel Parceiro"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export default function BancoPontos() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-border bg-background text-foreground hover:bg-muted/50 hover:text-foreground"
|
className="border-border bg-background text-foreground hover:bg-muted/50 hover:text-foreground"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(`/fechamento/banco-pontos/${row.parceiroId}`, {
|
navigate(`/intelligence-score/banco-pontos/${row.parceiroId}`, {
|
||||||
state: { nomeExibicao: getDisplayNome(row) },
|
state: { nomeExibicao: getDisplayNome(row) },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function BancoPontosExtrato() {
|
|||||||
<div className="flex h-full flex-col bg-background pb-16 md:pb-0">
|
<div className="flex h-full flex-col bg-background pb-16 md:pb-0">
|
||||||
<div className="border-b border-border bg-muted/20 p-3 md:p-6">
|
<div className="border-b border-border bg-muted/20 p-3 md:p-6">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Button variant="ghost" size="sm" onClick={() => navigate("/fechamento/banco-pontos")}>
|
<Button variant="ghost" size="sm" onClick={() => navigate("/intelligence-score/banco-pontos")}>
|
||||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||||
Voltar ao banco de pontos
|
Voltar ao banco de pontos
|
||||||
</Button>
|
</Button>
|
||||||
@@ -275,7 +275,7 @@ export default function BancoPontosExtrato() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="gap-1 text-foreground hover:bg-zinc-200 hover:text-foreground dark:hover:bg-zinc-700"
|
className="gap-1 text-foreground hover:bg-zinc-200 hover:text-foreground dark:hover:bg-zinc-700"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(`/fechamento/fechamentos/${linha.fechamentoId}`, {
|
navigate(`/intelligence-score/fechamentos/${linha.fechamentoId}`, {
|
||||||
state: { competenciaId: undefined, status: undefined },
|
state: { competenciaId: undefined, status: undefined },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,15 +36,6 @@ function getDisplayNome(row: FechamentoDaCompetenciaItem): string {
|
|||||||
return row.parceiroNome;
|
return row.parceiroNome;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHoras(minutos: number | null): string {
|
|
||||||
if (!minutos || minutos <= 0) return "0h";
|
|
||||||
const horas = Math.floor(minutos / 60);
|
|
||||||
const mins = Math.round(minutos % 60);
|
|
||||||
if (horas === 0) return `${mins}min`;
|
|
||||||
if (mins === 0) return `${horas}h`;
|
|
||||||
return `${horas}h ${mins}min`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AsanaImportLoadingCard() {
|
function AsanaImportLoadingCard() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -258,7 +249,7 @@ export default function CompetenciaFechamentos() {
|
|||||||
<div className="flex h-full flex-col bg-background pb-16 md:pb-0">
|
<div className="flex h-full flex-col bg-background pb-16 md:pb-0">
|
||||||
<div className="border-b border-border p-3 md:p-6">
|
<div className="border-b border-border p-3 md:p-6">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<Button variant="ghost" size="sm" onClick={() => navigate("/fechamento")}>
|
<Button variant="ghost" size="sm" onClick={() => navigate("/intelligence-score")}>
|
||||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||||
Voltar
|
Voltar
|
||||||
</Button>
|
</Button>
|
||||||
@@ -334,7 +325,6 @@ export default function CompetenciaFechamentos() {
|
|||||||
<TableHead className="min-w-[100px]">Logo</TableHead>
|
<TableHead className="min-w-[100px]">Logo</TableHead>
|
||||||
<TableHead className="min-w-[280px]">Nome</TableHead>
|
<TableHead className="min-w-[280px]">Nome</TableHead>
|
||||||
<TableHead className="min-w-[140px]">Pontuação Total</TableHead>
|
<TableHead className="min-w-[140px]">Pontuação Total</TableHead>
|
||||||
<TableHead className="min-w-[120px]">Horas Total</TableHead>
|
|
||||||
<TableHead className="min-w-[120px]">Status</TableHead>
|
<TableHead className="min-w-[120px]">Status</TableHead>
|
||||||
<TableHead className="text-center">Ações</TableHead>
|
<TableHead className="text-center">Ações</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -342,7 +332,7 @@ export default function CompetenciaFechamentos() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="py-16">
|
<TableCell colSpan={5} className="py-16">
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center justify-center gap-4 text-center"
|
className="flex flex-col items-center justify-center gap-4 text-center"
|
||||||
role="status"
|
role="status"
|
||||||
@@ -372,7 +362,6 @@ export default function CompetenciaFechamentos() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-medium">{getDisplayNome(row)}</TableCell>
|
<TableCell className="font-medium">{getDisplayNome(row)}</TableCell>
|
||||||
<TableCell>{row.pontuacaoTotalEntregue}</TableCell>
|
<TableCell>{row.pontuacaoTotalEntregue}</TableCell>
|
||||||
<TableCell>{formatHoras(row.horasTotal * 60)}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
className={
|
className={
|
||||||
@@ -402,7 +391,7 @@ export default function CompetenciaFechamentos() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="transition-colors hover:bg-muted/50 hover:text-foreground"
|
className="transition-colors hover:bg-muted/50 hover:text-foreground"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(`/fechamento/fechamentos/${row.id}`, {
|
navigate(`/intelligence-score/fechamentos/${row.id}`, {
|
||||||
state: { competenciaId, status: row.status },
|
state: { competenciaId, status: row.status },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export default function Configuracoes() {
|
|||||||
await fechamentoUnidadesService.createUnidade({ nome, estabelecimentoId: codigo });
|
await fechamentoUnidadesService.createUnidade({ nome, estabelecimentoId: codigo });
|
||||||
toast.success("Unidade cadastrada.");
|
toast.success("Unidade cadastrada.");
|
||||||
clearCommanderUnidadeIdCache();
|
clearCommanderUnidadeIdCache();
|
||||||
navigate("/fechamento", { replace: true });
|
navigate("/intelligence-score", { replace: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Erro ao salvar unidade.";
|
const message = error instanceof Error ? error.message : "Erro ao salvar unidade.";
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock3,
|
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -41,23 +40,6 @@ import { fechamentoBancoPontosService } from "@/services/fechamento/bancoPontos"
|
|||||||
import { fechamentoCompetenciasService, type CompetenciaItem } from "@/services/fechamento/competencias";
|
import { fechamentoCompetenciasService, type CompetenciaItem } from "@/services/fechamento/competencias";
|
||||||
import { fechamentoFechamentosService, type FechamentoTarefaItem } from "@/services/fechamento/fechamentos";
|
import { fechamentoFechamentosService, type FechamentoTarefaItem } from "@/services/fechamento/fechamentos";
|
||||||
|
|
||||||
function formatHoras(minutos: number | null): string {
|
|
||||||
if (!minutos || minutos <= 0) return "0h";
|
|
||||||
const horas = Math.floor(minutos / 60);
|
|
||||||
const mins = Math.round(minutos % 60);
|
|
||||||
if (horas === 0) return `${mins}min`;
|
|
||||||
if (mins === 0) return `${horas}h`;
|
|
||||||
return `${horas}h ${mins}min`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatHorasResumo(minutos: number): string {
|
|
||||||
const horas = Math.floor(minutos / 60);
|
|
||||||
const mins = Math.round(minutos % 60);
|
|
||||||
if (horas === 0) return `${mins}min`;
|
|
||||||
if (mins === 0) return `${horas}h`;
|
|
||||||
return `${horas}h ${mins}min`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPontos(valor: number): string {
|
function formatPontos(valor: number): string {
|
||||||
return valor.toLocaleString("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 4 });
|
return valor.toLocaleString("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 4 });
|
||||||
}
|
}
|
||||||
@@ -146,10 +128,33 @@ function formatDateTime(value: string | null): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTextFilter(value: string): string {
|
||||||
|
return value.trim().toLocaleLowerCase("pt-BR");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFilterDate(value: string | null): string {
|
||||||
|
if (!value) return "";
|
||||||
|
const isoMatch = value.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
if (isoMatch) return isoMatch[0];
|
||||||
|
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return "";
|
||||||
|
const year = parsed.getFullYear();
|
||||||
|
const month = String(parsed.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(parsed.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatCompetenciaMesAno(item: Pick<CompetenciaItem, "mes" | "ano">): string {
|
function formatCompetenciaMesAno(item: Pick<CompetenciaItem, "mes" | "ano">): string {
|
||||||
return `${String(item.mes).padStart(2, "0")}/${item.ano}`;
|
return `${String(item.mes).padStart(2, "0")}/${item.ano}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTipoLancamentoLabel(tipo: string): string {
|
||||||
|
if (tipo === "bonus") return "Bônus Comercial";
|
||||||
|
if (tipo === "desconto") return "Desconto Comercial";
|
||||||
|
return tipo;
|
||||||
|
}
|
||||||
|
|
||||||
type FechamentoDetalhesLocationState = {
|
type FechamentoDetalhesLocationState = {
|
||||||
competenciaId?: string;
|
competenciaId?: string;
|
||||||
status?: "em_aberto" | "fechado";
|
status?: "em_aberto" | "fechado";
|
||||||
@@ -206,6 +211,15 @@ export default function FechamentoDetalhes() {
|
|||||||
const [edicaoCliente, setEdicaoCliente] = useState("");
|
const [edicaoCliente, setEdicaoCliente] = useState("");
|
||||||
const [edicaoTempoMinutos, setEdicaoTempoMinutos] = useState("");
|
const [edicaoTempoMinutos, setEdicaoTempoMinutos] = useState("");
|
||||||
const [edicaoPontuacao, setEdicaoPontuacao] = useState("");
|
const [edicaoPontuacao, setEdicaoPontuacao] = useState("");
|
||||||
|
const tableScrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const stickyScrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const stickyScrollSpacerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [showStickyScrollbar, setShowStickyScrollbar] = useState(false);
|
||||||
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
const [filtroDescricao, setFiltroDescricao] = useState("");
|
||||||
|
const [filtroTicket, setFiltroTicket] = useState("");
|
||||||
|
const [filtroCliente, setFiltroCliente] = useState("");
|
||||||
|
const [filtroDataConclusao, setFiltroDataConclusao] = useState("");
|
||||||
const [competenciaStatus, setCompetenciaStatus] = useState<"em_aberto" | "concluido">("em_aberto");
|
const [competenciaStatus, setCompetenciaStatus] = useState<"em_aberto" | "concluido">("em_aberto");
|
||||||
const isFechado = fechamentoStatus === "fechado";
|
const isFechado = fechamentoStatus === "fechado";
|
||||||
const isCompetenciaConcluida = competenciaStatus === "concluido";
|
const isCompetenciaConcluida = competenciaStatus === "concluido";
|
||||||
@@ -215,10 +229,8 @@ export default function FechamentoDetalhes() {
|
|||||||
const totais = useMemo(() => {
|
const totais = useMemo(() => {
|
||||||
const aprovadas = tarefas.filter((t) => t.estaRevisada);
|
const aprovadas = tarefas.filter((t) => t.estaRevisada);
|
||||||
const pontos = aprovadas.reduce((acc, t) => acc + Number(t.pontuacao || 0), 0);
|
const pontos = aprovadas.reduce((acc, t) => acc + Number(t.pontuacao || 0), 0);
|
||||||
const minutos = aprovadas.reduce((acc, t) => acc + Number(t.tempoMinutos || 0), 0);
|
|
||||||
return {
|
return {
|
||||||
pontos,
|
pontos,
|
||||||
horas: formatHorasResumo(minutos),
|
|
||||||
aprovadas: aprovadas.length,
|
aprovadas: aprovadas.length,
|
||||||
};
|
};
|
||||||
}, [tarefas]);
|
}, [tarefas]);
|
||||||
@@ -244,6 +256,31 @@ export default function FechamentoDetalhes() {
|
|||||||
() => competenciasAbertas.filter((item) => item.id !== competenciaId),
|
() => competenciasAbertas.filter((item) => item.id !== competenciaId),
|
||||||
[competenciasAbertas, competenciaId],
|
[competenciasAbertas, competenciaId],
|
||||||
);
|
);
|
||||||
|
const hasActiveFilters = Boolean(
|
||||||
|
filtroDescricao ||
|
||||||
|
filtroTicket ||
|
||||||
|
filtroCliente ||
|
||||||
|
filtroDataConclusao,
|
||||||
|
);
|
||||||
|
const tarefasFiltradas = useMemo(() => {
|
||||||
|
const descricaoFilter = normalizeTextFilter(filtroDescricao);
|
||||||
|
const ticketFilter = normalizeTextFilter(filtroTicket);
|
||||||
|
const clienteFilter = normalizeTextFilter(filtroCliente);
|
||||||
|
|
||||||
|
return tarefas.filter((tarefa) => {
|
||||||
|
if (descricaoFilter && !normalizeTextFilter(tarefa.descricao ?? "").includes(descricaoFilter)) return false;
|
||||||
|
if (ticketFilter && !normalizeTextFilter(tarefa.numeroTicket ?? "").includes(ticketFilter)) return false;
|
||||||
|
if (clienteFilter && !normalizeTextFilter(tarefa.cliente ?? "").includes(clienteFilter)) return false;
|
||||||
|
if (filtroDataConclusao && toFilterDate(tarefa.dataConclusao) !== filtroDataConclusao) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
tarefas,
|
||||||
|
filtroDescricao,
|
||||||
|
filtroTicket,
|
||||||
|
filtroCliente,
|
||||||
|
filtroDataConclusao,
|
||||||
|
]);
|
||||||
|
|
||||||
const loadTarefas = async () => {
|
const loadTarefas = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -344,6 +381,51 @@ export default function FechamentoDetalhes() {
|
|||||||
}
|
}
|
||||||
}, [isConcluirOpen, parceiroId]);
|
}, [isConcluirOpen, parceiroId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const main = tableScrollRef.current;
|
||||||
|
const sticky = stickyScrollRef.current;
|
||||||
|
if (!main || !sticky) return;
|
||||||
|
|
||||||
|
let syncingFromMain = false;
|
||||||
|
let syncingFromSticky = false;
|
||||||
|
|
||||||
|
const syncMetricsAndVisibility = () => {
|
||||||
|
const spacer = stickyScrollSpacerRef.current;
|
||||||
|
const canScrollX = main.scrollWidth > main.clientWidth + 1;
|
||||||
|
if (spacer) spacer.style.width = `${main.scrollWidth}px`;
|
||||||
|
if (sticky.scrollLeft !== main.scrollLeft) sticky.scrollLeft = main.scrollLeft;
|
||||||
|
setShowStickyScrollbar(canScrollX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMainScroll = () => {
|
||||||
|
if (syncingFromSticky) return;
|
||||||
|
syncingFromMain = true;
|
||||||
|
sticky.scrollLeft = main.scrollLeft;
|
||||||
|
syncingFromMain = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStickyScroll = () => {
|
||||||
|
if (syncingFromMain) return;
|
||||||
|
syncingFromSticky = true;
|
||||||
|
main.scrollLeft = sticky.scrollLeft;
|
||||||
|
syncingFromSticky = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
syncMetricsAndVisibility();
|
||||||
|
main.addEventListener("scroll", onMainScroll, { passive: true });
|
||||||
|
sticky.addEventListener("scroll", onStickyScroll, { passive: true });
|
||||||
|
window.addEventListener("resize", syncMetricsAndVisibility);
|
||||||
|
const resizeObserver = new ResizeObserver(() => syncMetricsAndVisibility());
|
||||||
|
resizeObserver.observe(main);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
main.removeEventListener("scroll", onMainScroll);
|
||||||
|
sticky.removeEventListener("scroll", onStickyScroll);
|
||||||
|
window.removeEventListener("resize", syncMetricsAndVisibility);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [tarefas.length, loading]);
|
||||||
|
|
||||||
const handleToggleAprovada = async (tarefa: FechamentoTarefaItem, approved: boolean) => {
|
const handleToggleAprovada = async (tarefa: FechamentoTarefaItem, approved: boolean) => {
|
||||||
if (isCompetenciaConcluida) {
|
if (isCompetenciaConcluida) {
|
||||||
toastBloqueioCompetenciaConcluida();
|
toastBloqueioCompetenciaConcluida();
|
||||||
@@ -365,6 +447,13 @@ export default function FechamentoDetalhes() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLimparFiltros = () => {
|
||||||
|
setFiltroDescricao("");
|
||||||
|
setFiltroTicket("");
|
||||||
|
setFiltroCliente("");
|
||||||
|
setFiltroDataConclusao("");
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleTodasAprovadas = async () => {
|
const handleToggleTodasAprovadas = async () => {
|
||||||
if (isCompetenciaConcluida) {
|
if (isCompetenciaConcluida) {
|
||||||
toastBloqueioCompetenciaConcluida();
|
toastBloqueioCompetenciaConcluida();
|
||||||
@@ -501,7 +590,7 @@ export default function FechamentoDetalhes() {
|
|||||||
});
|
});
|
||||||
toast.success(`Fechamento concluído. Banco de pontos: ${formatPontosAte2Casas(data.pontuacaoBanco)}.`);
|
toast.success(`Fechamento concluído. Banco de pontos: ${formatPontosAte2Casas(data.pontuacaoBanco)}.`);
|
||||||
setFechamentoStatus("fechado");
|
setFechamentoStatus("fechado");
|
||||||
navigate(`/fechamento/competencias/${data.competenciaId}`);
|
navigate(`/intelligence-score/competencias/${data.competenciaId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Erro ao concluir fechamento.";
|
const message = error instanceof Error ? error.message : "Erro ao concluir fechamento.";
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
@@ -824,7 +913,7 @@ export default function FechamentoDetalhes() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!loading ? (
|
{!loading ? (
|
||||||
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{isCompetenciaConcluida ? (
|
{isCompetenciaConcluida ? (
|
||||||
<div className="sm:col-span-2 xl:col-span-4 rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
|
<div className="sm:col-span-2 xl:col-span-4 rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
|
||||||
Competência concluída. Reabra a competência para editar este fechamento.
|
Competência concluída. Reabra a competência para editar este fechamento.
|
||||||
@@ -857,15 +946,6 @@ export default function FechamentoDetalhes() {
|
|||||||
<CardTitle className="text-3xl">{formatPontos(totais.pontos)}</CardTitle>
|
<CardTitle className="text-3xl">{formatPontos(totais.pontos)}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="border-border/70 bg-card shadow-sm transition hover:shadow-md">
|
|
||||||
<CardHeader className="space-y-2 p-4">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<CardDescription className="text-[11px] uppercase tracking-wide">Horas aprovadas</CardDescription>
|
|
||||||
<Clock3 className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-3xl">{totais.horas}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -881,7 +961,20 @@ export default function FechamentoDetalhes() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{!loading ? (
|
{!loading ? (
|
||||||
<div className="flex justify-end">
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
<Button type="button" size="sm" variant="outline" onClick={() => setShowFilters((prev) => !prev)}>
|
||||||
|
{showFilters ? "Ocultar filtros" : "Exibir filtros"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleLimparFiltros}
|
||||||
|
disabled={!hasActiveFilters}
|
||||||
|
>
|
||||||
|
Limpar filtros
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -897,8 +990,49 @@ export default function FechamentoDetalhes() {
|
|||||||
{todasTarefasRevisadas ? "Desmarcar todas" : "Marcar todas"}
|
{todasTarefasRevisadas ? "Desmarcar todas" : "Marcar todas"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{showFilters ? (
|
||||||
|
<div className="grid grid-cols-1 gap-3 rounded-lg border bg-muted/20 p-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="filtro-descricao">Descrição</Label>
|
||||||
|
<Input
|
||||||
|
id="filtro-descricao"
|
||||||
|
value={filtroDescricao}
|
||||||
|
onChange={(e) => setFiltroDescricao(e.target.value)}
|
||||||
|
placeholder="Digite parte da descrição"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="filtro-ticket">Ticket</Label>
|
||||||
|
<Input
|
||||||
|
id="filtro-ticket"
|
||||||
|
value={filtroTicket}
|
||||||
|
onChange={(e) => setFiltroTicket(e.target.value)}
|
||||||
|
placeholder="Ex.: TKT-1234"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="filtro-cliente">Cliente</Label>
|
||||||
|
<Input
|
||||||
|
id="filtro-cliente"
|
||||||
|
value={filtroCliente}
|
||||||
|
onChange={(e) => setFiltroCliente(e.target.value)}
|
||||||
|
placeholder="Nome do cliente"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="filtro-data-conclusao">Data de conclusão</Label>
|
||||||
|
<Input
|
||||||
|
id="filtro-data-conclusao"
|
||||||
|
type="date"
|
||||||
|
value={filtroDataConclusao}
|
||||||
|
onChange={(e) => setFiltroDataConclusao(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="overflow-x-auto rounded-lg border">
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div ref={tableScrollRef} className="overflow-x-auto rounded-lg border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -911,7 +1045,6 @@ export default function FechamentoDetalhes() {
|
|||||||
<TableHead className="min-w-[140px]">Data vencimento</TableHead>
|
<TableHead className="min-w-[140px]">Data vencimento</TableHead>
|
||||||
<TableHead className="min-w-[170px]">Data conclusão</TableHead>
|
<TableHead className="min-w-[170px]">Data conclusão</TableHead>
|
||||||
<TableHead className="min-w-[240px]">Etiquetas</TableHead>
|
<TableHead className="min-w-[240px]">Etiquetas</TableHead>
|
||||||
<TableHead className="min-w-[120px]">Horas</TableHead>
|
|
||||||
<TableHead className="min-w-[120px]">Pontuação</TableHead>
|
<TableHead className="min-w-[120px]">Pontuação</TableHead>
|
||||||
<TableHead className="min-w-[140px] text-right">Ações</TableHead>
|
<TableHead className="min-w-[140px] text-right">Ações</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -919,12 +1052,18 @@ export default function FechamentoDetalhes() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={12} className="py-8 text-center text-muted-foreground">
|
<TableCell colSpan={11} className="py-8 text-center text-muted-foreground">
|
||||||
Carregando tarefas...
|
Carregando tarefas...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
) : tarefasFiltradas.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={11} className="py-8 text-center text-muted-foreground">
|
||||||
|
Nenhuma tarefa encontrada com os filtros aplicados.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
tarefas.map((tarefa) => (
|
tarefasFiltradas.map((tarefa) => (
|
||||||
<TableRow key={tarefa.id}>
|
<TableRow key={tarefa.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -938,7 +1077,7 @@ export default function FechamentoDetalhes() {
|
|||||||
<TableCell>{tarefa.numeroTicket || "—"}</TableCell>
|
<TableCell>{tarefa.numeroTicket || "—"}</TableCell>
|
||||||
<TableCell className="font-medium">{tarefa.descricao}</TableCell>
|
<TableCell className="font-medium">{tarefa.descricao}</TableCell>
|
||||||
<TableCell>{tarefa.cliente || "—"}</TableCell>
|
<TableCell>{tarefa.cliente || "—"}</TableCell>
|
||||||
<TableCell>{tarefa.tipo}</TableCell>
|
<TableCell>{getTipoLancamentoLabel(tarefa.tipo)}</TableCell>
|
||||||
<TableCell>{formatDateOnly(tarefa.dataInicio)}</TableCell>
|
<TableCell>{formatDateOnly(tarefa.dataInicio)}</TableCell>
|
||||||
<TableCell>{formatDateOnly(tarefa.dataVencimento)}</TableCell>
|
<TableCell>{formatDateOnly(tarefa.dataVencimento)}</TableCell>
|
||||||
<TableCell>{formatDateTime(tarefa.dataConclusao)}</TableCell>
|
<TableCell>{formatDateTime(tarefa.dataConclusao)}</TableCell>
|
||||||
@@ -955,7 +1094,6 @@ export default function FechamentoDetalhes() {
|
|||||||
"—"
|
"—"
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{formatHoras(tarefa.tempoMinutos)}</TableCell>
|
|
||||||
<TableCell>{Number(tarefa.pontuacao || 0)}</TableCell>
|
<TableCell>{Number(tarefa.pontuacao || 0)}</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
@@ -1014,6 +1152,13 @@ export default function FechamentoDetalhes() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
{showStickyScrollbar ? (
|
||||||
|
<div className="fixed bottom-2 left-2 right-2 z-50 rounded-md border bg-background/95 shadow-md backdrop-blur supports-[backdrop-filter]:bg-background/80">
|
||||||
|
<div ref={stickyScrollRef} className="overflow-x-auto">
|
||||||
|
<div ref={stickyScrollSpacerRef} className="h-4 min-w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1038,8 +1183,8 @@ export default function FechamentoDetalhes() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="bonus">Bonificação</SelectItem>
|
<SelectItem value="bonus">{getTipoLancamentoLabel("bonus")}</SelectItem>
|
||||||
<SelectItem value="desconto">Desconto</SelectItem>
|
<SelectItem value="desconto">{getTipoLancamentoLabel("desconto")}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export default function Fechamentos() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate(`/fechamento/competencias/${competencia.id}`)}
|
onClick={() => navigate(`/intelligence-score/competencias/${competencia.id}`)}
|
||||||
>
|
>
|
||||||
<ExternalLink className="mr-2 h-4 w-4" />
|
<ExternalLink className="mr-2 h-4 w-4" />
|
||||||
Acessar
|
Acessar
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export default function MeuFechamento() {
|
|||||||
<ListChecks className="h-5 w-5 md:h-6 md:w-6" />
|
<ListChecks className="h-5 w-5 md:h-6 md:w-6" />
|
||||||
Meu Fechamento
|
Meu Fechamento
|
||||||
</h1>
|
</h1>
|
||||||
<Button onClick={() => navigate("/fechamento/meu-fechamento/banco-pontos")} className="gap-2">
|
<Button onClick={() => navigate("/intelligence-score/meu-fechamento/banco-pontos")} className="gap-2">
|
||||||
<Landmark className="h-4 w-4" />
|
<Landmark className="h-4 w-4" />
|
||||||
Banco de Pontos
|
Banco de Pontos
|
||||||
</Button>
|
</Button>
|
||||||
@@ -266,7 +266,7 @@ export default function MeuFechamento() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!item.fechamento || item.fechamento.status !== "fechado"}
|
disabled={!item.fechamento || item.fechamento.status !== "fechado"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(`/fechamento/fechamentos/${item.fechamento?.id}`, {
|
navigate(`/intelligence-score/fechamentos/${item.fechamento?.id}`, {
|
||||||
state: {
|
state: {
|
||||||
competenciaId: item.competenciaId,
|
competenciaId: item.competenciaId,
|
||||||
status: item.fechamento?.status,
|
status: item.fechamento?.status,
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function MeuFechamentoBancoPontos() {
|
|||||||
if (semVinculo) {
|
if (semVinculo) {
|
||||||
return (
|
return (
|
||||||
<div className="p-3 md:p-6">
|
<div className="p-3 md:p-6">
|
||||||
<Button variant="ghost" size="sm" onClick={() => navigate("/fechamento/meu-fechamento")}>
|
<Button variant="ghost" size="sm" onClick={() => navigate("/intelligence-score/meu-fechamento")}>
|
||||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||||
Voltar
|
Voltar
|
||||||
</Button>
|
</Button>
|
||||||
@@ -133,7 +133,7 @@ export default function MeuFechamentoBancoPontos() {
|
|||||||
<div className="flex h-full flex-col bg-background pb-16 md:pb-0">
|
<div className="flex h-full flex-col bg-background pb-16 md:pb-0">
|
||||||
<div className="border-b border-border bg-muted/20 p-3 md:p-6">
|
<div className="border-b border-border bg-muted/20 p-3 md:p-6">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Button variant="ghost" size="sm" onClick={() => navigate("/fechamento/meu-fechamento")}>
|
<Button variant="ghost" size="sm" onClick={() => navigate("/intelligence-score/meu-fechamento")}>
|
||||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||||
Voltar ao meu fechamento
|
Voltar ao meu fechamento
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function NotFound() {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="mb-3 text-4xl font-bold">404</h1>
|
<h1 className="mb-3 text-4xl font-bold">404</h1>
|
||||||
<p className="mb-4 text-muted-foreground">Rota não encontrada neste módulo.</p>
|
<p className="mb-4 text-muted-foreground">Rota não encontrada neste módulo.</p>
|
||||||
<Link to="/fechamento" className="text-primary underline hover:text-primary/90">
|
<Link to="/intelligence-score" className="text-primary underline hover:text-primary/90">
|
||||||
Voltar para Fechamentos
|
Voltar para Fechamentos
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user