procurement: surface notification verbs in UI

This commit is contained in:
lizerk 2026-05-22 16:59:45 +03:00
parent b40f32e849
commit 27e412c2e6
3 changed files with 56 additions and 16 deletions

View File

@ -8,14 +8,21 @@ import { formatDistanceToNow } from 'date-fns';
import { ru } from 'date-fns/locale/ru';
import { getVerbCategory, getVerbName } from '@/lib/verbTranslations';
import { resolveNotificationActionUrl } from '@/lib/notifications/actionUrl';
import type { NotificationItem } from '@/contexts/NotificationsContext';
type NotificationReadFilter = 'all' | 'unread' | 'read';
type NotificationPageItem = NotificationItem & {
color?: string;
icon?: string;
};
export default function NotificationsPage() {
// Локальное состояние для ВСЕХ уведомлений (не только непрочитанных)
const [notifications, setNotifications] = useState<any[]>([]);
const [notifications, setNotifications] = useState<NotificationPageItem[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [filterRead, setFilterRead] = useState<'all' | 'unread' | 'read'>('all');
const [filterRead, setFilterRead] = useState<NotificationReadFilter>('all');
const [filterCategory, setFilterCategory] = useState<string>('');
const [filtersOpen, setFiltersOpen] = useState(false);
@ -25,7 +32,7 @@ export default function NotificationsPage() {
try {
const data = await apiClient.getNotifications({ page_size: 100 });
const notifs = data.notifications || data.results || data;
setNotifications(Array.isArray(notifs) ? notifs : []);
setNotifications(Array.isArray(notifs) ? notifs as NotificationPageItem[] : []);
} catch (err) {
console.error('Ошибка загрузки уведомлений:', err);
} finally {
@ -82,7 +89,7 @@ export default function NotificationsPage() {
// Уникальные категории
const categories = useMemo(() => {
const cats = new Set<string>();
notifications.forEach((n: any) => {
notifications.forEach((n) => {
if (n.category) cats.add(getVerbCategory(n.category));
});
return Array.from(cats).sort();
@ -90,7 +97,7 @@ export default function NotificationsPage() {
// Фильтрация
const filteredNotifications = useMemo(() => {
return notifications.filter((n: any) => {
return notifications.filter((n) => {
// Фильтр по прочитанности
if (filterRead === 'unread' && n.is_read) return false;
if (filterRead === 'read' && !n.is_read) return false;
@ -110,14 +117,14 @@ export default function NotificationsPage() {
});
}, [notifications, search, filterRead, filterCategory]);
const unreadCount = notifications.filter((n: any) => !n.is_read).length;
const readCount = notifications.filter((n: any) => n.is_read).length;
const unreadCount = notifications.filter((n) => !n.is_read).length;
const readCount = notifications.filter((n) => n.is_read).length;
// Подсчет уведомлений по категориям
const categoryCounts = useMemo(() => {
const counts: Record<string, { total: number; unread: number }> = {};
notifications.forEach((n: any) => {
notifications.forEach((n) => {
if (!n.category) return;
const category = getVerbCategory(n.category);
@ -137,7 +144,7 @@ export default function NotificationsPage() {
.map(([category, data]) => ({ category, ...data }));
}, [notifications]);
const handleNotificationClick = async (notification: any) => {
const handleNotificationClick = async (notification: NotificationPageItem) => {
if (!notification.is_read) {
await markAsRead(notification.id);
}
@ -148,6 +155,8 @@ export default function NotificationsPage() {
};
const getCategoryIconEmoji = (categoryCode?: string, icon?: string) => {
if (icon) return icon;
// Маппинг категорий на эмодзи
const iconMap: Record<string, string> = {
'communications': '💬',
@ -231,7 +240,7 @@ export default function NotificationsPage() {
<div className="app-surface-muted flex flex-col gap-2 rounded-xl p-3">
<select
value={filterRead}
onChange={(e) => setFilterRead(e.target.value as any)}
onChange={(e) => setFilterRead(e.target.value as NotificationReadFilter)}
className="app-select rounded-lg px-3 py-2 text-sm"
>
<option value="all">Все уведомления</option>
@ -366,7 +375,7 @@ export default function NotificationsPage() {
</div>
) : (
<div className="space-y-2">
{filteredNotifications.map((notification: any) => (
{filteredNotifications.map((notification) => (
<article
key={notification.id}
className={`group overflow-hidden rounded-xl border transition ${
@ -397,7 +406,11 @@ export default function NotificationsPage() {
>
<div className="mb-1 flex items-start justify-between gap-2">
<h3 className="font-semibold text-[var(--foreground)] transition group-hover:text-[var(--accent-primary-strong)]">
{notification.title || getVerbName(notification.verb || notification.category)}
{notification.title || (
typeof notification.data?.title === 'string'
? notification.data.title
: getVerbName(notification.verb || notification.category || '')
)}
</h3>
{!notification.is_read && (
<div className="app-dot-accent mt-1.5 h-2 w-2 shrink-0 rounded-full"></div>
@ -423,7 +436,7 @@ export default function NotificationsPage() {
addSuffix: true,
locale: ru,
});
} catch (e) {
} catch {
return 'недавно';
}
})()}

View File

@ -17,7 +17,12 @@ interface NotificationCenterProps {
}
function getNotificationTitle(notification: NotificationItem): string {
return notification.title || getVerbName(notification.verb || '');
const dataTitle = notification.data?.title;
return (
notification.title
|| (typeof dataTitle === 'string' ? dataTitle : '')
|| getVerbName(notification.verb || '')
);
}
function getNotificationMessage(notification: NotificationItem): string {

View File

@ -38,6 +38,17 @@ export const VERB_CATEGORIES: Record<string, string> = {
// Закупки
procurement_new_request: 'Закупки',
procurement_department_request: 'Закупки',
procurement_pending_approval: 'Закупки',
procurement_stage_approved: 'Закупки',
procurement_approved: 'Закупки',
procurement_rejected: 'Закупки',
procurement_in_progress: 'Закупки',
procurement_completed: 'Закупки',
procurement_cancelled: 'Закупки',
procurement_item_updated: 'Закупки',
procurement_request_commented: 'Закупки',
procurement_item_commented: 'Закупки',
// Новости
feed_new_post: 'Новости',
@ -82,6 +93,17 @@ export const VERB_NAMES: Record<string, string> = {
// Закупки
procurement_new_request: 'Новая закупка',
procurement_department_request: 'Новая заявка для отдела',
procurement_pending_approval: 'Требуется согласование закупки',
procurement_stage_approved: 'Этап согласования закупки пройден',
procurement_approved: 'Закупка одобрена',
procurement_rejected: 'Закупка отклонена',
procurement_in_progress: 'Закупка взята в работу',
procurement_completed: 'Закупка завершена',
procurement_cancelled: 'Закупка отменена',
procurement_item_updated: 'Позиция закупки изменена',
procurement_request_commented: 'Комментарий к закупке',
procurement_item_commented: 'Комментарий к позиции закупки',
// Новости
feed_new_post: 'Новая публикация',
@ -105,8 +127,8 @@ export function getVerbCategory(verb: string): string {
*/
export function getVerbsByCategory(category: string): string[] {
return Object.entries(VERB_CATEGORIES)
.filter(([_, cat]) => cat === category)
.map(([verb, _]) => verb);
.filter((entry) => entry[1] === category)
.map((entry) => entry[0]);
}
/**