mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
feat(activity): per-tenant audit feed in notification dropdown
activity_events records booking lifecycle transitions with the actor behind each change — staff (dashboard), customer (self-service), or system (auto-promotion). The notification dropdown leads with the real feed instead of derived recent-booking guesses. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
e5cb9bd543
commit
d86cba61e3
18 files changed
+634
-28
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { createSignal, createMemo, Show } from "solid-js";
|
||||
import { createSignal, createMemo, createResource, Show, Accessor } from "solid-js";
|
||||
import { useI18n } from "../../providers/i18n-provider";
|
||||
import { apiClient } from "../../lib/api-client";
|
||||
import { BellIcon } from "./icons";
|
||||
import { AnimatedList } from "../animated-list";
|
||||
|
||||
@@ -12,25 +13,69 @@ interface NotificationItem {
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export function NotificationDropdown(props: { bookings?: any[]; billing?: any }) {
|
||||
interface ActivityEvent {
|
||||
id: string;
|
||||
actorKind: "staff" | "customer" | "system";
|
||||
actorName?: string;
|
||||
action: string;
|
||||
summary: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export function NotificationDropdown(props: { bookings?: any[]; billing?: any; token?: Accessor<string | null | undefined> }) {
|
||||
const i18n = useI18n();
|
||||
const [open, setOpen] = createSignal(false);
|
||||
|
||||
const [activity] = createResource(
|
||||
() => props.token?.(),
|
||||
async (bearer) => {
|
||||
if (!bearer) return [] as ActivityEvent[];
|
||||
const response = await (apiClient as any).GET("/v1/activity", {
|
||||
params: { query: { limit: 20 } },
|
||||
headers: { Authorization: `Bearer ${bearer}` },
|
||||
});
|
||||
return (response.data?.events ?? []) as ActivityEvent[];
|
||||
},
|
||||
{ initialValue: [] },
|
||||
);
|
||||
|
||||
const notifications = createMemo<NotificationItem[]>(() => {
|
||||
const items: NotificationItem[] = [];
|
||||
const cs = i18n.locale() === "cs";
|
||||
const bookings = props.bookings ?? [];
|
||||
const locale = cs ? "cs-CZ" : "en-US";
|
||||
const events = activity() ?? [];
|
||||
|
||||
// Recent bookings as notifications
|
||||
bookings.slice(0, 3).forEach((b: any, i: number) => {
|
||||
items.push({
|
||||
id: `booking-${i}`,
|
||||
type: "booking",
|
||||
title: `${cs ? "Nová rezervace" : "New booking"} ${b.customerName}`,
|
||||
message: `${b.service} — ${new Date(b.startsAt).toLocaleDateString(cs ? "cs-CZ" : "en-US")}`,
|
||||
time: new Date(b.startsAt).toLocaleDateString(cs ? "cs-CZ" : "en-US", { month: "short", day: "numeric" }),
|
||||
read: false,
|
||||
if (events.length > 0) {
|
||||
// Real audit feed — replaces the derived "recent bookings" guesses.
|
||||
const actionLabel = (action: string) => {
|
||||
const key = `dashboard.activity.${action}`;
|
||||
const translated = i18n.t(key);
|
||||
return translated === key ? action : translated;
|
||||
};
|
||||
events.forEach((event) => {
|
||||
const actorSuffix = event.actorKind === "staff" && event.actorName ? ` · ${event.actorName}` : "";
|
||||
items.push({
|
||||
id: `activity-${event.id}`,
|
||||
type: "booking",
|
||||
title: actionLabel(event.action),
|
||||
message: `${event.summary}${actorSuffix}`,
|
||||
time: new Date(event.createdAt).toLocaleString(locale, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }),
|
||||
read: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const bookings = props.bookings ?? [];
|
||||
bookings.slice(0, 3).forEach((b: any, i: number) => {
|
||||
items.push({
|
||||
id: `booking-${i}`,
|
||||
type: "booking",
|
||||
title: `${cs ? "Nová rezervace" : "New booking"} ${b.customerName}`,
|
||||
message: `${b.service} — ${new Date(b.startsAt).toLocaleDateString(locale)}`,
|
||||
time: new Date(b.startsAt).toLocaleDateString(locale, { month: "short", day: "numeric" }),
|
||||
read: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Plan/trial notifications
|
||||
const billing = props.billing;
|
||||
|
||||
@@ -362,6 +362,11 @@ const dictionaries = {
|
||||
"dashboard.noNotifications": "Žádná nová oznámení",
|
||||
"dashboard.markAllRead": "Označit vše jako přečtené",
|
||||
"dashboard.notifications.title": "Oznámení",
|
||||
"dashboard.activity.booking.created": "Nová rezervace",
|
||||
"dashboard.activity.booking.cancelled": "Rezervace zrušena",
|
||||
"dashboard.activity.booking.rescheduled": "Rezervace přesunuta",
|
||||
"dashboard.activity.booking.moved": "Přesun na jinou lekci",
|
||||
"dashboard.activity.booking.promoted": "Povýšení z čekací listiny",
|
||||
"dashboard.demoBanner": "Demo režim",
|
||||
"dashboard.demoBannerDesc": "Prozkoumáte Bookra s ukázkovými daty. Pro plnou funkčnost se zaregistrujte.",
|
||||
"dashboard.demoBannerCTA": "Vytvořit účet",
|
||||
@@ -957,6 +962,11 @@ const dictionaries = {
|
||||
"dashboard.noNotifications": "No new notifications",
|
||||
"dashboard.markAllRead": "Mark all as read",
|
||||
"dashboard.notifications.title": "Notifications",
|
||||
"dashboard.activity.booking.created": "New booking",
|
||||
"dashboard.activity.booking.cancelled": "Booking cancelled",
|
||||
"dashboard.activity.booking.rescheduled": "Booking rescheduled",
|
||||
"dashboard.activity.booking.moved": "Moved to another session",
|
||||
"dashboard.activity.booking.promoted": "Promoted from waitlist",
|
||||
"dashboard.demoBanner": "Demo mode",
|
||||
"dashboard.demoBannerDesc": "You're exploring Bookra with sample data. Register for full functionality.",
|
||||
"dashboard.demoBannerCTA": "Create account",
|
||||
|
||||
@@ -379,7 +379,7 @@ function DashboardShell(props: { children: JSX.Element }) {
|
||||
<div class="lg:hidden bg-canvas/80 backdrop-blur-xl border-b border-border p-4 flex items-center justify-between sticky top-0 z-40">
|
||||
<DashboardLogo />
|
||||
<div class="flex items-center gap-2">
|
||||
<NotificationDropdown bookings={data.normalizedAllBookings()} billing={data.resolvedBilling()} />
|
||||
<NotificationDropdown bookings={data.normalizedAllBookings()} billing={data.resolvedBilling()} token={data.token} />
|
||||
<button onClick={() => setIsMobileMenuOpen(true)} aria-label={data.i18n.locale() === "cs" ? "Otevřít menu" : "Open menu"} class="p-2 hover:bg-canvas-subtle rounded-xl transition-all">
|
||||
<MenuIcon />
|
||||
</button>
|
||||
|
||||
@@ -32,7 +32,7 @@ function OverviewPage() {
|
||||
{data.i18n.t("dashboard.shareManage")}
|
||||
</button>
|
||||
<span class="text-sm text-ink-subtle hidden sm:inline">{new Date().toLocaleDateString(data.i18n.locale() === "cs" ? "cs-CZ" : "en-US", { weekday: "long", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
|
||||
<NotificationDropdown bookings={data.normalizedAllBookings()} billing={data.resolvedBilling()} />
|
||||
<NotificationDropdown bookings={data.normalizedAllBookings()} billing={data.resolvedBilling()} token={data.token} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user