feat(orders): show client push subscription and delivery log
This commit is contained in:
parent
ad7b92503c
commit
82d6f4f077
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue