feat(admin): add never-subscribed broadcast target with audience counts
Add a broadcast audience for users who registered but never had any subscription or trial (no Subscription rows at all), backed by a new get_user_ids_without_any_subscription DAL helper and a 'never' target in the webapp broadcast route. Add GET /api/admin/broadcast/audience-counts so the audience dropdown shows the recipient count next to each option, with graceful fallback when counts are unavailable.
This commit is contained in:
@@ -1,16 +1,27 @@
|
||||
<script>
|
||||
import { Textarea } from "$components/ui/index.js";
|
||||
import { Send } from "$components/ui/icons.js";
|
||||
import { getContext } from "svelte";
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { Label } from "$components/ui/primitives.js";
|
||||
import { AdminButton, AdminSelect } from "$components/patterns/admin/index.js";
|
||||
|
||||
export let at;
|
||||
const broadcastStore = getContext("broadcastStore");
|
||||
|
||||
$: ({ broadcastTarget, broadcastText, broadcastBusy, broadcastResult } = $broadcastStore);
|
||||
$: ({ broadcastTarget, broadcastText, broadcastBusy, broadcastResult, broadcastCounts } =
|
||||
$broadcastStore);
|
||||
|
||||
const BROADCAST_TARGET_OPTIONS = broadcastStore.BROADCAST_TARGET_OPTIONS;
|
||||
|
||||
// Append the resolved audience size to each option once counts are loaded.
|
||||
$: targetOptions = BROADCAST_TARGET_OPTIONS.map((option) => {
|
||||
const count = broadcastCounts?.[option.value];
|
||||
return count == null ? option : { ...option, label: `${option.label} (${count})` };
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
broadcastStore.loadCounts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="admin-card">
|
||||
@@ -24,7 +35,7 @@
|
||||
<span>{at("broadcast_label_audience", {}, "Аудитория")}</span>
|
||||
<AdminSelect
|
||||
value={broadcastTarget}
|
||||
items={BROADCAST_TARGET_OPTIONS}
|
||||
items={targetOptions}
|
||||
ariaLabel={at("broadcast_label_audience", {}, "Аудитория")}
|
||||
onValueChange={(value) => broadcastStore.updateField({ broadcastTarget: value })}
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,7 @@ export function createBroadcastStore({ api, onToast, at }) {
|
||||
broadcastText: "",
|
||||
broadcastBusy: false,
|
||||
broadcastResult: null,
|
||||
broadcastCounts: null,
|
||||
});
|
||||
|
||||
const BROADCAST_TARGET_OPTIONS = [
|
||||
@@ -13,8 +14,23 @@ export function createBroadcastStore({ api, onToast, at }) {
|
||||
{ value: "active", label: at("broadcast_target_active", {}, "С подпиской") },
|
||||
{ value: "inactive", label: at("broadcast_target_inactive", {}, "Без подписки") },
|
||||
{ value: "expired", label: at("broadcast_target_expired", {}, "Expired subscription") },
|
||||
{
|
||||
value: "never",
|
||||
label: at("broadcast_target_never", {}, "Без подписки и без истории"),
|
||||
},
|
||||
];
|
||||
|
||||
async function loadCounts() {
|
||||
try {
|
||||
const res = await api("/admin/broadcast/audience-counts");
|
||||
if (res?.ok && res.counts) {
|
||||
state.update((s) => ({ ...s, broadcastCounts: res.counts }));
|
||||
}
|
||||
} catch {
|
||||
// Counts are advisory; ignore failures and keep plain labels.
|
||||
}
|
||||
}
|
||||
|
||||
async function runBroadcast() {
|
||||
let text = "";
|
||||
let target = "";
|
||||
@@ -56,6 +72,7 @@ export function createBroadcastStore({ api, onToast, at }) {
|
||||
update: state.update,
|
||||
runBroadcast,
|
||||
updateField,
|
||||
loadCounts,
|
||||
BROADCAST_TARGET_OPTIONS,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -658,6 +658,12 @@ function demoApiResponse(path, cleanPath, options, context) {
|
||||
const params = queryParams(path);
|
||||
|
||||
if (cleanPath === "/admin/stats") return clone(DEMO_DATASET.stats);
|
||||
if (cleanPath === "/admin/broadcast/audience-counts") {
|
||||
return {
|
||||
ok: true,
|
||||
counts: { all: 1280, active: 742, inactive: 538, expired: 311, never: 227 },
|
||||
};
|
||||
}
|
||||
if (cleanPath === "/admin/sync") return { ok: true, status: "queued" };
|
||||
|
||||
if (cleanPath === "/admin/payments") {
|
||||
|
||||
Reference in New Issue
Block a user