141 lines
5.6 KiB
JavaScript
141 lines
5.6 KiB
JavaScript
import React from "react";
|
|
import { Panel } from "../UI/Panel";
|
|
import { Button } from "../UI/Button";
|
|
import { Bell, Check, CheckCheck, Settings, X } from "../UI/Icons";
|
|
|
|
const TYPE_ICONS = {
|
|
driver_assigned: "🚚",
|
|
driver_unassigned: "📤",
|
|
order_status_change: "📦",
|
|
delivery_problem: "⚠️",
|
|
};
|
|
|
|
const TYPE_LABELS = {
|
|
driver_assigned: "Назначение водителя",
|
|
driver_unassigned: "Снятие водителя",
|
|
order_status_change: "Изменение статуса",
|
|
delivery_problem: "Проблема доставки",
|
|
};
|
|
|
|
function formatTimeAgo(dateStr) {
|
|
const date = new Date(dateStr);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMin = Math.floor(diffMs / 60000);
|
|
if (diffMin < 1) return "сейчас";
|
|
if (diffMin < 60) return `${diffMin} мин`;
|
|
const diffH = Math.floor(diffMin / 60);
|
|
if (diffH < 24) return `${diffH} ч`;
|
|
const diffD = Math.floor(diffH / 24);
|
|
return `${diffD} д`;
|
|
}
|
|
|
|
export function NotificationBell({ notifications, unreadCount, onMarkAsRead, onMarkAllAsRead, onOpenSettings }) {
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
const bellRef = React.useRef(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!isOpen) return;
|
|
const handleClickOutside = (e) => {
|
|
if (bellRef.current && !bellRef.current.contains(e.target)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [isOpen]);
|
|
|
|
return (
|
|
<div className="relative" ref={bellRef}>
|
|
<button
|
|
className="relative flex h-9 w-9 items-center justify-center rounded-full transition hover:bg-[var(--color-surface-strong)]"
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
aria-label={`Уведомления${unreadCount > 0 ? ` (${unreadCount})` : ""}`}
|
|
>
|
|
<Bell className="h-5 w-5" />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute -right-0.5 -top-0.5 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-bold text-white">
|
|
{unreadCount > 99 ? "99+" : unreadCount}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="absolute right-0 top-full z-50 mt-2 w-80 sm:w-96">
|
|
<Panel className="max-h-[480px] overflow-hidden p-0 shadow-xl">
|
|
<div className="flex items-center justify-between border-b border-[var(--color-border)] px-4 py-3">
|
|
<h3 className="text-sm font-semibold">Уведомления</h3>
|
|
<div className="flex items-center gap-1">
|
|
{unreadCount > 0 && (
|
|
<button
|
|
className="rounded p-1 text-xs text-[var(--color-accent)] hover:bg-[var(--color-surface-strong)]"
|
|
onClick={onMarkAllAsRead}
|
|
title="Прочитать все"
|
|
>
|
|
<CheckCheck className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
{onOpenSettings && (
|
|
<button
|
|
className="rounded p-1 text-xs text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
|
|
onClick={() => { onOpenSettings(); setIsOpen(false); }}
|
|
title="Настройки"
|
|
>
|
|
<Settings className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
<button
|
|
className="rounded p-1 text-xs text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
|
|
onClick={() => setIsOpen(false)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-y-auto" style={{ maxHeight: "400px" }}>
|
|
{notifications.length === 0 ? (
|
|
<div className="px-4 py-8 text-center text-sm text-[var(--color-text-muted)]">
|
|
Нет уведомлений
|
|
</div>
|
|
) : (
|
|
notifications.map((notif) => (
|
|
<div
|
|
key={notif.id}
|
|
className={`flex gap-3 border-b border-[var(--color-border)] px-4 py-3 transition hover:bg-[var(--color-surface-strong)] ${
|
|
!notif.read ? "bg-[var(--color-accent-soft)]/30" : ""
|
|
}`}
|
|
onClick={() => !notif.read && onMarkAsRead(notif.id)}
|
|
>
|
|
<span className="mt-0.5 text-base">
|
|
{TYPE_ICONS[notif.type] || "📦"}
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className={`text-sm ${!notif.read ? "font-semibold" : "font-normal"}`}>
|
|
{notif.title}
|
|
</p>
|
|
<span className="shrink-0 text-[10px] text-[var(--color-text-muted)]">
|
|
{formatTimeAgo(notif.created_at)}
|
|
</span>
|
|
</div>
|
|
{notif.body && (
|
|
<p className="mt-0.5 text-xs text-[var(--color-text-muted)] line-clamp-2">
|
|
{notif.body}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{!notif.read && (
|
|
<div className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-[var(--color-accent)]" />
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</Panel>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|