Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 105 additions & 45 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
-->
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
script-src 'self' 'sha256-vXWTTploKEvVnQIl6BHHd4RlAebj/YczcjzvtMQDrJU=' 'sha256-IXAIIRKPiV26ooPz7sFNeiZgqV0lZ0enYnLo4taJL6w=' 'sha256-YUyMlC9hDDvLEiBbT5a1sly4KWHe+hgQyZ+PNXv8a/E=' 'sha256-4WSPCb69NjUOafUni0dDUQlotXJjdckj0F9YDv8fESc=' 'sha256-bKSU+Byi5gD2SDWD8SW0s+uaE4KuM7WEt9wKcWbY6hU=' 'sha256-7Q/PrhTi3jY+FByW7I2iVA8xr5aq2vzzmoXbLtxy77E=';
script-src 'self' 'sha256-vXWTTploKEvVnQIl6BHHd4RlAebj/YczcjzvtMQDrJU=' 'sha256-IXAIIRKPiV26ooPz7sFNeiZgqV0lZ0enYnLo4taJL6w=' 'sha256-YUyMlC9hDDvLEiBbT5a1sly4KWHe+hgQyZ+PNXv8a/E=' 'sha256-zl8UGWjyGcM/dYz16d2/l8Cpfou9ra8ExylKHhlcC6s=' 'sha256-bKSU+Byi5gD2SDWD8SW0s+uaE4KuM7WEt9wKcWbY6hU=' 'sha256-7Q/PrhTi3jY+FByW7I2iVA8xr5aq2vzzmoXbLtxy77E=';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
img-src 'self' data: blob:;
Expand Down Expand Up @@ -7929,7 +7929,14 @@ <h1>Workflow de Revisão<br>do Supermercado Dupla Caixa</h1>
// ─────────────────────────────────────────────────────────────────────────────
// PROCESS CARD
// ─────────────────────────────────────────────────────────────────────────────
function ProcessCard({
// Memoizado: é o componente que se multiplica pela lista inteira, e cada cartão
// chama o `procOverallPct` e o `calcStatus` dos dois percursos ao desenhar-se.
// Sem isto, escrever na pesquisa redesenhava todos os cartões que continuavam na
// lista, mesmo sem nenhum deles ter mudado. O `onClick` que recebe é o
// `setSelected` — um setter de estado, estável entre renders —, pelo que a
// comparação de props do React.memo não é anulada por uma função nova a cada
// render. Trocá-lo por uma arrow inline anula a memoização toda.
const ProcessCard = React.memo(function ProcessCard({
proc,
onClick
}) {
Expand Down Expand Up @@ -8612,7 +8619,7 @@ <h1>Workflow de Revisão<br>do Supermercado Dupla Caixa</h1>
color: cfg.dot
}
}, cfg.label))));
}
});

// ─────────────────────────────────────────────────────────────────────────────
// NEW PROCESS MODAL
Expand Down Expand Up @@ -10761,6 +10768,42 @@ <h1>Workflow de Revisão<br>do Supermercado Dupla Caixa</h1>
// ─────────────────────────────────────────────────────────────────────────────
// MAIN APP
// ─────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// PREDICADOS DE ESTADO DE UM PROCESSO
//
// Vivem ao nível do módulo, e não dentro do `App`, por duas razões. A primeira é
// que não dependem de estado nenhum — só dos argumentos e das funções de data,
// que também são do módulo. A segunda é que a lista filtrada e os totais são
// memoizados: uma função recriada a cada render entra nas dependências do
// `useMemo` e faz o `useMemo` recalcular sempre, que é o mesmo que não o ter.
// ─────────────────────────────────────────────────────────────────────────────

/**
* Um processo está atrasado quando algum dos percursos *activos* o está e o
* processo ainda não terminou.
*/
function isProcLate(p) {
const hasCli = p.roles?.clinico !== false;
const hasFar = p.roles?.farmacia !== false;
const cliLate = hasCli && calcStatus(p.clinico, p.targetDate) === "late";
const farLate = hasFar && calcStatus(p.farmacia, p.targetDate) === "late";
return (cliLate || farLate) && !isProcDone(p);
}

/**
* Estado de um percurso, ou `null` se esse percurso não estiver activo.
*
* Um processo pode ter apenas um dos percursos activo (`roles`). O tracking do
* percurso desactivado continua a existir no objecto, parado na fase 1, e o
* `calcStatus` classifica-o a partir da Data Alvo do processo — pelo que um
* processo só-clínico com prazo ultrapassado aparecia também como «farmácia
* atrasada». Os totais 🏥/💊 contavam então percursos inexistentes e não batiam
* certo com o número grande ao lado nem com os mesmos totais no KPI Executivo,
* que já filtra por role. Contar sempre através deste helper.
*/
function procRoleStatus(p, role) {
return p.roles?.[role] === false ? null : calcStatus(p[role], p.targetDate);
}
function App() {
const session = window.__SUPERMERCADOS_SESSION;
const [procs, setProcs] = useState(session.procs || []);
Expand Down Expand Up @@ -10940,21 +10983,25 @@ <h1>Workflow de Revisão<br>do Supermercado Dupla Caixa</h1>
// separar as duas chamadas para o `loadedFilename` ficar para trás.
}, [annualPlan, procs, persist]);

// Regra única, partilhada com as funções de data ao nível do módulo.
const isDone = p => isProcDone(p);
const isLate = p => {
const hasCli = p.roles?.clinico !== false;
const hasFar = p.roles?.farmacia !== false;
const cliLate = hasCli && calcStatus(p.clinico, p.targetDate) === "late";
const farLate = hasFar && calcStatus(p.farmacia, p.targetDate) === "late";
return (cliLate || farLate) && !isDone(p);
};
const filtered = procs.filter(p => {
// ── Lista filtrada e ordenada ───────────────────────────────────────────
//
// Era recalculada a cada render — incluindo a cada tecla escrita na caixa de
// pesquisa, e a cada render provocado por qualquer outro estado do `App`. Por
// processo, o filtro chama o `calcStatus` até quatro vezes e a ordenação
// chama o `procOverallPct`, que percorre todas as fases e passos dos dois
// percursos. Com dezenas de processos ainda não se nota; com centenas nota-se
// no teclado.
//
// A pesquisa usa o valor *adiado*: o React mantém a caixa a responder à tecla
// e refaz a lista logo a seguir, em vez de fazer as duas coisas no mesmo
// frame. É o que evita o atraso visível ao escrever.
const searchAdiado = React.useDeferredValue(search);
const filtered = React.useMemo(() => procs.filter(p => {
const cliEff = calcStatus(p.clinico, p.targetDate);
const farEff = calcStatus(p.farmacia, p.targetDate);
if (filterStatus === "active") {
// Exclui processos onde todos os roles activos estão done
if (isDone(p)) return false;
if (isProcDone(p)) return false;
} else if (filterStatus === "inactive") {
const hasCli = p.roles?.clinico !== false;
const hasFar = p.roles?.farmacia !== false;
Expand Down Expand Up @@ -10988,7 +11035,10 @@ <h1>Workflow de Revisão<br>do Supermercado Dupla Caixa</h1>
const procTags = p.tags || [];
if (!filterTags.every(t => procTags.includes(t))) return false;
}
if (search && !p.service.toLowerCase().includes(search.toLowerCase()) && !(p.clinico.responsible || "").toLowerCase().includes(search.toLowerCase()) && !(p.farmacia.responsible || "").toLowerCase().includes(search.toLowerCase()) && !(p.costCenter || "").toLowerCase().includes(search.toLowerCase()) && !(p.supermarketCode || "").toLowerCase().includes(search.toLowerCase())) return false;
if (searchAdiado) {
const termo = searchAdiado.toLowerCase();
if (!p.service.toLowerCase().includes(termo) && !(p.clinico.responsible || "").toLowerCase().includes(termo) && !(p.farmacia.responsible || "").toLowerCase().includes(termo) && !(p.costCenter || "").toLowerCase().includes(termo) && !(p.supermarketCode || "").toLowerCase().includes(termo)) return false;
}
return true;
}).slice().sort((a, b) => {
if (sortBy === "prazo") {
Expand All @@ -11004,46 +11054,56 @@ <h1>Workflow de Revisão<br>do Supermercado Dupla Caixa</h1>
}
// "criacao" — ordem de criação (id crescente)
return a.id - b.id;
});
}), [procs, filterStatus, filterRole, filterResponsavel, filterTags, searchAdiado, sortBy]);

// Um processo pode ter apenas um dos percursos activo (`roles`). O tracking do
// percurso desactivado continua a existir no objecto, parado na fase 1, e o
// `calcStatus` classifica-o a partir da Data Alvo do processo — pelo que um
// processo só-clínico com prazo ultrapassado aparecia também como "farmácia
// atrasada". Os totais 🏥/💊 contavam então percursos inexistentes e não batiam
// certo com o número grande ao lado nem com os mesmos totais no KPI Executivo,
// que já filtra por role. Contar sempre através deste helper.
const roleStatus = (p, role) => p.roles?.[role] === false ? null : calcStatus(p[role], p.targetDate);
const stats = {
// Oito varrimentos da lista, um por total. Dependem só do `procs`, portanto
// não têm de voltar a correr quando muda um filtro ou a pesquisa.
const stats = React.useMemo(() => ({
total: procs.length,
active: procs.filter(p => !isDone(p) && roleStatus(p, "clinico") !== "paused" && roleStatus(p, "farmacia") !== "paused").length,
late: procs.filter(isLate).length,
done: procs.filter(isDone).length,
cliDone: procs.filter(p => roleStatus(p, "clinico") === "done").length,
farDone: procs.filter(p => roleStatus(p, "farmacia") === "done").length,
cliLate: procs.filter(p => roleStatus(p, "clinico") === "late").length,
farLate: procs.filter(p => roleStatus(p, "farmacia") === "late").length
};
active: procs.filter(p => !isProcDone(p) && procRoleStatus(p, "clinico") !== "paused" && procRoleStatus(p, "farmacia") !== "paused").length,
late: procs.filter(isProcLate).length,
done: procs.filter(isProcDone).length,
cliDone: procs.filter(p => procRoleStatus(p, "clinico") === "done").length,
farDone: procs.filter(p => procRoleStatus(p, "farmacia") === "done").length,
cliLate: procs.filter(p => procRoleStatus(p, "clinico") === "late").length,
farLate: procs.filter(p => procRoleStatus(p, "farmacia") === "late").length
}), [procs]);

// ── Throughput — processos concluídos nos últimos 30 dias ────────────────
// `getRealCompletionTs` devolve a conclusão de qualquer um dos percursos, pelo
// que um processo com o clínico terminado e a farmácia ainda a decorrer era
// contado aqui como concluído enquanto o cartão CONCLUÍDOS, que usa `isDone`,
// o excluía. Exigir `isDone` alinha os dois.
const now30 = Date.now();
const throughput30 = procs.filter(p => {
if (!isDone(p)) return false;
const realDoneTs = getRealCompletionTs(p);
if (!realDoneTs) return false;
const lastDone = new Date(realDoneTs).getTime();
return now30 - lastDone <= 30 * 86400000;
}).length;
//
// O `Date.now()` passou para dentro do `useMemo`, pelo que o "agora" é o do
// último `procs` e não o de cada render. A janela é de 30 dias: a diferença de
// umas horas numa página deixada aberta não muda nenhum dos números, e a
// gravação seguinte refaz a conta.
const throughput30 = React.useMemo(() => {
const agora = Date.now();
return procs.filter(p => {
if (!isProcDone(p)) return false;
const realDoneTs = getRealCompletionTs(p);
if (!realDoneTs) return false;
const lastDone = new Date(realDoneTs).getTime();
return agora - lastDone <= 30 * 86400000;
}).length;
}, [procs]);

// ── Lead/Cycle time médios (processos concluídos) ────────────────────────
const doneProcs = procs.filter(isDone);
const avgOf = arr => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length * 10) / 10 : null;
const avgLeadTime = avgOf(doneProcs.map(calcLeadTime));
const avgCycleTime = avgOf(doneProcs.map(calcCycleTime));
// O `calcLeadTime`/`calcCycleTime` percorrem o `phaseLog` inteiro de cada
// processo concluído.
const {
avgLeadTime,
avgCycleTime
} = React.useMemo(() => {
const doneProcs = procs.filter(isProcDone);
const avgOf = arr => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length * 10) / 10 : null;
return {
avgLeadTime: avgOf(doneProcs.map(calcLeadTime)),
avgCycleTime: avgOf(doneProcs.map(calcCycleTime))
};
}, [procs]);
return /*#__PURE__*/React.createElement("div", {
style: {
background: "#eef2f7",
Expand Down
Loading
Loading