From 170a9aa7ac6a1a1a1c222638dbc730c7d6c2f84a Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sun, 21 Jun 2026 05:08:39 +0330 Subject: [PATCH] feat(dashboard): Notifications & sound settings panel (fa/en/ar) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Settings → "Notifications & sound" leaf to make the alert channels changeable: toggle sound (+ picker with live preview + volume slider), enable desktop notifications (permission flow + test button), toggle the tab unread badge and in-app toasts. Strings added for fa/en/ar. Co-Authored-By: Claude Opus 4.8 --- web/dashboard/messages/ar.json | 34 +++ web/dashboard/messages/en.json | 34 +++ web/dashboard/messages/fa.json | 34 +++ .../settings/settings-notifications-panel.tsx | 227 ++++++++++++++++++ .../components/settings/settings-screen.tsx | 6 + .../src/components/settings/settings-types.ts | 2 + 6 files changed, 337 insertions(+) create mode 100644 web/dashboard/src/components/settings/settings-notifications-panel.tsx diff --git a/web/dashboard/messages/ar.json b/web/dashboard/messages/ar.json index 6a2022a..33cec9d 100644 --- a/web/dashboard/messages/ar.json +++ b/web/dashboard/messages/ar.json @@ -1200,6 +1200,7 @@ "shop": "المقهى والمتجر", "shopGeneral": "الملف والتكاملات", "shopAppearance": "المظهر والألوان", + "shopNotifications": "الإشعارات والصوت", "printer": "الطابعة", "printerSettings": "إعدادات الطابعة", "printTest": "صفحة اختبار الطباعة", @@ -1207,6 +1208,39 @@ "team": "الفريق والموظفون", "customRoles": "الأدوار المخصصة" }, + "notifPrefs": { + "soundSection": "الصوت", + "soundEnabled": "تشغيل صوت للإشعارات الجديدة", + "soundEnabledHint": "يصدر صوتًا عند وصول طلب جديد أو نداء نادل أو تنبيه.", + "soundChoice": "صوت الإشعار", + "preview": "معاينة", + "volume": "مستوى الصوت", + "soundClassic": "كلاسيكي", + "soundDing": "رنين", + "soundBell": "جرس", + "soundChime": "أجراس", + "soundMarimba": "ماريمبا", + "soundAlert": "تنبيه", + "desktopSection": "إشعارات سطح المكتب", + "desktopHint": "إظهار نافذة منبثقة على ويندوز/سطح المكتب حتى عندما تكون لوحة التحكم في تبويب آخر أو مصغّرة.", + "enableDesktop": "تفعيل إشعارات سطح المكتب", + "desktopEnabled": "نوافذ سطح المكتب", + "desktopEnabledHint": "تظهر فقط عندما لا يكون هذا التبويب نشطًا.", + "desktopGranted": "تم تفعيل إشعارات سطح المكتب", + "desktopDenied": "تم رفض الإذن من المتصفح", + "desktopBlocked": "الإشعارات محظورة لهذا الموقع. اسمح بها من إعدادات الموقع في المتصفح ثم أعد التحميل.", + "desktopUnsupported": "هذا المتصفح لا يدعم إشعارات سطح المكتب.", + "desktopFocusNote": "تظهر النافذة التجريبية فقط إذا انتقلت إلى نافذة أخرى أولًا.", + "sendTest": "إرسال إشعار تجريبي", + "testTitle": "ميزي", + "testBody": "هذا إشعار تجريبي.", + "testToast": "تم إرسال الإشعار التجريبي", + "inAppSection": "داخل التطبيق", + "tabBadge": "عدد غير المقروء على تبويب المتصفح", + "tabBadgeHint": "يعرض عدد الإشعارات غير المقروءة في عنوان التبويب والأيقونة المفضلة.", + "toast": "تنبيه داخل التطبيق", + "toastHint": "إظهار شريط صغير داخل لوحة التحكم للإشعارات الجديدة." + }, "customRoles": { "title": "الأدوار المخصصة", "subtitle": "حدّد أدواراً بصلاحيات مخصصة لموظفيك", diff --git a/web/dashboard/messages/en.json b/web/dashboard/messages/en.json index 68bdd95..4bc949f 100644 --- a/web/dashboard/messages/en.json +++ b/web/dashboard/messages/en.json @@ -1272,6 +1272,7 @@ "shop": "Shop & café", "shopGeneral": "Profile & integrations", "shopAppearance": "Appearance & colors", + "shopNotifications": "Notifications & sound", "printer": "Printer", "printerSettings": "Printer settings", "printTest": "Print test page", @@ -1279,6 +1280,39 @@ "team": "Team & Staff", "customRoles": "Custom Roles" }, + "notifPrefs": { + "soundSection": "Sound", + "soundEnabled": "Play a sound for new notifications", + "soundEnabledHint": "Chimes when a new order, waiter call, or alert arrives.", + "soundChoice": "Notification sound", + "preview": "Preview", + "volume": "Volume", + "soundClassic": "Classic", + "soundDing": "Ding", + "soundBell": "Bell", + "soundChime": "Chime", + "soundMarimba": "Marimba", + "soundAlert": "Alert", + "desktopSection": "Desktop notifications", + "desktopHint": "Show a Windows/desktop popup even when the dashboard is in another tab or minimized.", + "enableDesktop": "Enable desktop notifications", + "desktopEnabled": "Desktop popups", + "desktopEnabledHint": "Pop up only when this tab is not focused.", + "desktopGranted": "Desktop notifications enabled", + "desktopDenied": "Permission denied by the browser", + "desktopBlocked": "Notifications are blocked for this site. Allow them in your browser's site settings, then reload.", + "desktopUnsupported": "This browser does not support desktop notifications.", + "desktopFocusNote": "A test popup only appears if you switch to another window first.", + "sendTest": "Send a test notification", + "testTitle": "Meezi", + "testBody": "This is a test notification.", + "testToast": "Test sent", + "inAppSection": "In-app", + "tabBadge": "Unread count on the browser tab", + "tabBadgeHint": "Shows the number of unread notifications in the tab title and favicon.", + "toast": "In-app toast", + "toastHint": "Show a small banner inside the dashboard for new notifications." + }, "customRoles": { "title": "Custom Roles", "subtitle": "Define roles with tailored permissions for your staff", diff --git a/web/dashboard/messages/fa.json b/web/dashboard/messages/fa.json index e574af0..84723d0 100644 --- a/web/dashboard/messages/fa.json +++ b/web/dashboard/messages/fa.json @@ -1273,6 +1273,7 @@ "shop": "کافه و فروشگاه", "shopGeneral": "پروفایل و اتصال‌ها", "shopAppearance": "ظاهر و رنگ‌بندی", + "shopNotifications": "اعلان‌ها و صدا", "printer": "پرینتر", "printerSettings": "تنظیمات پرینتر", "printTest": "صفحه تست چاپ", @@ -1280,6 +1281,39 @@ "team": "تیم و کارمندان", "customRoles": "نقش‌های سفارشی" }, + "notifPrefs": { + "soundSection": "صدا", + "soundEnabled": "پخش صدا برای اعلان‌های جدید", + "soundEnabledHint": "هنگام رسیدن سفارش جدید، درخواست میز یا هشدار، صدا پخش می‌شود.", + "soundChoice": "صدای اعلان", + "preview": "پیش‌نمایش", + "volume": "بلندی صدا", + "soundClassic": "کلاسیک", + "soundDing": "دینگ", + "soundBell": "زنگ", + "soundChime": "ناقوس", + "soundMarimba": "ماریمبا", + "soundAlert": "هشدار", + "desktopSection": "اعلان‌های دسکتاپ", + "desktopHint": "نمایش پاپ‌آپ ویندوز/دسکتاپ حتی وقتی داشبورد در تب دیگری باز است یا کوچک شده.", + "enableDesktop": "فعال‌سازی اعلان‌های دسکتاپ", + "desktopEnabled": "پاپ‌آپ دسکتاپ", + "desktopEnabledHint": "فقط وقتی این تب فعال نیست نمایش داده می‌شود.", + "desktopGranted": "اعلان‌های دسکتاپ فعال شد", + "desktopDenied": "دسترسی توسط مرورگر رد شد", + "desktopBlocked": "اعلان‌ها برای این سایت مسدود شده‌اند. از تنظیمات سایت در مرورگر اجازه دهید و سپس صفحه را دوباره بارگذاری کنید.", + "desktopUnsupported": "این مرورگر از اعلان‌های دسکتاپ پشتیبانی نمی‌کند.", + "desktopFocusNote": "پاپ‌آپ آزمایشی فقط زمانی نمایش داده می‌شود که ابتدا به پنجره دیگری بروید.", + "sendTest": "ارسال اعلان آزمایشی", + "testTitle": "میزی", + "testBody": "این یک اعلان آزمایشی است.", + "testToast": "اعلان آزمایشی ارسال شد", + "inAppSection": "درون‌برنامه", + "tabBadge": "شمارش خوانده‌نشده روی تب مرورگر", + "tabBadgeHint": "تعداد اعلان‌های خوانده‌نشده را در عنوان تب و فاویکون نشان می‌دهد.", + "toast": "نوتیف درون‌برنامه", + "toastHint": "نمایش یک بنر کوچک داخل داشبورد برای اعلان‌های جدید." + }, "customRoles": { "title": "نقش‌های سفارشی", "subtitle": "نقش‌هایی با دسترسی دلخواه برای کارمندان تعریف کنید", diff --git a/web/dashboard/src/components/settings/settings-notifications-panel.tsx b/web/dashboard/src/components/settings/settings-notifications-panel.tsx new file mode 100644 index 0000000..13daefe --- /dev/null +++ b/web/dashboard/src/components/settings/settings-notifications-panel.tsx @@ -0,0 +1,227 @@ +"use client"; + +import { Bell, Volume2 } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { LabeledField } from "@/components/ui/labeled-field"; +import { notify } from "@/lib/notify"; +import { useNotifPrefs } from "@/lib/notifications/notification-prefs.store"; +import { SOUND_PRESETS, playSound, type SoundId } from "@/lib/notifications/sounds"; +import { + showDesktopNotification, + useNotificationPermission, +} from "@/lib/notifications/use-desktop-notifications"; + +function Toggle({ + id, + checked, + onChange, + label, + hint, +}: { + id: string; + checked: boolean; + onChange: (v: boolean) => void; + label: string; + hint?: string; +}) { + return ( + + ); +} + +export function SettingsNotificationsPanel() { + const t = useTranslations("settings.notifPrefs"); + const prefs = useNotifPrefs(); + const hasHydrated = useNotifPrefs((s) => s._hasHydrated); + const setPrefs = useNotifPrefs((s) => s.setPrefs); + const { permission, request, supported } = useNotificationPermission(); + + if (!hasHydrated) { + return

; + } + + const enableDesktop = async () => { + const p = await request(); + if (p === "granted") { + setPrefs({ desktopEnabled: true }); + notify.success(t("desktopGranted")); + } else if (p === "denied") { + setPrefs({ desktopEnabled: false }); + notify.error(t("desktopDenied")); + } + }; + + const desktopReady = supported && permission === "granted"; + + return ( +
+ {/* Sound */} + + + + + {t("soundSection")} + + + + setPrefs({ soundEnabled: v })} + label={t("soundEnabled")} + hint={t("soundEnabledHint")} + /> + +
+ +
+ + +
+
+ + +
+ setPrefs({ volume: Number(e.target.value) / 100 })} + className="h-2 w-full cursor-pointer accent-[#0F6E56] disabled:opacity-50" + /> + + {Math.round(prefs.volume * 100)}% + +
+
+
+
+
+ + {/* Desktop / Windows popups */} + + + + + {t("desktopSection")} + + + +

{t("desktopHint")}

+ + {!supported ? ( +

+ {t("desktopUnsupported")} +

+ ) : permission === "denied" ? ( +

+ {t("desktopBlocked")} +

+ ) : permission !== "granted" ? ( + + ) : ( +
+ setPrefs({ desktopEnabled: v })} + label={t("desktopEnabled")} + hint={t("desktopEnabledHint")} + /> + + {desktopReady ? ( +

{t("desktopFocusNote")}

+ ) : null} +
+ )} +
+
+ + {/* Tab badge + in-app toast */} + + + {t("inAppSection")} + + + setPrefs({ tabBadgeEnabled: v })} + label={t("tabBadge")} + hint={t("tabBadgeHint")} + /> + setPrefs({ toastEnabled: v })} + label={t("toast")} + hint={t("toastHint")} + /> + + +
+ ); +} diff --git a/web/dashboard/src/components/settings/settings-screen.tsx b/web/dashboard/src/components/settings/settings-screen.tsx index 3ebc8b2..b8495e0 100644 --- a/web/dashboard/src/components/settings/settings-screen.tsx +++ b/web/dashboard/src/components/settings/settings-screen.tsx @@ -6,6 +6,7 @@ import { useAuthStore } from "@/lib/stores/auth.store"; import { PageHeader } from "@/components/layout/page-header"; import { SettingsNav } from "@/components/settings/settings-nav"; import { SettingsAppearancePanel } from "@/components/settings/settings-appearance-panel"; +import { SettingsNotificationsPanel } from "@/components/settings/settings-notifications-panel"; import { CafeDiscoverProfilePanel } from "@/components/discover/cafe-discover-profile-panel"; import { CafePublicProfilePanel } from "@/components/discover/cafe-public-profile-panel"; import { SettingsShopPanel } from "@/components/settings/settings-shop-panel"; @@ -23,6 +24,7 @@ import { const LEAF_PAGE_TITLE: Record = { "shop-general": "nav.shopGeneral", "shop-appearance": "nav.shopAppearance", + "shop-notifications": "nav.shopNotifications", "shop-discover": "nav.shopDiscover", "printer-config": "nav.printerSettings", "print-test": "nav.printTest", @@ -83,6 +85,10 @@ export function SettingsScreen() { ) : null} + {activeLeaf === "shop-notifications" ? ( + + ) : null} + {activeLeaf === "shop-discover" ? (
diff --git a/web/dashboard/src/components/settings/settings-types.ts b/web/dashboard/src/components/settings/settings-types.ts index 1caefe1..b836dcb 100644 --- a/web/dashboard/src/components/settings/settings-types.ts +++ b/web/dashboard/src/components/settings/settings-types.ts @@ -3,6 +3,7 @@ export type SettingsGroupId = "shop" | "printer" | "team"; export type SettingsLeafId = | "shop-general" | "shop-appearance" + | "shop-notifications" | "shop-discover" | "printer-config" | "print-test" @@ -21,6 +22,7 @@ export const SETTINGS_NAV: SettingsNavGroup[] = [ children: [ { id: "shop-general", labelKey: "nav.shopGeneral" }, { id: "shop-appearance", labelKey: "nav.shopAppearance" }, + { id: "shop-notifications", labelKey: "nav.shopNotifications" }, { id: "shop-discover", labelKey: "nav.shopDiscover" }, ], },