diff --git a/src/pages/Campaigns.tsx b/src/pages/Campaigns.tsx index 9634003..93baff6 100644 --- a/src/pages/Campaigns.tsx +++ b/src/pages/Campaigns.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; -import { AlertTriangle, CheckCircle2, Clock, Megaphone, RefreshCw, RotateCcw, Send, XCircle } from 'lucide-react'; +import { AlertTriangle, CheckCircle2, Clock, Megaphone, RefreshCw, RotateCcw, Send, Users, XCircle } from 'lucide-react'; +import RefreshStatus from '../components/RefreshStatus'; import type { CampaignGroup, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CampaignStatus } from '../types'; import { fetchCampaignPreview, fetchCampaigns, processCampaignsNow, retryCampaignGroup } from '../dataService'; @@ -11,12 +12,19 @@ const statusLabels: Record = { skipped: 'Ignorada' }; -const statusStyles: Record = { - pending: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20', - processing: 'bg-blue-500/10 text-blue-400 border-blue-500/20', - sent: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', - failed: 'bg-red-500/10 text-red-400 border-red-500/20', - skipped: 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20' +type StatusVisual = { + accent: string; + bg: string; + border: string; + text: string; +}; + +const statusStyles: Record = { + pending: { accent: '#FFC247', bg: 'rgba(255, 194, 71, 0.10)', border: 'rgba(255, 194, 71, 0.28)', text: '#FFC247' }, + processing: { accent: '#25C2FF', bg: 'rgba(37, 194, 255, 0.10)', border: 'rgba(37, 194, 255, 0.28)', text: '#25C2FF' }, + sent: { accent: '#52DFA0', bg: 'rgba(82, 223, 160, 0.10)', border: 'rgba(82, 223, 160, 0.28)', text: '#52DFA0' }, + failed: { accent: '#FF7A9B', bg: 'rgba(255, 122, 155, 0.11)', border: 'rgba(255, 122, 155, 0.32)', text: '#FF7A9B' }, + skipped: { accent: '#8c9298', bg: 'rgba(140, 146, 152, 0.08)', border: 'rgba(140, 146, 152, 0.24)', text: '#b3b7bb' } }; const statusIcons: Record = { @@ -34,6 +42,66 @@ const formatDate = (value?: string | null) => { const formatDelta = (value: number) => `${value} un.`; +const statusPillStyle = (status: CampaignStatus) => { + const style = statusStyles[status]; + return { + backgroundColor: style.bg, + borderColor: style.border, + color: style.text + }; +}; + +const CampaignsSkeleton = () => ( +
+
+ {[0, 1, 2, 3, 4].map(item => ( +
+
+
+
+
+
+
+ ))} +
+ +
+ {[0, 1].map(section => ( +
+
+
+ {[0, 1, 2, 3].map(item => ( +
+
+
+
+ ))} +
+
+ ))} +
+ +
+
+
+ {[0, 1, 2, 3, 4, 5, 6].map(item => ( +
+ ))} +
+
+
+ {[0, 1, 2, 3, 4, 5].map(row => ( +
+ {[0, 1, 2, 3, 4, 5, 6].map(column => ( +
+ ))} +
+ ))} +
+
+
+); + const Campaigns = () => { const [summary, setSummary] = useState(null); const [preview, setPreview] = useState(null); @@ -72,6 +140,31 @@ const Campaigns = () => { return counts; }, [summary]); + const threshold = summary?.threshold ?? preview?.threshold ?? 100; + const previewProducts = useMemo(() => { + const ready = (preview?.readyProducts || []).map(product => ({ ...product, isReady: true })); + const below = (preview?.belowThresholdProducts || []).map(product => ({ ...product, isReady: false })); + return [...ready, ...below].sort((a, b) => Number(b.isReady) - Number(a.isReady) || b.total_delta - a.total_delta); + }, [preview]); + + const resultStats = processResult ? [ + { label: 'Processados', value: processResult.claimed, color: '#25C2FF' }, + { label: 'Enviados', value: processResult.sentGroups, color: '#52DFA0' }, + { label: 'Falhas', value: processResult.failedGroups, color: '#FF7A9B' }, + { label: 'Abaixo do limite', value: processResult.pendingBelowThresholdGroups, color: '#FFC247' } + ] : []; + + const statusSummary = (Object.keys(statusLabels) as CampaignStatus[]).map(status => ({ + status, + label: statusLabels[status], + count: groupedCounts[status], + Icon: statusIcons[status], + style: statusStyles[status] + })); + + const shouldShowSkeleton = isLoading && !summary && !preview; + const isRefreshing = isLoading && Boolean(summary || preview); + const handleProcessNow = async () => { setIsProcessing(true); const result = await processCampaignsNow(); @@ -114,21 +207,35 @@ const Campaigns = () => {
+ + {processResult && ( -
- Resultado: {processResult.claimed} itens processados, {processResult.sentGroups} grupos enviados, {processResult.failedGroups} falhas, {processResult.pendingBelowThresholdGroups} abaixo do limite. +
+
+ {resultStats.map(item => ( +
+ + + {item.label} + + {item.value} +
+ ))} +
)} + {shouldShowSkeleton ? ( + + ) : ( +
- {Object.entries(groupedCounts).map(([status, count]) => { - const typedStatus = status as CampaignStatus; - const Icon = statusIcons[typedStatus]; + {statusSummary.map(({ status, label, count, Icon, style }) => { return (
- {statusLabels[typedStatus]} - + {label} +

{count}

@@ -158,35 +265,67 @@ const Campaigns = () => {
- {(preview?.readyProducts || []).map(product => ( -
- {product.baseProduct} - {formatDelta(product.total_delta)} + {previewProducts.map(product => { + const progress = threshold ? Math.min(100, (product.total_delta / threshold) * 100) : 0; + const accent = product.isReady ? statusStyles.sent.accent : statusStyles.pending.accent; + + return ( +
+
+
+

+ {product.baseProduct} +

+

+ {formatDelta(product.total_delta)} de {formatDelta(threshold)} +

+
+ + {product.isReady ? 'Pronto' : `${Math.round(progress)}%`} + +
+
+
+
- ))} - {(preview?.belowThresholdProducts || []).map(product => ( -
- {product.baseProduct} - {formatDelta(product.total_delta)} + ); + })} + {!previewProducts.length && ( +
+ Nenhum produto para campanha no momento.
- ))} + )}
-

Top clientes da campanha

-
- {(preview?.customersPreview || []).map(customer => ( -
+
+ +

Top clientes da campanha

+
+
+ {(preview?.customersPreview || []).map((customer, index) => ( +
+
+ {index + 1} +

{customer.nome}

{customer.fone}

- {customer.total_comprado || 0} un. + {customer.total_comprado || 0} un.
))} - {!preview?.customersPreview.length &&

Nenhum cliente com telefone válido encontrado.

} + {!preview?.customersPreview.length && ( +
Nenhum cliente com telefone válido encontrado.
+ )}
@@ -215,7 +354,10 @@ const Campaigns = () => { {group.lastError &&

{group.lastError}

} - + {statusLabels[group.status]} @@ -246,6 +388,8 @@ const Campaigns = () => {
Nenhuma campanha registrada ainda.
)}
+
+ )}
); };