FEAT: Melhora distribuição e incrementa os templates

This commit is contained in:
Rafael Alves Lopes 2026-05-22 10:51:07 -03:00
parent 4d287faf28
commit fbdbca7f20
9 changed files with 573 additions and 25 deletions

View File

@ -219,7 +219,7 @@ export function NewAttendancePage() {
if (!isMounted) return;
setContacts(Array.isArray(contactsData) ? contactsData.map(normalizeAgendaContact) : []);
const supportedTemplates = Array.isArray(templatesData)
? templatesData.filter((template) => !requiresUnsupportedTemplateFields(template))
? templatesData.filter((template) => template.status === 'approved' && !requiresUnsupportedTemplateFields(template))
: [];
setTemplates(supportedTemplates);
setSelectedTemplateId((current) => current || (supportedTemplates?.[0]?.id ? String(supportedTemplates[0].id) : ''));

View File

@ -23,26 +23,69 @@ function ChannelBadge({ channel }) {
);
}
function LastMessageDot({ fromMe }) {
const color = fromMe ? '#e5a22a' : '#00a4b7';
const label = fromMe ? 'Última mensagem enviada pelo atendimento' : 'Última mensagem enviada pelo cliente';
function AssignmentDot({ contact, currentUserId }) {
const assignment = contact.assignment;
const assignedUserId = assignment?.user_id ? Number(assignment.user_id) : null;
const isQueued = assignment?.status === 'queued' && !assignedUserId;
const isMine = assignedUserId && currentUserId && assignedUserId === Number(currentUserId);
const meta = isQueued
? {
color: '#e5a22a',
label: 'Chamado na fila da especialidade, ainda sem atribuição',
}
: isMine
? {
color: '#00a4b7',
label: 'Chamado atribuído a mim',
}
: assignedUserId
? {
color: '#d62828',
label: `Chamado atribuído a ${assignment?.user_nome || 'outra pessoa'}`,
}
: null;
if (!meta) return null;
return (
<span
title={label}
aria-label={label}
title={meta.label}
aria-label={meta.label}
style={{
width: 10,
height: 10,
borderRadius: 999,
background: color,
boxShadow: `0 0 0 3px ${color}22`,
background: meta.color,
boxShadow: `0 0 0 3px ${meta.color}22`,
flex: '0 0 auto',
}}
/>
);
}
function SpecialtyBadge({ contact }) {
const specialty = contact.assignment?.area_nome || contact.area;
if (!specialty || specialty === 'Sem fila') return null;
return (
<span
title={`Especialidade: ${specialty}`}
style={{
color: 'var(--color-primary)',
flex: '0 0 auto',
fontSize: '0.72rem',
fontWeight: 800,
lineHeight: 1,
borderRadius: 999,
padding: '0.2rem 0.5rem',
background: 'rgba(0, 49, 80, 0.08)',
}}
>
{specialty}
</span>
);
}
function UnreadBadge({ count }) {
if (!count) return null;
@ -83,7 +126,7 @@ function SavedContactLabel({ contact }) {
lineHeight: 1,
}}
>
Salvo
Salvo
</span>
);
}
@ -95,6 +138,7 @@ export function ChatConversationList({
activeContactId,
onSelectContact,
onOpenContact,
currentUserId,
isMobile = false,
}) {
return (
@ -158,7 +202,7 @@ export function ChatConversationList({
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', minWidth: 0 }}>
<LastMessageDot fromMe={contact.lastMessageFromMe} />
<AssignmentDot contact={contact} currentUserId={currentUserId} />
<strong style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{contact.name}
</strong>
@ -170,6 +214,7 @@ export function ChatConversationList({
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', minWidth: 0 }}>
<ChannelBadge channel={contact.channel} />
<SpecialtyBadge contact={contact} />
<SavedContactLabel contact={contact} />
</span>
<UnreadBadge count={contact.unread} />

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useWhatsappSocket } from '../../../shared/hooks/useWhatsappSocket';
import { API_BASE_URL } from '../../../shared/services/apiConfig';
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
import { getCurrentUser } from '../../auth/services/sessionService';
import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService';
import { chatContacts, transferAreas as fallbackTransferAreas } from '../services/chatMocks';
function buildInitialMessages() {
@ -212,8 +212,10 @@ function getUserDisplayName(user) {
export function useChat() {
const currentUser = getCurrentUser();
const currentUserProfile = getCurrentUserProfile();
const currentUserId = getUserId(currentUser);
const currentUserAreas = getUserAreas(currentUser);
const isAdminUser = currentUserProfile === 'admin';
const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket();
const [contacts, setContacts] = useState(buildFallbackContacts);
const [activeContactId, setActiveContactId] = useState(chatContacts[0].id);
@ -261,7 +263,7 @@ export function useChat() {
const isWaitingCustomerReply = Boolean(activeAssignment?.awaiting_customer_reply);
const isQueuedForUserArea = Boolean(
activeAssignment?.status === 'queued' &&
(!activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)),
(isAdminUser || !activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)),
);
const canAssumeChat = Boolean(activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea);
const canReply = Boolean(isAssignedToCurrentUser && !isWaitingCustomerReply);
@ -314,6 +316,9 @@ export function useChat() {
}, []);
function canSeeContact(contact) {
if (isAdminUser) {
return Boolean(contact.assignment && contact.assignment.status !== 'bot_triage');
}
if (!currentUserAreas.length) return false;
if (!contact.assignment) return false;
if (contact.assignment.status === 'bot_triage') return false;
@ -369,7 +374,7 @@ export function useChat() {
isMounted = false;
window.clearInterval(intervalId);
};
}, [currentUserId, currentUserAreas.join('|'), whatsappStatus]);
}, [currentUserId, currentUserAreas.join('|'), isAdminUser, whatsappStatus]);
useEffect(() => {
if (!activeContactId) return;
@ -724,6 +729,7 @@ export function useChat() {
}
return {
currentUserId,
contacts,
activeContact,
activeContactId,

View File

@ -13,6 +13,7 @@ export function ChatPage() {
const [searchParams, setSearchParams] = useSearchParams();
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
const {
currentUserId,
contacts,
activeContact,
activeContactId,
@ -144,6 +145,7 @@ export function ChatPage() {
setIsTransferOpen(false);
setIsContactPanelOpen(true);
}}
currentUserId={currentUserId}
isMobile={isMobile}
/>

View File

@ -4,11 +4,15 @@ import { clearSession } from '../../auth/services/sessionService';
const navigationBySection = {
supervisor: [
{ id: 'dashboard', label: 'Dashboard', count: null },
{ id: 'queues', label: 'Filas em tempo real', count: 42 },
{ id: 'areas', label: 'Especialidades supervisionadas', count: 3 },
{ id: 'agents', label: 'Agentes online', count: 18 },
{ id: 'reports', label: 'Relatórios', count: null },
{ id: 'dashboard', label: 'Home' },
{ id: 'templates', label: 'Templates' },
{ id: 'knowledge', label: 'Base de conhecimento IA' },
{ id: 'audit', label: 'Auditoria' },
{ type: 'separator' },
{ id: 'attendance', label: 'Atendimento' },
{ id: 'new-attendance', label: 'Abrir Atendimento', path: '/new-attendance' },
{ id: 'mass-message', label: 'Disparo em Massa' },
{ id: 'contacts', label: 'Contatos' },
],
admin: [
{ id: 'home', label: 'Home' },
@ -31,7 +35,7 @@ const navigationBySection = {
const actionLabelBySection = {
supervisor: '+ Redistribuir atendimento',
admin: '+ Nova configuração',
admin: 'Home',
};
export function ManagementLayout({
@ -48,7 +52,7 @@ export function ManagementLayout({
}) {
const navigate = useNavigate();
const navItems = navigationBySection[activeSection] || navigationBySection.supervisor;
const actionLabel = actionLabelBySection[activeSection] || '+ Nova ação';
const actionLabel = actionLabelBySection[activeSection] || 'Home';
function handleLogout() {
clearSession();

View File

@ -0,0 +1,347 @@
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>
);
}

View File

@ -4,6 +4,7 @@ import { ManagementLayout } from '../components/ManagementLayout';
import { ManagementTable } from '../components/ManagementTable';
import { MetricGrid } from '../components/MetricGrid';
import { OperationalDashboard } from '../components/OperationalDashboard';
import { TemplateManagementPanel } from '../components/TemplateManagementPanel';
import { aiContentRows, areaRows, userRows } from '../services/managementMocks';
import { AttendantOpsPanel } from '../../home/components/AttendantOpsPanel';
import { MessagesWorkspace } from '../../home/components/MessagesWorkspace';
@ -111,7 +112,7 @@ function toHomeConversation(contact, messages = []) {
};
}
function AdminAttendanceWorkspace({ isWideDesktop, isDesktop, isTablet, isMobile }) {
export function AdminAttendanceWorkspace({ isWideDesktop, isDesktop, isTablet, isMobile }) {
const {
contacts,
activeContactId,
@ -865,7 +866,7 @@ export function AdminPage() {
home: renderMonthlyHome(),
today: <OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />,
'users-access': renderUsersAccess(),
templates: renderPlaceholder('Templates', 'Gestão de templates aprovados pela Meta.'),
templates: <TemplateManagementPanel areas={areas} mode="admin" isMobile={isMobile} />,
knowledge: (
<DataPanel title="Base de conhecimento IA" description="Entradas para alimentar a base de conhecimento.">
<ManagementTable columns={contentColumns} rows={aiContentRows} getRowId={(row) => row.id} isMobile={isMobile} />

View File

@ -1,11 +1,99 @@
import { useEffect, useState } from 'react';
import { ManagementLayout } from '../components/ManagementLayout';
import { OperationalDashboard } from '../components/OperationalDashboard';
import { TemplateManagementPanel } from '../components/TemplateManagementPanel';
import { DataPanel } from '../components/DataPanel';
import { ManagementTable } from '../components/ManagementTable';
import { aiContentRows } from '../services/managementMocks';
import { getAccessOptions } from '../services/adminAccessService';
import { useViewport } from '../../../shared/hooks/useViewport';
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
import { getCurrentUser, getCurrentUserDisplay } from '../../auth/services/sessionService';
import { AdminAttendanceWorkspace } from './AdminPage';
function getUserSpecialties(user) {
const normalize = (area) => {
if (!area) return null;
if (typeof area === 'string') return area;
return area.nome || area.name || null;
};
const areas = Array.isArray(user?.areas) ? user.areas.map(normalize).filter(Boolean) : [];
const primary = normalize(user?.areaPrincipal);
return primary && !areas.includes(primary) ? [primary, ...areas] : areas;
}
export function SupervisorPage() {
const { isDesktop, isMobile } = useViewport();
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
const userDisplay = getCurrentUserDisplay();
const currentUser = getCurrentUser();
const managedSpecialties = getUserSpecialties(currentUser);
const [activeSection, setActiveSection] = useState('dashboard');
const [areas, setAreas] = useState([]);
useEffect(() => {
let isMounted = true;
getAccessOptions()
.then((options) => {
if (isMounted) setAreas(options.areas || []);
})
.catch(() => {
if (isMounted) setAreas([]);
});
return () => {
isMounted = false;
};
}, []);
function renderPlaceholder(title, description) {
return (
<DataPanel title={title} description={description}>
<div style={{ border: '1px solid var(--color-border)', borderRadius: 18, padding: '1rem', background: '#fff', color: 'var(--color-text-soft)', fontWeight: 700 }}>
Seção em preparação.
</div>
</DataPanel>
);
}
const contentColumns = [
{ key: 'title', label: 'Conteúdo' },
{ key: 'area', label: 'Especialidade' },
{ key: 'status', label: 'Status' },
{ key: 'updatedAt', label: 'Atualizado' },
];
const filteredKnowledgeRows = managedSpecialties.length
? aiContentRows.filter((row) => managedSpecialties.includes(row.area))
: aiContentRows;
const sectionContent = {
dashboard: <OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />,
templates: (
<TemplateManagementPanel
areas={areas}
mode="supervisor"
managedAreaNames={managedSpecialties}
isMobile={isMobile}
/>
),
knowledge: (
<DataPanel title="Base de conhecimento" description="Conteúdos da IA para as especialidades supervisionadas.">
<ManagementTable columns={contentColumns} rows={filteredKnowledgeRows} getRowId={(row) => row.id} isMobile={isMobile} />
</DataPanel>
),
audit: renderPlaceholder('Auditoria', 'Eventos do time supervisionado serão consolidados aqui.'),
attendance: (
<AdminAttendanceWorkspace
isWideDesktop={isWideDesktop}
isDesktop={isDesktop}
isTablet={isTablet}
isMobile={isMobile}
/>
),
'mass-message': renderPlaceholder('Disparo em massa', 'Fluxo de disparos por templates aprovados.'),
contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
};
return (
<ManagementLayout
@ -16,8 +104,10 @@ export function SupervisorPage() {
initials={userDisplay.initials}
isDesktop={isDesktop}
isMobile={isMobile}
activeNavItem={activeSection}
onNavItemChange={setActiveSection}
>
<OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />
{sectionContent[activeSection] || sectionContent.dashboard}
</ManagementLayout>
);
}

View File

@ -0,0 +1,53 @@
import { API_BASE_URL } from '../../../shared/services/apiConfig';
async function request(path, options = {}) {
const response = await fetch(`${API_BASE_URL}${path}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error('Falha ao consultar templates.');
}
return response.json();
}
export function listTemplates() {
return request('/whatsapp/templates');
}
export function saveTemplate(payload) {
return request('/whatsapp/templates', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export function updateTemplate(id, payload) {
return request(`/whatsapp/templates/update/${id}`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export function approveTemplateByAdmin(id) {
return request(`/whatsapp/templates/approve-admin/${id}`, {
method: 'POST',
});
}
export function rejectTemplateByAdmin(id) {
return request(`/whatsapp/templates/reject-admin/${id}`, {
method: 'POST',
});
}
export function deleteTemplate(id) {
return request(`/whatsapp/templates/${id}`, {
method: 'DELETE',
});
}