348 lines
12 KiB
React
348 lines
12 KiB
React
|
|
import { useEffect, useMemo, useState } from 'react';
|
||
|
|
import { DataPanel } from './DataPanel';
|
||
|
|
import {
|
||
|
|
approveTemplateByAdmin,
|
||
|
|
deleteTemplate,
|
||
|
|
listTemplates,
|
||
|
|
rejectTemplateByAdmin,
|
||
|
|
saveTemplate,
|
||
|
|
} from '../services/templateService';
|
||
|
|
|
||
|
|
const fieldStyle = {
|
||
|
|
width: '100%',
|
||
|
|
border: '1px solid var(--color-border)',
|
||
|
|
borderRadius: '14px',
|
||
|
|
padding: '0.75rem 0.85rem',
|
||
|
|
background: '#fff',
|
||
|
|
color: 'var(--color-text)',
|
||
|
|
fontWeight: 600,
|
||
|
|
};
|
||
|
|
|
||
|
|
const statusMeta = {
|
||
|
|
approved: {
|
||
|
|
label: 'Aprovado pela Meta',
|
||
|
|
background: 'rgba(34, 197, 94, 0.12)',
|
||
|
|
color: '#15803d',
|
||
|
|
},
|
||
|
|
meta_review: {
|
||
|
|
label: 'Em análise pela Meta',
|
||
|
|
background: 'rgba(229, 162, 42, 0.16)',
|
||
|
|
color: '#8a5a00',
|
||
|
|
},
|
||
|
|
admin_review: {
|
||
|
|
label: 'Aguardando aprovação do admin',
|
||
|
|
background: 'rgba(0, 49, 80, 0.1)',
|
||
|
|
color: 'var(--color-primary)',
|
||
|
|
},
|
||
|
|
rejected: {
|
||
|
|
label: 'Reprovado pelo admin',
|
||
|
|
background: 'rgba(181, 31, 31, 0.1)',
|
||
|
|
color: 'var(--color-secondary)',
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
function getTemplateStatus(template) {
|
||
|
|
return statusMeta[template.status] || statusMeta.approved;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getRemainingMetaText(template) {
|
||
|
|
if (template.status !== 'meta_review' || !template.meta_submitted_at) return '';
|
||
|
|
const submittedAt = new Date(template.meta_submitted_at).getTime();
|
||
|
|
const approvedAt = submittedAt + 15 * 60 * 1000;
|
||
|
|
const remainingMs = approvedAt - Date.now();
|
||
|
|
if (remainingMs <= 0) return 'Aprovação fake disponível ao atualizar.';
|
||
|
|
const minutes = Math.ceil(remainingMs / 60000);
|
||
|
|
return `Aprovação fake em aproximadamente ${minutes} min.`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function TemplateManagementPanel({
|
||
|
|
areas = [],
|
||
|
|
mode = 'admin',
|
||
|
|
managedAreaNames = [],
|
||
|
|
isMobile = false,
|
||
|
|
}) {
|
||
|
|
const [templates, setTemplates] = useState([]);
|
||
|
|
const [selectedArea, setSelectedArea] = useState('all');
|
||
|
|
const [form, setForm] = useState({ name: '', content: '', areaId: '' });
|
||
|
|
const [statusMessage, setStatusMessage] = useState('');
|
||
|
|
const [isLoading, setIsLoading] = useState(true);
|
||
|
|
|
||
|
|
const isAdmin = mode === 'admin';
|
||
|
|
const visibleAreas = isAdmin
|
||
|
|
? areas
|
||
|
|
: areas.filter((area) => managedAreaNames.includes(area.nome));
|
||
|
|
|
||
|
|
const filteredTemplates = useMemo(() => {
|
||
|
|
return templates.filter((template) => {
|
||
|
|
const areaMatches = selectedArea === 'all' || String(template.area_id || '') === selectedArea;
|
||
|
|
const supervisorAreaMatches =
|
||
|
|
isAdmin ||
|
||
|
|
!template.area_nome ||
|
||
|
|
managedAreaNames.includes(template.area_nome);
|
||
|
|
return areaMatches && supervisorAreaMatches;
|
||
|
|
});
|
||
|
|
}, [templates, selectedArea, isAdmin, managedAreaNames]);
|
||
|
|
|
||
|
|
async function loadTemplates() {
|
||
|
|
setIsLoading(true);
|
||
|
|
try {
|
||
|
|
const data = await listTemplates();
|
||
|
|
setTemplates(Array.isArray(data) ? data : []);
|
||
|
|
setStatusMessage('');
|
||
|
|
} catch (error) {
|
||
|
|
setStatusMessage(error.message);
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadTemplates();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
async function submitTemplate(event) {
|
||
|
|
event.preventDefault();
|
||
|
|
const name = form.name.trim();
|
||
|
|
const content = form.content.trim();
|
||
|
|
if (!name || !content) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await saveTemplate({
|
||
|
|
name,
|
||
|
|
content,
|
||
|
|
areaId: Number(form.areaId) || null,
|
||
|
|
requestedByRole: isAdmin ? 'admin' : 'supervisor',
|
||
|
|
});
|
||
|
|
setForm({ name: '', content: '', areaId: '' });
|
||
|
|
setStatusMessage(
|
||
|
|
isAdmin
|
||
|
|
? 'Template enviado para aprovação.'
|
||
|
|
: 'Template enviado para aprovação do admin.',
|
||
|
|
);
|
||
|
|
await loadTemplates();
|
||
|
|
} catch (error) {
|
||
|
|
setStatusMessage(error.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function approveTemplate(templateId) {
|
||
|
|
try {
|
||
|
|
await approveTemplateByAdmin(templateId);
|
||
|
|
setStatusMessage('Template aprovado pelo admin e enviado para análise fake da Meta.');
|
||
|
|
await loadTemplates();
|
||
|
|
} catch (error) {
|
||
|
|
setStatusMessage(error.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function rejectTemplate(templateId) {
|
||
|
|
try {
|
||
|
|
await rejectTemplateByAdmin(templateId);
|
||
|
|
setStatusMessage('Template reprovado pelo admin.');
|
||
|
|
await loadTemplates();
|
||
|
|
} catch (error) {
|
||
|
|
setStatusMessage(error.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function removeTemplate(templateId) {
|
||
|
|
try {
|
||
|
|
await deleteTemplate(templateId);
|
||
|
|
setStatusMessage('Template excluído.');
|
||
|
|
await loadTemplates();
|
||
|
|
} catch (error) {
|
||
|
|
setStatusMessage(error.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section style={{ display: 'grid', gap: '1rem' }}>
|
||
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||
|
|
<label style={{ display: 'grid', gap: '0.35rem', width: isMobile ? '100%' : 280 }}>
|
||
|
|
<span style={{ fontWeight: 800 }}>Filtrar por especialidade</span>
|
||
|
|
<select value={selectedArea} onChange={(event) => setSelectedArea(event.target.value)} style={fieldStyle}>
|
||
|
|
<option value="all">Todas as especialidades</option>
|
||
|
|
{visibleAreas.map((area) => (
|
||
|
|
<option key={area.id} value={area.id}>
|
||
|
|
{area.nome}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<DataPanel
|
||
|
|
title={isAdmin ? 'Templates WhatsApp' : 'Solicitar template'}
|
||
|
|
description={
|
||
|
|
isAdmin
|
||
|
|
? 'Crie templates e aprove solicitações de supervisores antes do envio fake para a Meta.'
|
||
|
|
: 'Templates enviados por supervisor passam primeiro pela aprovação do admin.'
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<form onSubmit={submitTemplate} style={{ display: 'grid', gap: '0.85rem' }}>
|
||
|
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 0.8fr) minmax(0, 0.7fr)', gap: '0.85rem' }}>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={form.name}
|
||
|
|
onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))}
|
||
|
|
placeholder="Identificador do template"
|
||
|
|
style={fieldStyle}
|
||
|
|
/>
|
||
|
|
<select
|
||
|
|
value={form.areaId}
|
||
|
|
onChange={(event) => setForm((current) => ({ ...current, areaId: event.target.value }))}
|
||
|
|
style={fieldStyle}
|
||
|
|
>
|
||
|
|
<option value="">Sem especialidade</option>
|
||
|
|
{visibleAreas.map((area) => (
|
||
|
|
<option key={area.id} value={area.id}>
|
||
|
|
{area.nome}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<textarea
|
||
|
|
value={form.content}
|
||
|
|
onChange={(event) => setForm((current) => ({ ...current, content: event.target.value }))}
|
||
|
|
placeholder="Mensagem do template. Ex: Olá, {nome}. Podemos seguir com seu atendimento por aqui?"
|
||
|
|
rows={4}
|
||
|
|
style={{ ...fieldStyle, resize: 'vertical', lineHeight: 1.5 }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
style={{
|
||
|
|
border: 'none',
|
||
|
|
borderRadius: 16,
|
||
|
|
padding: '0.9rem 1rem',
|
||
|
|
background: 'var(--color-primary)',
|
||
|
|
color: '#fff',
|
||
|
|
fontWeight: 800,
|
||
|
|
width: 'fit-content',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{isAdmin ? 'Enviar para aprovação' : 'Enviar para admin'}
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
{statusMessage ? (
|
||
|
|
<div style={{ marginTop: '0.85rem', color: 'var(--color-primary)', fontWeight: 800 }}>
|
||
|
|
{statusMessage}
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</DataPanel>
|
||
|
|
|
||
|
|
<DataPanel title="Lista de templates" description={isLoading ? 'Carregando templates...' : 'Status do fluxo de aprovação.'}>
|
||
|
|
<div style={{ display: 'grid', gap: '0.75rem', maxHeight: 520, overflowY: 'auto', paddingRight: '0.2rem' }}>
|
||
|
|
{filteredTemplates.map((template) => {
|
||
|
|
const status = getTemplateStatus(template);
|
||
|
|
const remainingMetaText = getRemainingMetaText(template);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<article
|
||
|
|
key={template.id}
|
||
|
|
style={{
|
||
|
|
border: '1px solid var(--color-border)',
|
||
|
|
borderRadius: 18,
|
||
|
|
padding: '1rem',
|
||
|
|
background: '#fff',
|
||
|
|
display: 'grid',
|
||
|
|
gap: '0.65rem',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem', flexWrap: 'wrap' }}>
|
||
|
|
<div>
|
||
|
|
<strong style={{ display: 'block' }}>{template.name}</strong>
|
||
|
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem' }}>
|
||
|
|
{template.area_nome || 'Sem especialidade'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
width: 'fit-content',
|
||
|
|
borderRadius: 999,
|
||
|
|
padding: '0.3rem 0.65rem',
|
||
|
|
background: status.background,
|
||
|
|
color: status.color,
|
||
|
|
fontWeight: 800,
|
||
|
|
fontSize: '0.82rem',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{status.label}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<p style={{ margin: 0, color: 'var(--color-text-soft)', lineHeight: 1.5 }}>
|
||
|
|
{template.content}
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{remainingMetaText ? (
|
||
|
|
<span style={{ color: '#8a5a00', fontWeight: 700 }}>{remainingMetaText}</span>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
{isAdmin ? (
|
||
|
|
<div style={{ display: 'flex', gap: '0.55rem', flexWrap: 'wrap' }}>
|
||
|
|
{template.status === 'admin_review' ? (
|
||
|
|
<>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => approveTemplate(template.id)}
|
||
|
|
style={{
|
||
|
|
border: 'none',
|
||
|
|
borderRadius: 14,
|
||
|
|
padding: '0.75rem 0.9rem',
|
||
|
|
background: 'var(--color-primary)',
|
||
|
|
color: '#fff',
|
||
|
|
fontWeight: 800,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Aprovar e enviar para Meta
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => rejectTemplate(template.id)}
|
||
|
|
style={{
|
||
|
|
border: 'none',
|
||
|
|
borderRadius: 14,
|
||
|
|
padding: '0.75rem 0.9rem',
|
||
|
|
background: 'rgba(181, 31, 31, 0.1)',
|
||
|
|
color: 'var(--color-secondary)',
|
||
|
|
fontWeight: 800,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Reprovar
|
||
|
|
</button>
|
||
|
|
</>
|
||
|
|
) : null}
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => removeTemplate(template.id)}
|
||
|
|
style={{
|
||
|
|
border: '1px solid rgba(181, 31, 31, 0.22)',
|
||
|
|
borderRadius: 14,
|
||
|
|
padding: '0.75rem 0.9rem',
|
||
|
|
background: '#fff',
|
||
|
|
color: 'var(--color-secondary)',
|
||
|
|
fontWeight: 800,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Excluir
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</article>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
|
||
|
|
{!filteredTemplates.length ? (
|
||
|
|
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>
|
||
|
|
Nenhum template encontrado para o filtro atual.
|
||
|
|
</span>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
</DataPanel>
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|