Constrain campaign preview list
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m38s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m38s
This commit is contained in:
@@ -19,6 +19,8 @@ type StatusVisual = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
type PreviewFilter = 'all' | 'ready' | 'near' | 'below';
|
||||
|
||||
const statusStyles: Record<CampaignStatus, StatusVisual> = {
|
||||
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' },
|
||||
@@ -42,6 +44,8 @@ const formatDate = (value?: string | null) => {
|
||||
|
||||
const formatDelta = (value: number) => `${value} un.`;
|
||||
|
||||
const NEAR_THRESHOLD_PERCENT = 80;
|
||||
|
||||
const statusPillStyle = (status: CampaignStatus) => {
|
||||
const style = statusStyles[status];
|
||||
return {
|
||||
@@ -108,6 +112,7 @@ const Campaigns = () => {
|
||||
const [processResult, setProcessResult] = useState<CampaignProcessSummary | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [previewFilter, setPreviewFilter] = useState<PreviewFilter>('all');
|
||||
|
||||
const loadCampaigns = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -147,6 +152,37 @@ const Campaigns = () => {
|
||||
return [...ready, ...below].sort((a, b) => Number(b.isReady) - Number(a.isReady) || b.total_delta - a.total_delta);
|
||||
}, [preview]);
|
||||
|
||||
const previewProductRows = useMemo(() => {
|
||||
return previewProducts.map(product => {
|
||||
const progress = threshold ? Math.min(100, (product.total_delta / threshold) * 100) : 0;
|
||||
return {
|
||||
...product,
|
||||
progress,
|
||||
isNear: !product.isReady && progress >= NEAR_THRESHOLD_PERCENT
|
||||
};
|
||||
});
|
||||
}, [previewProducts, threshold]);
|
||||
|
||||
const filteredPreviewProducts = useMemo(() => {
|
||||
switch (previewFilter) {
|
||||
case 'ready':
|
||||
return previewProductRows.filter(product => product.isReady);
|
||||
case 'near':
|
||||
return previewProductRows.filter(product => product.isNear);
|
||||
case 'below':
|
||||
return previewProductRows.filter(product => !product.isReady);
|
||||
default:
|
||||
return previewProductRows;
|
||||
}
|
||||
}, [previewFilter, previewProductRows]);
|
||||
|
||||
const previewFilterOptions = useMemo(() => [
|
||||
{ key: 'all' as const, label: 'Todos', count: previewProductRows.length },
|
||||
{ key: 'ready' as const, label: 'Prontos', count: previewProductRows.filter(product => product.isReady).length },
|
||||
{ key: 'near' as const, label: 'Quase prontos', count: previewProductRows.filter(product => product.isNear).length },
|
||||
{ key: 'below' as const, label: 'Abaixo', count: previewProductRows.filter(product => !product.isReady).length }
|
||||
], [previewProductRows]);
|
||||
|
||||
const resultStats = processResult ? [
|
||||
{ label: 'Processados', value: processResult.claimed, color: '#25C2FF' },
|
||||
{ label: 'Enviados', value: processResult.sentGroups, color: '#52DFA0' },
|
||||
@@ -264,9 +300,26 @@ const Campaigns = () => {
|
||||
<p className="text-xl font-bold text-dark-text">{summary?.threshold ?? preview?.threshold ?? 100}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{previewProducts.map(product => {
|
||||
const progress = threshold ? Math.min(100, (product.total_delta / threshold) * 100) : 0;
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{previewFilterOptions.map(option => (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
onClick={() => setPreviewFilter(option.key)}
|
||||
className={`inline-flex h-8 cursor-pointer items-center gap-2 rounded-lg border px-3 text-xs font-bold transition-colors ${
|
||||
previewFilter === option.key
|
||||
? 'border-brand-primary bg-brand-primary/10 text-dark-text'
|
||||
: 'border-dark-border bg-dark-input/45 text-dark-muted hover:text-dark-text'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
<span className="rounded-full bg-dark-card px-1.5 py-0.5 text-[10px] text-dark-muted">{option.count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-h-[34rem] space-y-2 overflow-y-auto pr-1">
|
||||
{filteredPreviewProducts.map(product => {
|
||||
const accent = product.isReady ? statusStyles.sent.accent : statusStyles.pending.accent;
|
||||
|
||||
return (
|
||||
@@ -287,18 +340,18 @@ const Campaigns = () => {
|
||||
className="rounded-full border px-2.5 py-1 text-[11px] font-bold"
|
||||
style={product.isReady ? statusPillStyle('sent') : statusPillStyle('pending')}
|
||||
>
|
||||
{product.isReady ? 'Pronto' : `${Math.round(progress)}%`}
|
||||
{product.isReady ? 'Pronto' : `${Math.round(product.progress)}%`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 h-1.5 overflow-hidden rounded-full bg-dark-border/70">
|
||||
<div className="h-full rounded-full" style={{ width: `${progress}%`, backgroundColor: accent }} />
|
||||
<div className="h-full rounded-full" style={{ width: `${product.progress}%`, backgroundColor: accent }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!previewProducts.length && (
|
||||
{!filteredPreviewProducts.length && (
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/45 p-4 text-sm font-semibold text-dark-muted">
|
||||
Nenhum produto para campanha no momento.
|
||||
Nenhum produto nesta visualização.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user