fix: SMS campaign — compact cards + pagination 20/page for logs & manual groups
This commit is contained in:
parent
d8930925be
commit
a5817110c9
|
|
@ -123,7 +123,10 @@ export const SmsCampaignPanel = () => {
|
||||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||||
const [togglingCampaign, setTogglingCampaign] = useState(null);
|
const [togglingCampaign, setTogglingCampaign] = useState(null);
|
||||||
const [showCityRules, setShowCityRules] = useState(false);
|
const [showCityRules, setShowCityRules] = useState(false);
|
||||||
|
const [logPage, setLogPage] = useState(1);
|
||||||
|
const [manualPage, setManualPage] = useState(1);
|
||||||
const refreshTimer = useRef(null);
|
const refreshTimer = useRef(null);
|
||||||
|
const PER_PAGE = 20;
|
||||||
|
|
||||||
const handleOpenGroup = useCallback((groupId) => {
|
const handleOpenGroup = useCallback((groupId) => {
|
||||||
if (groupId) navigate("/dashboard/group/" + groupId);
|
if (groupId) navigate("/dashboard/group/" + groupId);
|
||||||
|
|
@ -331,7 +334,7 @@ export const SmsCampaignPanel = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Campaign cards */}
|
{/* 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 => {
|
{CAMPAIGNS.map(c => {
|
||||||
const isActive = activeCampaign === c.key;
|
const isActive = activeCampaign === c.key;
|
||||||
const s = allSettings[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-accent)] bg-[var(--color-accent-soft)]"
|
||||||
: "border-[var(--color-border)] bg-[var(--color-surface)] hover:bg-[var(--color-surface-strong)]"
|
: "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 */}
|
{/* Header: icon + toggle */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -618,7 +621,10 @@ export const SmsCampaignPanel = () => {
|
||||||
{manualGroups.length === 0 ? (
|
{manualGroups.length === 0 ? (
|
||||||
<div className="px-4 py-6 text-xs text-[var(--color-text-muted)]">Нет групп в ручном управлении</div>
|
<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
|
<div
|
||||||
key={group.id}
|
key={group.id}
|
||||||
onClick={() => handleOpenGroup(group.id)}
|
onClick={() => handleOpenGroup(group.id)}
|
||||||
|
|
@ -640,11 +646,31 @@ export const SmsCampaignPanel = () => {
|
||||||
<div className="text-[10px]">{fmtElapsed(group.updated_at)}</div>
|
<div className="text-[10px]">{fmtElapsed(group.updated_at)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
));
|
||||||
|
})()
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</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 ─────────────────────────────────────────── */}
|
{/* ── Filter + auto-refresh ─────────────────────────────────────────── */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex flex-wrap 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]) => {
|
{Object.entries(STATUS_LABELS).map(([status, label]) => {
|
||||||
const count = stats[status] || 0;
|
const count = stats[status] || 0;
|
||||||
if (count === 0) return null;
|
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>
|
</div>
|
||||||
<label className="flex items-center gap-1.5 text-xs text-[var(--color-text-muted)]">
|
<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 ? (
|
{logs.length === 0 ? (
|
||||||
<div className="px-4 py-6 text-xs text-[var(--color-text-muted)]">Нет записей</div>
|
<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 canRecheck = entry.status === "sent" || entry.status === "checking";
|
||||||
const isChecking = checkingIds.has(entry.id);
|
const isChecking = checkingIds.has(entry.id);
|
||||||
const isTestMode = settings?.test_mode;
|
const isTestMode = settings?.test_mode;
|
||||||
|
|
@ -736,12 +766,32 @@ export const SmsCampaignPanel = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
|
})()
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</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">
|
<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)]">
|
<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)]">
|
||||||
↻ Обновить
|
↻ Обновить
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue