fix: SMS campaign — compact cards + pagination 20/page for logs & manual groups

This commit is contained in:
root 2026-07-22 12:33:38 +00:00
parent d8930925be
commit a5817110c9
1 changed files with 58 additions and 8 deletions

View File

@ -123,7 +123,10 @@ export const SmsCampaignPanel = () => {
const [autoRefresh, setAutoRefresh] = useState(true);
const [togglingCampaign, setTogglingCampaign] = useState(null);
const [showCityRules, setShowCityRules] = useState(false);
const [logPage, setLogPage] = useState(1);
const [manualPage, setManualPage] = useState(1);
const refreshTimer = useRef(null);
const PER_PAGE = 20;
const handleOpenGroup = useCallback((groupId) => {
if (groupId) navigate("/dashboard/group/" + groupId);
@ -331,7 +334,7 @@ export const SmsCampaignPanel = () => {
)}
{/* Campaign cards */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4 max-w-4xl">
{CAMPAIGNS.map(c => {
const isActive = activeCampaign === c.key;
const s = allSettings[c.key];
@ -348,7 +351,7 @@ export const SmsCampaignPanel = () => {
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] hover:bg-[var(--color-surface-strong)]"
}`}
onClick={() => { setActiveCampaign(c.key); setFilter("all"); }}
onClick={() => { setActiveCampaign(c.key); setFilter("all"); setLogPage(1); setManualPage(1); }}
>
{/* Header: icon + toggle */}
<div className="flex items-center justify-between">
@ -618,7 +621,10 @@ export const SmsCampaignPanel = () => {
{manualGroups.length === 0 ? (
<div className="px-4 py-6 text-xs text-[var(--color-text-muted)]">Нет групп в ручном управлении</div>
) : (
manualGroups.map((group) => (
(() => {
const start = (manualPage - 1) * PER_PAGE;
const pageGroups = manualGroups.slice(start, start + PER_PAGE);
return pageGroups.map((group) => (
<div
key={group.id}
onClick={() => handleOpenGroup(group.id)}
@ -640,11 +646,31 @@ export const SmsCampaignPanel = () => {
<div className="text-[10px]">{fmtElapsed(group.updated_at)}</div>
</div>
</div>
))
));
})()
)}
</div>
</div>
</Panel>
{/* ── Pagination: manual groups ─────────────────────────────────── */}
{manualGroups.length > PER_PAGE && (
<div className="flex items-center justify-center gap-2">
<button
onClick={() => setManualPage(p => Math.max(1, p - 1))}
disabled={manualPage <= 1}
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1 text-xs font-medium text-[var(--color-text)] hover:bg-[var(--color-surface-strong)] disabled:opacity-40"
> Назад</button>
<span className="text-xs text-[var(--color-text-muted)]">
Стр. {manualPage} / {Math.ceil(manualGroups.length / PER_PAGE)} · {manualGroups.length} записей
</span>
<button
onClick={() => setManualPage(p => Math.min(Math.ceil(manualGroups.length / PER_PAGE), p + 1))}
disabled={manualPage >= Math.ceil(manualGroups.length / PER_PAGE)}
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1 text-xs font-medium text-[var(--color-text)] hover:bg-[var(--color-surface-strong)] disabled:opacity-40"
>Вперёд </button>
</div>
)}
</>
) : (
<>
@ -663,11 +689,11 @@ export const SmsCampaignPanel = () => {
{/* ── Filter + auto-refresh ─────────────────────────────────────────── */}
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap gap-2">
<FilterButton active={filter === "all"} onClick={() => setFilter("all")}>Все ({logs.length})</FilterButton>
<FilterButton active={filter === "all"} onClick={() => { setFilter("all"); setLogPage(1); }}>Все ({logs.length})</FilterButton>
{Object.entries(STATUS_LABELS).map(([status, label]) => {
const count = stats[status] || 0;
if (count === 0) return null;
return <FilterButton key={status} active={filter === status} onClick={() => setFilter(status)}>{label} ({count})</FilterButton>;
return <FilterButton key={status} active={filter === status} onClick={() => { setFilter(status); setLogPage(1); }}>{label} ({count})</FilterButton>;
})}
</div>
<label className="flex items-center gap-1.5 text-xs text-[var(--color-text-muted)]">
@ -692,7 +718,11 @@ export const SmsCampaignPanel = () => {
{logs.length === 0 ? (
<div className="px-4 py-6 text-xs text-[var(--color-text-muted)]">Нет записей</div>
) : (
logs.map((entry) => {
(() => {
const totalPages = Math.ceil(logs.length / PER_PAGE);
const start = (logPage - 1) * PER_PAGE;
const pageLogs = logs.slice(start, start + PER_PAGE);
return pageLogs.map((entry) => {
const canRecheck = entry.status === "sent" || entry.status === "checking";
const isChecking = checkingIds.has(entry.id);
const isTestMode = settings?.test_mode;
@ -736,12 +766,32 @@ export const SmsCampaignPanel = () => {
</div>
</div>
);
})
});
})()
)}
</div>
</div>
</Panel>
{/* ── Pagination: logs ─────────────────────────────────────────────── */}
{logs.length > PER_PAGE && (
<div className="flex items-center justify-center gap-2">
<button
onClick={() => setLogPage(p => Math.max(1, p - 1))}
disabled={logPage <= 1}
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1 text-xs font-medium text-[var(--color-text)] hover:bg-[var(--color-surface-strong)] disabled:opacity-40"
> Назад</button>
<span className="text-xs text-[var(--color-text-muted)]">
Стр. {logPage} / {Math.ceil(logs.length / PER_PAGE)} · {logs.length} записей
</span>
<button
onClick={() => setLogPage(p => Math.min(Math.ceil(logs.length / PER_PAGE), p + 1))}
disabled={logPage >= Math.ceil(logs.length / PER_PAGE)}
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1 text-xs font-medium text-[var(--color-text)] hover:bg-[var(--color-surface-strong)] disabled:opacity-40"
>Вперёд </button>
</div>
)}
<div className="flex justify-end">
<button onClick={loadData} className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 text-xs font-medium text-[var(--color-text)] hover:bg-[var(--color-surface-strong)]">
Обновить