const { Card, Tabs, Button, Icon, IconButton, DropdownMenu, Breadcrumbs, StatusPill, PriorityBadge, SectorTag, DeadlineIndicator, ChecklistPanel, Timeline, Avatar, AvatarGroup, ProgressBar, Tag, Modal, Textarea, Badge, DataTable, Tooltip, FileDropzone, CommentComposer, Select, Checkbox, Switch, SearchField } = window.ACRFlowDesignSystem_ba1e9c;
const SECTOR_LABEL = new Proxy({}, { get: (_, id) => { const s = (window.ACRData.sectorsCfg || []).find((x) => x.id === id); return s ? s.name : "Setor"; } });
function AssignModal({ current, sector, onClose, onConfirm }) {
const team = (window.ACRData.team || []).filter((m) => m.active !== false);
const [toSector, setToSector] = React.useState(sector);
const [owner, setOwner] = React.useState(current[0] ? current[0].name : "");
const [extras, setExtras] = React.useState(current.slice(1).map((p) => p.name));
const [q, setQ] = React.useState("");
const [note, setNote] = React.useState("");
const [notify, setNotify] = React.useState(true);
// Qualquer colaborador ativo pode ser responsável — o setor é só informativo,
// já que um supervisor pode atender mais de uma área.
const list = team.filter((m) => m.name.toLowerCase().includes(q.toLowerCase()));
const forwarding = toSector !== sector;
return (
Cancelar } disabled={!owner} onClick={() => onConfirm({ owner, extras, toSector, note })}>{forwarding ? "Encaminhar e atribuir" : "Salvar atribuição"}>}>
setToSector(e.target.value)}
options={window.ACRData.sectorsCfg.filter((s) => s.active).map((s) => ({ value: s.id, label: s.name }))}
hint={forwarding ? "A demanda será encaminhada para outra área — o histórico é preservado." : "Qualquer colaborador ativo pode ser responsável, de qualquer área."} />
setQ(e.target.value)} />
{list.map((m) => {
const isOwner = m.name === owner;
const isExtra = extras.includes(m.name);
return (
{m.name}
{[m.role, m.profile === "supervisor" ? "Supervisor" : "Operacional", m.email].filter(Boolean).join(" · ")}
{isOwner ?
Principal : (
{ setOwner(m.name); setExtras((s) => s.filter((n) => n !== m.name)); }}>Tornar principal
)}
setExtras((s) => (s.includes(m.name) ? s.filter((n) => n !== m.name) : [...s, m.name]))} />
);
})}
{!list.length ?
{team.length === 0 ? "Nenhum colaborador cadastrado — cadastre em Controle de Acesso." : "Nenhuma pessoa com esse nome."} : null}
);
}
function Field({ label, children }) {
return (
{label}
{children}
);
}
function DemandDetailScreen({ user, demandId, onBack, onToast }) {
const D = window.ACRData;
const demand = D.demands.find((d) => d.id === demandId) || D.demands[1];
const supervisor = user.profile === "supervisor";
const souResponsavel = (demand.people || []).some((p) => p.name === user.name);
const souCriador = demand.openedById ? demand.openedById === user.id : demand.requester === user.name;
const podeMexer = supervisor || souResponsavel || souCriador;
const podeExcluir = supervisor || souCriador;
const [tab, setTab] = React.useState("visao");
const [items, setItems] = React.useState(window.ACRStore.online() ? [] : (D.checklist || []));
const [modal, setModal] = React.useState(null);
const [status, setStatus] = React.useState(demand.status === "atribuida" ? "aguardando_atendimento" : demand.status);
const [touched, setTouched] = React.useState(!["aguardando_atendimento", "atribuida", "aberta", "triagem"].includes(demand.status));
const S = window.ACRStore;
const [detalhe, setDetalhe] = React.useState(null);
const [editandoId, setEditandoId] = React.useState(null);
const [rascunho, setRascunho] = React.useState("");
const [apagarComentario, setApagarComentario] = React.useState(null);
const [anexoUrls, setAnexoUrls] = React.useState({});
const [edicao, setEdicao] = React.useState(null);
const [uploads, setUploads] = React.useState([]);
const [enviando, setEnviando] = React.useState(false);
const falhou = (r) => { if (!r.ok) { onToast({ tone: "danger", title: "Não foi possível gravar", message: r.error }); return true; } return false; };
const [people, setPeople] = React.useState(demand.people);
const [sector, setSector] = React.useState(demand.sector);
const [assign, setAssign] = React.useState(null);
const [priority, setPriority] = React.useState(demand.priority);
const [log, setLog] = React.useState([]);
const [comments, setComments] = React.useState([]);
/** Ao abrir, busca no banco o conteúdo completo desta demanda. */
const recarregarDetalhe = React.useCallback(async () => {
const d = await S.carregarDemanda(demand.id);
if (!d) return;
setDetalhe(d);
setComments((d.comments || []).map((c) => ({
id: c.id, userId: c.user_id, editado: !!c.edited_at,
n: c.user_name || "—",
d: new Date(c.created_at).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" }),
t: c.body,
files: (d.attachments || []).filter((a) => a.comment_id === c.id).map((a) => ({
id: a.id, name: a.file_name, url: a.public_url || null, key: a.storage_key,
isImage: (a.kind === "imagem") || (a.content_type || "").startsWith("image/"),
})),
})));
setItems((d.checklist || []).map((i) => ({
id: i.id, text: i.text, done: !!i.done,
owner: ((D.team || []).find((u) => u.id === i.owner_id) || {}).name || "",
due: i.due_date ? String(i.due_date).slice(0, 10) : "",
note: i.note || "",
})));
setLog([]);
}, [demand.id]);
React.useEffect(() => { recarregarDetalhe(); }, [recarregarDetalhe]);
/** Bucket é privado: pede a URL assinada de cada imagem para exibir no comentário. */
React.useEffect(() => {
if (!S.online() || !detalhe) return;
const pendentes = (detalhe.attachments || []).filter((a) => !a.public_url && !anexoUrls[a.id]);
if (!pendentes.length) return;
let vivo = true;
Promise.all(pendentes.map(async (a) => {
try { const r = await window.ACRApi.attachments.url(a.id); return [a.id, r.url]; } catch { return null; }
})).then((pares) => {
if (!vivo) return;
const mapa = {};
pares.filter(Boolean).forEach(([id, url]) => (mapa[id] = url));
if (Object.keys(mapa).length) setAnexoUrls((s) => ({ ...s, ...mapa }));
});
return () => { vivo = false; };
}, [detalhe]);
const abrirAnexo = async (f) => {
const url = f.url || anexoUrls[f.id];
if (url) { window.open(url, "_blank", "noopener"); return; }
try { const r = await window.ACRApi.attachments.url(f.id); window.open(r.url, "_blank", "noopener"); }
catch { onToast({ tone: "danger", title: "Não foi possível abrir o anexo" }); }
};
const historico = (detalhe && detalhe.history || []).map((h) => ({
id: h.id, kind: "status", actor: (h.user_name || "Sistema").split(" ")[0], text: h.description || h.action,
date: new Date(h.created_at).toLocaleDateString("pt-BR"),
time: new Date(h.created_at).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }),
}));
const anexosBanco = (detalhe && detalhe.attachments || []).map((a) => ({
name: a.file_name, desc: a.description || "", type: a.kind || (a.content_type && a.content_type.startsWith("image/") ? "Imagem" : "Arquivo"),
user: a.uploaded_by_name || "", date: new Date(a.uploaded_at).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" }),
}));
const evidenceRows = [
...uploads.map((f) => ({ name: f.name, desc: "Anexado agora nesta demanda", type: f.kind, user: user.name, date: new Date().toLocaleDateString("pt-BR") + " " + new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }) })),
...anexosBanco,
...(S.online() ? [] : D.evidences),
];
const done = items.filter((i) => i.done).length;
const pct = items.length ? Math.round((done / items.length) * 100) : 0;
const events = S.online() ? [...historico, ...log] : [...D.timeline, ...log];
const toggle = async (id) => {
const atual = items.find((i) => i.id === id);
const r = await S.checklistAtualizar(demand.id, id, { done: !(atual && atual.done) });
if (falhou(r)) return;
registerInteraction("Checklist atualizado.");
setItems((s) => s.map((i) => (i.id === id ? { ...i, done: !i.done } : i)));
};
const now = () => new Date().toLocaleDateString("pt-BR") + " " + new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
const logEvent = (text, meta) => setLog((s) => [...s, { id: "l" + Date.now() + Math.random(), kind: "status", actor: user.name.split(" ")[0], text, meta, date: now().slice(0, 10), time: now().slice(11) }]);
const setStatusManual = async (next) => {
if (next === status) return;
const label = (D.statuses.find((s) => s.id === next) || {}).name || next;
const r = await S.atualizarDemanda(demand.id, { statusId: next });
if (falhou(r)) return;
setStatus(next);
logEvent('alterou o status para "' + label + '".');
onToast({ tone: "info", title: "Status alterado", message: "Agora: " + label });
};
/** Primeira interação do operacional tira a demanda de "Aguardando atendimento". */
const registerInteraction = (what) => {
if (touched) return;
setTouched(true);
if (status === "aguardando_atendimento") {
setStatus("andamento");
logEvent('iniciou o atendimento — status alterado automaticamente para "Em andamento".', what);
onToast({ tone: "success", title: "Atendimento iniciado", message: "A demanda passou para Em andamento." });
}
};
const changePriority = async (p) => {
if (p === priority) return;
const label = (D.priorities.find((x) => x.id === p) || {}).name || p;
const from = (D.priorities.find((x) => x.id === priority) || {}).name || priority;
const r = await S.atualizarDemanda(demand.id, { priorityId: p });
if (falhou(r)) return;
setPriority(p);
setLog((s) => [...s, { id: "p" + Date.now(), kind: "status", actor: user.name.split(" ")[0], text: "alterou a prioridade de " + from + " para " + label + ".", date: now().slice(0, 10), time: now().slice(11) }]);
onToast({ tone: p === "urgente" ? "danger" : "info", title: "Prioridade alterada", message: "Agora esta demanda está como " + label + "." });
};
const applyAssign = async ({ owner, extras, toSector, note }) => {
const idDe = (n) => ((D.team || []).find((u) => u.name === n) || {}).id;
const principal = idDe(owner);
const ids = [principal, ...extras.map(idDe)].filter(Boolean);
if (ids.length) {
const r = await S.atualizarDemanda(demand.id, { assigneeIds: ids, primaryAssigneeId: principal, note: note || undefined });
if (falhou(r)) return;
}
setPeople([{ name: owner }, ...extras.map((n) => ({ name: n }))]);
setStatus("aguardando_atendimento");
setTouched(false);
setSector(toSector);
const changedSector = toSector !== demand.sector;
setLog((s) => [...s, {
id: "a" + Date.now(), kind: "status", actor: user.name.split(" ")[0],
text: changedSector ? "encaminhou a demanda para " + SECTOR_LABEL[toSector] + " e atribuiu a " + owner + "." : "atribuiu a demanda a " + owner + ".",
date: now().slice(0, 10), time: now().slice(11),
meta: (extras.length ? "Responsáveis adicionais: " + extras.join(", ") + ". " : "") + (note || "") + " Status: Aguardando atendimento até a primeira interação do responsável.",
}]);
setAssign(null);
onToast({ tone: "success", title: changedSector ? "Demanda encaminhada" : "Demanda atribuída", message: owner + " foi notificado por e-mail e WhatsApp." });
};
const [resumo, setResumo] = React.useState("");
const [resultado, setResultado] = React.useState("");
const [motivo, setMotivo] = React.useState("");
const close = async (kind) => {
setEnviando(true);
let r = { ok: true };
if (kind === "solicitar") r = await S.solicitarEncerramento(demand.id, { summary: resumo.trim() || "Atividades concluídas.", result: resultado.trim() || undefined });
if (kind === "aprovar") r = await S.atualizarDemanda(demand.id, { statusId: "encerrada" });
if (kind === "devolver") r = await S.devolverDemanda(demand.id, motivo.trim() || "Devolvida para ajustes.");
setEnviando(false);
if (falhou(r)) return;
setModal(null);
if (kind === "solicitar") { setStatus("aguardando_aprovacao"); setResumo(""); setResultado(""); onToast({ tone: "success", title: "Encerramento solicitado", message: "Aguardando aprovação do supervisor." }); }
if (kind === "aprovar") { setStatus("encerrada"); onToast({ tone: "success", title: "Encerramento aprovado", message: demand.id + " foi encerrada." }); }
if (kind === "devolver") { setStatus("andamento"); setMotivo(""); onToast({ tone: "danger", title: "Demanda devolvida", message: "O responsável foi notificado com o motivo." }); }
};
return (
} label="Voltar" variant="outline" onClick={onBack} />
{demand.id}
{demand.title}
s.id === status) || {}).name} tone={(D.statuses.find((s) => s.id === status) || {}).tone} /> }
items={D.statuses.filter((s) => s.id !== "encerrada" || supervisor).map((s) => ({
label: s.name, icon: ,
onSelect: () => setStatusManual(s.id),
}))} />
p.id === priority) || {}).name} tone={(D.priorities.find((p) => p.id === priority) || {}).tone} />{status === "encerrada" ? null : }}
items={D.priorities.map((p) => ({
label: "Prioridade " + p.name, icon: ,
onSelect: () => changePriority(p.id),
}))} />
{demand.tags.map((t) => {t} )}
{status === "encerrada" ? null : (
} onClick={() => setAssign(true)}>Atribuir / encaminhar
)}
{status === "aguardando_aprovacao" && supervisor ? (
<>
} onClick={() => setModal("devolver")}>Devolver para execução
} onClick={() => setModal("aprovar")}>Aprovar encerramento
>
) : status === "aguardando_aprovacao" ? (
}>Aguardando aprovação
) : status === "encerrada" ? (
} disabled={!supervisor}>Reabrir demanda
) : (
} onClick={() => setModal("solicitar")}>Solicitar encerramento
)}
} label="Ações" variant="outline" />}
items={[
{ label: "Atribuir / encaminhar", icon: , onSelect: () => setAssign(true) },
...(podeMexer ? [{ label: "Editar demanda", icon: , onSelect: () => { setEdicao({ title: demand.title, description: demand.description || "", priorityId: priority, deadline: demand.dueISO || "" }); } }] : []),
{ divider: true },
...(podeExcluir ? [{ label: "Excluir demanda", icon: , tone: "danger", onSelect: () => setModal("excluir") }] : []),
]} />
{status === "aguardando_atendimento" ? (
Aguardando atendimento — a demanda passa para Em andamento automaticamente na primeira interação do responsável (comentário, checklist ou evidência).
) : null}
{tab === "visao" ? (
{demand.description || "Sem descrição registrada."}
{demand.requester || "—"}
{demand.category || "—"}
{demand.opened}
{demand.due}
{demand.context || "—"}
{demand.recurrenceLabel || "Não recorrente"}
} onClick={() => setAssign(true)}>Atribuir}>
{people.slice(1).map((p) =>
)}
} style={{ alignSelf: "flex-start" }} onClick={() => setAssign(true)}>Adicionar responsável
Setor responsável: {SECTOR_LABEL[sector]}
) : null}
{tab === "check" ?
{ const idDe = (n) => ((D.team || []).find((u) => u.name === n) || {}).id; const r = await S.checklistAdicionar(demand.id, { text: it.text, ownerId: idDe(it.owner), dueDate: it.due || undefined }); if (falhou(r)) return; registerInteraction("Etapa criada no checklist."); setItems((s) => [...s, { ...it, id: "c" + Date.now(), done: false }]); onToast({ tone: "success", title: "Etapa criada", message: it.text }); }}
onUpdate={async (id, patch) => { const idDe = (n) => ((D.team || []).find((u) => u.name === n) || {}).id; const body = {}; if (patch.text !== undefined) body.text = patch.text; if (patch.done !== undefined) body.done = patch.done; if (patch.owner !== undefined) body.ownerId = idDe(patch.owner) || null; if (patch.due !== undefined) body.dueDate = patch.due || null; const r = await S.checklistAtualizar(demand.id, id, body); if (falhou(r)) return; registerInteraction("Checklist editado."); setItems((s) => s.map((i) => (i.id === id ? { ...i, ...patch } : i))); onToast({ tone: "info", title: "Etapa atualizada", message: patch.text }); }}
onRemove={async (id) => { const it = items.find((i) => i.id === id); const r = await S.checklistRemover(demand.id, id); if (falhou(r)) return; setItems((s) => s.filter((i) => i.id !== id)); onToast({ tone: "danger", title: "Etapa excluída", message: it && it.text }); }} /> : null}
{tab === "hist" ?
: null}
{tab === "evid" ? (
{ setEnviando(true); const r = await S.anexar(demand.id, f); setEnviando(false); if (falhou(r)) return; registerInteraction("Evidência anexada."); setUploads((s) => [...s, ...f]); onToast({ tone: "success", title: f.length > 1 ? f.length + " arquivos anexados" : "Arquivo anexado", message: f.map((x) => x.name).join(", ") }); }}
onRemove={(id) => setUploads((s) => s.filter((f) => f.id !== id))} />
{r.name} },
{ key: "desc", label: "Descrição" },
{ key: "type", label: "Tipo", width: 96, render: (r) => {r.type} },
{ key: "user", label: "Usuário", width: 150 },
{ key: "date", label: "Data", width: 150, render: (r) => {r.date} },
]} rows={evidenceRows} rowKey="name" dense />
) : null}
{tab === "coment" ? (
{comments.length === 0 ? Nenhum comentário ainda.
: null}
{comments.map((c, i) => {
const meu = c.userId ? c.userId === user.id : c.n === user.name;
const editando = editandoId === c.id;
return (
{c.n}
{c.d}
{c.editado ? · editado : null}
{meu && status !== "encerrada" && !editando ? (
{ setEditandoId(c.id); setRascunho(c.t); }}>Editar
setApagarComentario(c)}>Excluir
) : null}
{editando ? (
) : c.t ?
{c.t} : null}
{c.files && c.files.length ? (
{c.files.map((f) => (
abrirAnexo(f)}
style={{ padding: 0, border: "none", background: "transparent", cursor: "pointer" }}>
{f.isImage ? (
{ e.target.style.display = "none"; }}
style={{ width: 190, height: 118, objectFit: "cover", borderRadius: "var(--radius-sm)", border: "1px solid var(--border-subtle)", display: "block" }} />
) : (
{f.name}
)}
))}
) : null}
);})}
{
setEnviando(true);
const r = await S.comentar(demand.id, text, files);
setEnviando(false);
if (falhou(r)) return;
registerInteraction("Comentário publicado.");
setComments((s) => [...s, { n: user.name, d: now(), t: text, files }]);
if (files.length) setUploads((s) => [...s, ...files]);
onToast({ tone: "success", title: "Comentário publicado", message: files.length ? files.length + (files.length === 1 ? " anexo salvo no storage." : " anexos salvos no storage.") : undefined });
}} />
) : null}
{assign ?
setAssign(null)} onConfirm={applyAssign} /> : null}
{edicao ? (
setEdicao(null)}
footer={<> setEdicao(null)}>Cancelar
{
setEnviando(true);
const r = await S.editarDemanda(demand.id, {
title: edicao.title.trim(),
description: edicao.description.trim(),
priorityId: edicao.priorityId,
deadline: edicao.deadline || null,
});
setEnviando(false);
if (falhou(r)) return;
setPriority(edicao.priorityId);
setEdicao(null);
await recarregarDetalhe();
onToast({ tone: "success", title: "Demanda atualizada" });
}}>{enviando ? "Gravando…" : "Salvar alterações"} >}>
setEdicao({ ...edicao, title: e.target.value })} />
) : null}
{modal === "excluir" ? (
setModal(null)}
footer={<> setModal(null)}>Cancelar
{
setEnviando(true);
const r = await S.excluirDemanda(demand.id);
setEnviando(false);
if (falhou(r)) return;
onToast({ tone: "danger", title: "Demanda excluída", message: demand.id });
onBack();
}}>{enviando ? "Excluindo…" : "Excluir definitivamente"} >}>
Comentários, anexos, checklist e histórico desta demanda serão apagados. Esta ação não pode ser desfeita.
) : null}
{apagarComentario ? (
setApagarComentario(null)}
footer={<> setApagarComentario(null)}>Cancelar
{
setEnviando(true);
const r = await S.removerComentario(demand.id, apagarComentario.id);
setEnviando(false);
if (falhou(r)) return;
setApagarComentario(null);
await recarregarDetalhe();
onToast({ tone: "danger", title: "Comentário excluído" });
}}>Excluir >}>
O comentário e os anexos enviados junto com ele serão removidos. O histórico registra a exclusão.
) : null}
{modal === "solicitar" ? (
setModal(null)}
footer={<> setModal(null)}>Cancelar close("solicitar")}>{enviando ? "Enviando…" : "Enviar para aprovação"} >}>
) : null}
{modal === "aprovar" ? (
setModal(null)}
footer={<> setModal(null)}>Cancelar close("aprovar")}>{enviando ? "Gravando…" : "Aprovar e encerrar"} >}>
Ao aprovar, a demanda passa para Encerrada e o histórico é congelado. O responsável recebe a notificação.
) : null}
{modal === "devolver" ? (
setModal(null)}
footer={<> setModal(null)}>Cancelar close("devolver")}>{enviando ? "Gravando…" : "Devolver demanda"} >}>
setMotivo(e.target.value)} placeholder="Explique o que precisa ser corrigido." counterMax={500} />
A demanda retorna para "Em andamento" e o responsável é notificado com o motivo.
) : null}
);
}
Object.assign(window, { DemandDetailScreen });