feat(orders): show client push subscription and delivery log

This commit is contained in:
root 2026-08-06 12:06:11 +00:00
parent ad7b92503c
commit 82d6f4f077
2 changed files with 93 additions and 4 deletions

View File

@ -1,8 +1,8 @@
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
if (!isLocalhost) {
const STATIC_CACHE = "construction-delivery-static-v95";
const RUNTIME_CACHE = "construction-delivery-runtime-v95";
const STATIC_CACHE = "construction-delivery-static-v96";
const RUNTIME_CACHE = "construction-delivery-runtime-v96";
const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
self.addEventListener("install", (event) => {

View File

@ -390,6 +390,8 @@ const StatusSummary = ({ order }) => {
export const OrderHistoryTimeline = ({ order, userRole }) => {
const [history, setHistory] = useState(null);
const [smsLogs, setSmsLogs] = useState([]);
const [pushLogs, setPushLogs] = useState([]);
const [pushSubscriptions, setPushSubscriptions] = useState([]);
const [invitation, setInvitation] = useState(null);
useEffect(() => {
@ -419,6 +421,34 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
if (!smsError) setSmsLogs(smsData || []);
} catch { if (!cancelled) setSmsLogs([]); }
// Push is stored on the client phone, so it is available for every current and future order.
const phoneNormalized = String(order?.customerPhoneNormalized || order?.customer_phone_normalized || order?.customerPhone || order?.customer_phone || "").replace(/\D/g, "").replace(/^8/, "7");
if (phoneNormalized) {
try {
const { data: pushData, error: pushError } = await supabase
.from("push_log")
.select("id, status, title, body, url, error_message, created_at")
.eq("phone_normalized", phoneNormalized)
.order("created_at", { ascending: false })
.limit(50);
if (cancelled) return;
if (!pushError) setPushLogs(pushData || []);
} catch { if (!cancelled) setPushLogs([]); }
try {
const { data: subscriptionData, error: subscriptionError } = await supabase
.from("push_subscriptions")
.select("id, is_active, created_at, last_sent_at, last_seen_at")
.eq("phone_normalized", phoneNormalized)
.order("created_at", { ascending: false });
if (cancelled) return;
if (!subscriptionError) setPushSubscriptions(subscriptionData || []);
} catch { if (!cancelled) setPushSubscriptions([]); }
} else {
setPushLogs([]);
setPushSubscriptions([]);
}
try {
const { data: invData, error: invError } = await supabase
.from("delivery_invitations")
@ -473,6 +503,21 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
}
}
if (pushLogs && pushLogs.length > 0) {
for (const push of pushLogs) {
events.push({
id: `push-${push.id}`,
created_at: push.created_at,
type: "push",
label: push.status === "sent" ? "Push-уведомление отправлено" : "Ошибка отправки Push",
pushStatus: push.status,
pushTitle: push.title || "СуперСам",
pushBody: push.body || "",
pushError: push.error_message || null,
});
}
}
if (invitation) {
if (invitation.sent_at) {
events.push({ id: `inv-sent-${invitation.id}`, created_at: invitation.sent_at, type: "invitation", label: "Приглашение отправлено" });
@ -487,14 +532,42 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
events.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
return events;
}, [history, smsLogs, invitation]);
}, [history, smsLogs, pushLogs, invitation]);
if (!history && smsLogs.length === 0 && !invitation) return null;
const activePushSubscriptions = pushSubscriptions.filter((sub) => sub.is_active);
const latestPush = pushLogs[0] || null;
if (!history && smsLogs.length === 0 && pushLogs.length === 0 && !invitation) return null;
return (
<div className="space-y-4">
<ProgressStepper order={order} />
<StatusSummary order={order} />
<div className={`rounded-xl border px-3 py-2.5 ${
activePushSubscriptions.length > 0
? "border-[rgba(18,128,92,0.25)] bg-[var(--color-accent-soft)]"
: "border-[var(--color-border)] bg-[var(--color-surface)]"
}`}>
<div className="flex items-start gap-2.5">
<span className="mt-0.5 text-base">{activePushSubscriptions.length > 0 ? "🔔" : "🔕"}</span>
<div className="min-w-0 flex-1">
<p className="text-[11px] font-medium text-[var(--color-text-muted)]">Push-уведомления клиента</p>
{activePushSubscriptions.length > 0 ? (
<>
<p className="text-sm font-semibold text-[var(--color-accent)]">
Подписан · устройств: {activePushSubscriptions.length}
</p>
<p className="text-xs text-[var(--color-text-muted)]">
Подписка: {fmtTimestamp(activePushSubscriptions[0]?.created_at)}
{latestPush ? ` · последнее push: ${fmtTimestamp(latestPush.created_at)}` : " · уведомлений ещё не было"}
</p>
</>
) : (
<p className="text-sm font-semibold text-[var(--color-text-muted)]">Не подписан</p>
)}
</div>
</div>
</div>
{allEvents.length > 0 && (
<ol className="relative space-y-3 pl-4">
{allEvents.map((ev, idx) => (
@ -533,6 +606,22 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
</>
)}
{ev.type === "push" && (
<>
<p className="mt-0.5 text-sm">
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold ${
ev.pushStatus === "sent"
? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]"
: "bg-[rgba(239,68,68,0.12)] text-[var(--color-danger)]"
}`}>
{ev.pushStatus === "sent" ? "принято сервисом Push" : "не отправлено"}
</span>
</p>
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">«{ev.pushTitle}{ev.pushBody ? `${ev.pushBody}` : ""}»</p>
{ev.pushError && <p className="mt-0.5 text-xs text-[var(--color-danger)]">Ошибка: {ev.pushError}</p>}
</>
)}
{ev.type === "invitation" && ev.accessCount != null && ev.accessCount > 0 && (
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">Открытий страницы: {ev.accessCount}</p>
)}