Добавлены изменения для PWA
This commit is contained in:
Binary file not shown.
@@ -1,13 +1,11 @@
|
||||
|
||||
export function HanLogo() {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-2xl bg-primary text-primary-foreground flex-shrink-0">
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
className="w-7 h-7"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M20 25 L20 75 M20 50 L50 50 M50 25 L50 75 M70 25 L90 25 L90 50 L70 50 L70 75 L90 75"
|
||||
<svg viewBox="0 0 100 100" className="w-7 h-7" fill="currentColor">
|
||||
<path
|
||||
d="M20 25 L20 75 M20 50 L50 50 M50 25 L50 75 M70 25 L90 25 L90 50 L70 50 L70 75 L90 75"
|
||||
stroke="currentColor"
|
||||
strokeWidth="6"
|
||||
fill="none"
|
||||
@@ -16,12 +14,14 @@ export function HanLogo() {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h1 className="text-base font-medium mb-0.5">Привет! Я HAN</h1>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Помощник по документам и жизни в России
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { User, History } from 'lucide-react';
|
||||
import { User, Bell } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import { getActiveMessages } from '../data/companyMessages';
|
||||
|
||||
export function Header() {
|
||||
const count = getActiveMessages().length;
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between px-4 py-3 bg-background border-b border-border">
|
||||
<Link to="/history" className="flex items-center gap-2 text-foreground hover:opacity-70 transition-opacity">
|
||||
<History className="w-5 h-5" />
|
||||
<span className="text-sm">История</span>
|
||||
{/* Колокольчик с бейджем */}
|
||||
<Link to="/history" className="relative flex items-center justify-center w-10 h-10 rounded-full hover:bg-muted transition-colors">
|
||||
<Bell className="w-5 h-5" />
|
||||
{count > 0 && (
|
||||
<span className="absolute top-0.5 right-0.5 min-w-[16px] h-4 px-1 flex items-center justify-center bg-destructive text-white text-[10px] font-bold rounded-full leading-none">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Профиль */}
|
||||
<Link to="/profile" className="relative flex items-center justify-center w-10 h-10 rounded-full bg-muted hover:opacity-70 transition-opacity">
|
||||
<User className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-primary rounded-full border-2 border-background"></span>
|
||||
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-primary rounded-full border-2 border-background" />
|
||||
</Link>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -1,61 +1,185 @@
|
||||
import { Calendar, FileText } from 'lucide-react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import {
|
||||
Calendar, AlertCircle, Bell, MessageCircle, Tag,
|
||||
X, ChevronLeft, ChevronRight, ArrowRight,
|
||||
} from 'lucide-react';
|
||||
import { getActiveMessages, dismissMessage, CompanyMessage } from '../data/companyMessages';
|
||||
|
||||
interface Notification {
|
||||
id: string;
|
||||
type: 'urgent' | 'reminder';
|
||||
// ─── Тип-конфиг ──────────────────────────────────────────────────────────────
|
||||
|
||||
type TypeConfig = {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
action: string;
|
||||
bg: string;
|
||||
iconWrap: string;
|
||||
badge: string;
|
||||
actionColor: string;
|
||||
};
|
||||
|
||||
function getConfig(type: CompanyMessage['type']): TypeConfig {
|
||||
switch (type) {
|
||||
case 'urgent':
|
||||
return {
|
||||
label: 'Срочно',
|
||||
icon: <AlertCircle className="w-4 h-4" />,
|
||||
bg: 'bg-destructive/8 border-destructive/25',
|
||||
iconWrap: 'bg-destructive/12 text-destructive',
|
||||
badge: 'bg-destructive/12 text-destructive',
|
||||
actionColor: 'text-destructive',
|
||||
};
|
||||
case 'reminder':
|
||||
return {
|
||||
label: 'Напоминание',
|
||||
icon: <Calendar className="w-4 h-4" />,
|
||||
bg: 'bg-primary/6 border-primary/20',
|
||||
iconWrap: 'bg-primary/10 text-primary',
|
||||
badge: 'bg-primary/10 text-primary',
|
||||
actionColor: 'text-primary',
|
||||
};
|
||||
case 'message':
|
||||
return {
|
||||
label: 'Сообщение',
|
||||
icon: <MessageCircle className="w-4 h-4" />,
|
||||
bg: 'bg-[#e8f5e9] border-[#a5d6a7]',
|
||||
iconWrap: 'bg-[#c8e6c9] text-[#2e7d32]',
|
||||
badge: 'bg-[#c8e6c9] text-[#2e7d32]',
|
||||
actionColor: 'text-[#2e7d32]',
|
||||
};
|
||||
case 'promo':
|
||||
return {
|
||||
label: 'Предложение',
|
||||
icon: <Tag className="w-4 h-4" />,
|
||||
bg: 'bg-[#fff8e1] border-[#ffe082]',
|
||||
iconWrap: 'bg-[#fff3cd] text-[#e65100]',
|
||||
badge: 'bg-[#fff3cd] text-[#e65100]',
|
||||
actionColor: 'text-[#e65100]',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: 'Новость',
|
||||
icon: <Bell className="w-4 h-4" />,
|
||||
bg: 'bg-muted/60 border-border',
|
||||
iconWrap: 'bg-muted text-muted-foreground',
|
||||
badge: 'bg-muted text-muted-foreground',
|
||||
actionColor: 'text-foreground/70',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const notifications: Notification[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'urgent',
|
||||
icon: <Calendar className="w-4 h-4" />,
|
||||
title: 'Продление патента через 14 дней',
|
||||
description: 'Не забудьте подать документы заранее',
|
||||
action: 'Подробнее'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'reminder',
|
||||
icon: <FileText className="w-4 h-4" />,
|
||||
title: 'Проверьте статус РВП',
|
||||
description: 'Возможно, уже готово к получению',
|
||||
action: 'Проверить'
|
||||
}
|
||||
];
|
||||
// ─── Основной компонент ───────────────────────────────────────────────────────
|
||||
|
||||
export function Notifications() {
|
||||
const navigate = useNavigate();
|
||||
const [messages, setMessages] = useState(() => getActiveMessages());
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
if (notifications.length === 0) return null;
|
||||
const handleDismiss = useCallback((id: string) => {
|
||||
dismissMessage(id);
|
||||
const next = getActiveMessages();
|
||||
setMessages(next);
|
||||
setIndex(i => Math.min(i, Math.max(next.length - 1, 0)));
|
||||
}, []);
|
||||
|
||||
if (messages.length === 0) return null;
|
||||
|
||||
const msg = messages[index];
|
||||
const cfg = getConfig(msg.type);
|
||||
const hasMultiple = messages.length > 1;
|
||||
|
||||
function handleAction() {
|
||||
if (msg.type === 'message' && msg.chatId) {
|
||||
navigate(`/chat/${msg.chatId}`);
|
||||
} else if (msg.type === 'promo') {
|
||||
// внешний переход — в реальном приложении будет ссылка
|
||||
navigate(`/notification/${msg.id}`);
|
||||
} else {
|
||||
navigate(`/notification/${msg.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const actionLabel =
|
||||
msg.type === 'message' ? 'Открыть чат →' :
|
||||
msg.type === 'promo' ? (msg.promo?.cta ?? 'Подробнее') + ' →' :
|
||||
'Подробнее →';
|
||||
|
||||
return (
|
||||
<div className="px-4 py-3 space-y-2">
|
||||
{notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className="bg-primary/5 border border-primary/20 rounded-lg p-3 flex items-start gap-2.5"
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary flex-shrink-0 mt-0.5">
|
||||
{notification.icon}
|
||||
<div className="px-4 py-3">
|
||||
<div className={`rounded-xl border ${cfg.bg} overflow-hidden`}>
|
||||
{/* Верхняя строка: метка + навигация + закрыть */}
|
||||
<div className="flex items-center justify-between px-3 pt-2.5 pb-2">
|
||||
<span className={`text-[11px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full ${cfg.badge}`}>
|
||||
{cfg.label}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-0.5">
|
||||
{hasMultiple && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIndex(i => (i - 1 + messages.length) % messages.length)}
|
||||
className="w-6 h-6 flex items-center justify-center rounded-md hover:bg-black/8 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
<span className="text-[11px] text-muted-foreground tabular-nums min-w-[28px] text-center">
|
||||
{index + 1}/{messages.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIndex(i => (i + 1) % messages.length)}
|
||||
className="w-6 h-6 flex items-center justify-center rounded-md hover:bg-black/8 transition-colors"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDismiss(msg.id)}
|
||||
className="w-6 h-6 flex items-center justify-center rounded-md hover:bg-black/8 transition-colors ml-1"
|
||||
aria-label="Скрыть"
|
||||
>
|
||||
<X className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Тело */}
|
||||
<div className="flex items-start gap-3 px-3 pb-3">
|
||||
<div className={`flex items-center justify-center w-8 h-8 rounded-full flex-shrink-0 ${cfg.iconWrap}`}>
|
||||
{cfg.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-sm mb-0.5">{notification.title}</h3>
|
||||
<p className="text-xs text-muted-foreground leading-tight">{notification.description}</p>
|
||||
<p className="font-medium text-sm text-foreground leading-snug mb-0.5">{msg.title}</p>
|
||||
<p className="text-xs text-muted-foreground leading-snug">{msg.description}</p>
|
||||
|
||||
{/* Промо-цена */}
|
||||
{msg.type === 'promo' && msg.promo && (
|
||||
<div className="flex items-baseline gap-1.5 mt-1.5">
|
||||
<span className="text-base font-bold text-[#e65100]">{msg.promo.price}</span>
|
||||
{msg.promo.originalPrice && (
|
||||
<span className="text-xs text-muted-foreground line-through">{msg.promo.originalPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate(`/notification/${notification.id}`)}
|
||||
className="text-xs text-primary font-medium flex-shrink-0 mt-1 hover:underline"
|
||||
>
|
||||
{notification.action}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Футер с действием */}
|
||||
<div className="border-t border-black/6 px-3 py-2 flex items-center justify-between">
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className={`text-xs font-semibold flex items-center gap-1 ${cfg.actionColor} hover:opacity-75 transition-opacity`}
|
||||
>
|
||||
{msg.type === 'message' && <MessageCircle className="w-3 h-3" />}
|
||||
{msg.type === 'promo' && <ArrowRight className="w-3 h-3" />}
|
||||
{actionLabel}
|
||||
</button>
|
||||
|
||||
{/* Для промо — дополнительно показываем дату */}
|
||||
{msg.type === 'promo' && (
|
||||
<span className="text-[11px] text-muted-foreground">{msg.date}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { Headphones } from 'lucide-react';
|
||||
import { Headphones, MessageCircle } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
export function QuickActions() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-4">
|
||||
<button className="w-full flex items-center justify-center gap-2 py-2.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors">
|
||||
<div className="px-4 pb-4 flex gap-2">
|
||||
<button
|
||||
onClick={() => navigate('/chat/1')}
|
||||
className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">Чат</span>
|
||||
</button>
|
||||
|
||||
<button className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors">
|
||||
<Headphones className="w-4 h-4" />
|
||||
<span className="text-sm">Связь с оператором</span>
|
||||
<span className="text-sm">Оператор</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
export interface CompanyMessage {
|
||||
id: string;
|
||||
type: 'urgent' | 'reminder' | 'info' | 'message' | 'promo';
|
||||
title: string;
|
||||
description: string;
|
||||
fullContent: string;
|
||||
deadline?: string;
|
||||
steps?: string[];
|
||||
date: string;
|
||||
// Для type === 'message' — ID чата куда перейти
|
||||
chatId?: string;
|
||||
// Для type === 'promo'
|
||||
promo?: {
|
||||
price: string;
|
||||
originalPrice?: string;
|
||||
cta: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const companyMessages: CompanyMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'urgent',
|
||||
title: 'Продление патента через 14 дней',
|
||||
description: 'Не забудьте подать документы заранее',
|
||||
fullContent: 'Ваш патент на работу истекает 19 июня 2026 года. Рекомендуем начать процесс продления заранее, чтобы избежать перерыва в легальном статусе.',
|
||||
deadline: '19 июня 2026',
|
||||
steps: [
|
||||
'Подготовьте пакет документов (паспорт, миграционная карта, текущий патент)',
|
||||
'Оплатите госпошлину и НДФЛ',
|
||||
'Подайте документы в МФЦ или МВД',
|
||||
'Получите новый патент в течение 10 рабочих дней',
|
||||
],
|
||||
date: 'Сегодня, 09:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'reminder',
|
||||
title: 'Проверьте статус РВП',
|
||||
description: 'Возможно, уже готово к получению',
|
||||
fullContent: 'Прошло более 6 месяцев с момента подачи документов на РВП. Рекомендуем проверить готовность документа на сайте МВД или обратиться в отделение лично.',
|
||||
steps: [
|
||||
'Зайдите на сайт гувм.мвд.рф',
|
||||
'Проверьте статус по номеру заявления',
|
||||
'При готовности запишитесь на получение',
|
||||
'Подготовьте документы для получения РВП',
|
||||
],
|
||||
date: 'Вчера, 11:30',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'info',
|
||||
title: 'Новые услуги в приложении',
|
||||
description: 'Теперь можно загружать документы прямо в чат',
|
||||
fullContent: 'Мы обновили приложение: теперь вы можете прикладывать фотографии документов прямо в диалог с консультантом. HAN проверит данные и подскажет, всё ли в порядке.',
|
||||
date: '20 мая, 15:00',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'message',
|
||||
title: 'Ваш консультант ответил',
|
||||
description: 'Готов пакет документов для подачи на ВНЖ',
|
||||
fullContent: 'Консультант подготовил полный список документов для подачи на вид на жительство. Откройте чат, чтобы скачать список и задать вопросы.',
|
||||
date: 'Сегодня, 12:14',
|
||||
chatId: '2',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'urgent',
|
||||
title: 'Истекает регистрация — 3 дня',
|
||||
description: 'Продлите регистрацию, чтобы избежать штрафа',
|
||||
fullContent: 'Срок вашей регистрации по месту пребывания истекает 24 июля 2026 года. Нарушение сроков регистрации влечёт административный штраф до 5 000 ₽ и риск аннулирования патента.',
|
||||
deadline: '24 июля 2026',
|
||||
steps: [
|
||||
'Обратитесь к собственнику жилья для продления уведомления',
|
||||
'Подайте уведомление в МФЦ или МВД',
|
||||
'Получите отметку о регистрации в течение 1 рабочего дня',
|
||||
],
|
||||
date: 'Сегодня, 08:15',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'promo',
|
||||
title: 'Полное оформление ВНЖ под ключ',
|
||||
description: 'Юрист сам подаст документы — вам только расписаться',
|
||||
fullContent: 'Наш партнёр «МиграЛекс» берёт на себя весь процесс: сбор документов, перевод, нотариус, подача в МВД и отслеживание статуса. Вы приходите только на получение.',
|
||||
date: 'Вчера, 10:00',
|
||||
promo: {
|
||||
price: '8 900 ₽',
|
||||
originalPrice: '14 000 ₽',
|
||||
cta: 'Узнать подробнее',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const DISMISSED_KEY = 'han_dismissed_notifications';
|
||||
|
||||
export function getDismissedIds(): string[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(DISMISSED_KEY) ?? '[]');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function dismissMessage(id: string) {
|
||||
const current = getDismissedIds();
|
||||
if (!current.includes(id)) {
|
||||
localStorage.setItem(DISMISSED_KEY, JSON.stringify([...current, id]));
|
||||
}
|
||||
}
|
||||
|
||||
export function getActiveMessages(): CompanyMessage[] {
|
||||
const dismissed = getDismissedIds();
|
||||
return companyMessages.filter(m => !dismissed.includes(m.id));
|
||||
}
|
||||
|
||||
export function getAllMessages(): (CompanyMessage & { dismissed: boolean })[] {
|
||||
const dismissed = getDismissedIds();
|
||||
return companyMessages.map(m => ({ ...m, dismissed: dismissed.includes(m.id) }));
|
||||
}
|
||||
@@ -1,52 +1,33 @@
|
||||
import { MessageSquare, Clock, ArrowLeft } from 'lucide-react';
|
||||
import { Clock, ArrowLeft, Bell, AlertCircle, Calendar, MessageCircle, Tag } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { getActiveMessages, CompanyMessage } from '../data/companyMessages';
|
||||
|
||||
interface ChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
lastMessage: string;
|
||||
timestamp: string;
|
||||
unread?: boolean;
|
||||
function getIcon(type: CompanyMessage['type']) {
|
||||
if (type === 'urgent') return <AlertCircle className="w-4 h-4" />;
|
||||
if (type === 'reminder') return <Calendar className="w-4 h-4" />;
|
||||
if (type === 'message') return <MessageCircle className="w-4 h-4" />;
|
||||
if (type === 'promo') return <Tag className="w-4 h-4" />;
|
||||
return <Bell className="w-4 h-4" />;
|
||||
}
|
||||
|
||||
const chatHistory: ChatSession[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Продление патента',
|
||||
lastMessage: 'Спасибо за помощь! Я понял что нужно делать.',
|
||||
timestamp: 'Сегодня, 14:30',
|
||||
unread: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Документы для РВП',
|
||||
lastMessage: 'Какие документы нужны для подачи на РВП?',
|
||||
timestamp: 'Вчера, 18:45',
|
||||
unread: false
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Регистрация по месту жительства',
|
||||
lastMessage: 'HAN: Вам нужно обратиться в МФЦ с паспортом и...',
|
||||
timestamp: '20 мая, 10:15',
|
||||
unread: false
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Оплата патента',
|
||||
lastMessage: 'Где можно оплатить патент?',
|
||||
timestamp: '18 мая, 16:20',
|
||||
unread: false
|
||||
},
|
||||
];
|
||||
function typeColors(type: CompanyMessage['type']) {
|
||||
if (type === 'urgent') return { icon: 'bg-destructive/12 text-destructive', dot: 'bg-destructive' };
|
||||
if (type === 'reminder') return { icon: 'bg-primary/10 text-primary', dot: 'bg-primary' };
|
||||
if (type === 'message') return { icon: 'bg-[#c8e6c9] text-[#2e7d32]', dot: 'bg-[#43a047]' };
|
||||
if (type === 'promo') return { icon: 'bg-[#fff3cd] text-[#e65100]', dot: 'bg-[#fb8c00]' };
|
||||
return { icon: 'bg-muted text-muted-foreground', dot: 'bg-muted-foreground' };
|
||||
}
|
||||
|
||||
export function History() {
|
||||
const navigate = useNavigate();
|
||||
const messages = getActiveMessages();
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="px-4 py-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="px-4 py-4 space-y-4">
|
||||
|
||||
{/* Заголовок страницы */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="flex items-center justify-center w-9 h-9 rounded-full hover:bg-muted transition-colors"
|
||||
@@ -54,35 +35,58 @@ export function History() {
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-xl font-medium">История общения</h1>
|
||||
<p className="text-sm text-muted-foreground">Все ваши диалоги с HAN</p>
|
||||
</div>
|
||||
<h1 className="flex-1 text-xl font-medium">Центр уведомлений</h1>
|
||||
{messages.length > 0 && (
|
||||
<span className="text-xs font-medium text-primary bg-primary/10 rounded-full px-2.5 py-1">
|
||||
{messages.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{chatHistory.map((chat) => (
|
||||
<button
|
||||
key={chat.id}
|
||||
onClick={() => navigate(`/chat/${chat.id}`)}
|
||||
className="w-full bg-card border border-border rounded-lg p-4 flex items-start gap-3 hover:bg-accent/50 transition-colors text-left"
|
||||
>
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 text-primary flex-shrink-0">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-sm mb-1">{chat.title}</h3>
|
||||
<p className="text-xs text-muted-foreground line-clamp-1 mb-2">
|
||||
{chat.lastMessage}
|
||||
</p>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{chat.timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Список уведомлений */}
|
||||
{messages.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{messages.map(msg => {
|
||||
const colors = typeColors(msg.type);
|
||||
return (
|
||||
<button
|
||||
key={msg.id}
|
||||
onClick={() => msg.type === 'message' && msg.chatId
|
||||
? navigate(`/chat/${msg.chatId}`)
|
||||
: navigate(`/notification/${msg.id}`)
|
||||
}
|
||||
className="w-full bg-card border border-border rounded-xl p-3.5 flex items-start gap-3 hover:bg-accent/50 transition-colors text-left"
|
||||
>
|
||||
<div className={`relative flex items-center justify-center w-9 h-9 rounded-full flex-shrink-0 ${colors.icon}`}>
|
||||
{getIcon(msg.type)}
|
||||
<span className={`absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-background ${colors.dot}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground leading-snug line-clamp-1 mb-0.5">
|
||||
{msg.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground line-clamp-1 mb-1.5">
|
||||
{msg.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{msg.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-3">
|
||||
<Bell className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-foreground mb-1">Всё актуально</p>
|
||||
<p className="text-xs text-muted-foreground">Новых уведомлений нет</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,89 +1,70 @@
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { ArrowLeft, Calendar, FileText, CheckCircle, EyeOff, Clock } from 'lucide-react';
|
||||
import { ArrowLeft, Calendar, AlertCircle, Bell, CheckCircle, EyeOff, Clock } from 'lucide-react';
|
||||
import { companyMessages, dismissMessage } from '../data/companyMessages';
|
||||
|
||||
interface NotificationData {
|
||||
id: string;
|
||||
type: 'urgent' | 'reminder';
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
fullContent: string;
|
||||
deadline?: string;
|
||||
steps?: string[];
|
||||
function getIcon(type: 'urgent' | 'reminder' | 'info', className = 'w-5 h-5') {
|
||||
if (type === 'urgent') return <AlertCircle className={className} />;
|
||||
if (type === 'reminder') return <Calendar className={className} />;
|
||||
return <Bell className={className} />;
|
||||
}
|
||||
|
||||
const notificationDetails: Record<string, NotificationData> = {
|
||||
'1': {
|
||||
id: '1',
|
||||
type: 'urgent',
|
||||
icon: <Calendar className="w-6 h-6" />,
|
||||
title: 'Продление патента через 14 дней',
|
||||
description: 'Не забудьте подать документы заранее',
|
||||
fullContent: 'Ваш патент на работу истекает 19 июня 2026 года. Рекомендуем начать процесс продления заранее, чтобы избежать перерыва в легальном статусе.',
|
||||
deadline: '19 июня 2026',
|
||||
steps: [
|
||||
'Подготовьте пакет документов (паспорт, миграционная карта, текущий патент)',
|
||||
'Оплатите госпошлину и НДФЛ',
|
||||
'Подайте документы в МФЦ или МВД',
|
||||
'Получите новый патент в течение 10 рабочих дней'
|
||||
]
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
type: 'reminder',
|
||||
icon: <FileText className="w-6 h-6" />,
|
||||
title: 'Проверьте статус РВП',
|
||||
description: 'Возможно, уже готово к получению',
|
||||
fullContent: 'Прошло более 6 месяцев с момента подачи документов на РВП. Рекомендуем проверить готовность документа на сайте МВД или обратиться в отделение лично.',
|
||||
steps: [
|
||||
'Зайдите на сайт гувм.мвд.рф',
|
||||
'Проверьте статус по номеру заявления',
|
||||
'При готовности запишитесь на получение',
|
||||
'Подготовьте документы для получения РВП'
|
||||
]
|
||||
}
|
||||
};
|
||||
function typeStyle(type: 'urgent' | 'reminder' | 'info') {
|
||||
if (type === 'urgent') return {
|
||||
wrapper: 'bg-destructive/10 text-destructive',
|
||||
deadline: 'bg-destructive/6 border-destructive/20',
|
||||
deadlineText: 'text-destructive',
|
||||
step: 'bg-destructive/10 text-destructive',
|
||||
};
|
||||
if (type === 'reminder') return {
|
||||
wrapper: 'bg-primary/10 text-primary',
|
||||
deadline: 'bg-primary/5 border-primary/20',
|
||||
deadlineText: 'text-primary',
|
||||
step: 'bg-primary/10 text-primary',
|
||||
};
|
||||
return {
|
||||
wrapper: 'bg-muted text-muted-foreground',
|
||||
deadline: 'bg-muted border-border',
|
||||
deadlineText: 'text-foreground',
|
||||
step: 'bg-muted text-muted-foreground',
|
||||
};
|
||||
}
|
||||
|
||||
export function NotificationDetail() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const notification = id ? notificationDetails[id] : null;
|
||||
const notification = id ? companyMessages.find(m => m.id === id) : null;
|
||||
|
||||
if (!notification) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-4">
|
||||
<p className="text-muted-foreground">Уведомление не найдено</p>
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="mt-4 text-primary font-medium"
|
||||
>
|
||||
<button onClick={() => navigate('/')} className="mt-4 text-primary font-medium">
|
||||
Вернуться на главную
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const style = typeStyle(notification.type);
|
||||
|
||||
const handleComplete = () => {
|
||||
// В реальном приложении здесь будет логика отметки уведомления как выполненного
|
||||
dismissMessage(notification.id);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const handleHide = () => {
|
||||
// В реальном приложении здесь будет логика скрытия уведомления
|
||||
dismissMessage(notification.id);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const handleLater = () => {
|
||||
// В реальном приложении здесь будет логика напоминания позже
|
||||
navigate('/');
|
||||
};
|
||||
const handleLater = () => navigate('/');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Хедер с кнопкой назад */}
|
||||
{/* Хедер */}
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b border-border/40">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
onClick={() => navigate(-1)}
|
||||
className="p-1.5 -ml-1.5 hover:bg-muted/50 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
@@ -96,12 +77,8 @@ export function NotificationDetail() {
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Иконка и заголовок */}
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`flex items-center justify-center w-12 h-12 rounded-full flex-shrink-0 ${
|
||||
notification.type === 'urgent'
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}>
|
||||
{notification.icon}
|
||||
<div className={`flex items-center justify-center w-12 h-12 rounded-full flex-shrink-0 ${style.wrapper}`}>
|
||||
{getIcon(notification.type, 'w-6 h-6')}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 pt-1">
|
||||
<h2 className="font-semibold text-base mb-1">{notification.title}</h2>
|
||||
@@ -109,13 +86,13 @@ export function NotificationDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Срок, если есть */}
|
||||
{/* Дедлайн */}
|
||||
{notification.deadline && (
|
||||
<div className="bg-primary/5 border border-primary/20 rounded-lg p-3">
|
||||
<div className={`border rounded-lg p-3 ${style.deadline}`}>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="w-4 h-4 text-primary" />
|
||||
<Calendar className={`w-4 h-4 ${style.deadlineText}`} />
|
||||
<span className="font-medium">Срок:</span>
|
||||
<span className="text-primary font-semibold">{notification.deadline}</span>
|
||||
<span className={`font-semibold ${style.deadlineText}`}>{notification.deadline}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -123,24 +100,20 @@ export function NotificationDetail() {
|
||||
{/* Полное описание */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-sm">Подробности</h3>
|
||||
<p className="text-sm text-foreground/90 leading-relaxed">
|
||||
{notification.fullContent}
|
||||
</p>
|
||||
<p className="text-sm text-foreground/90 leading-relaxed">{notification.fullContent}</p>
|
||||
</div>
|
||||
|
||||
{/* Шаги к действию */}
|
||||
{/* Шаги */}
|
||||
{notification.steps && notification.steps.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-sm">Что нужно сделать</h3>
|
||||
<div className="space-y-2">
|
||||
{notification.steps.map((step, index) => (
|
||||
<div key={index} className="flex gap-2.5">
|
||||
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-xs font-medium flex-shrink-0 mt-0.5">
|
||||
{index + 1}
|
||||
{notification.steps.map((step, i) => (
|
||||
<div key={i} className="flex gap-2.5">
|
||||
<div className={`flex items-center justify-center w-5 h-5 rounded-full text-xs font-medium flex-shrink-0 mt-0.5 ${style.step}`}>
|
||||
{i + 1}
|
||||
</div>
|
||||
<p className="text-sm text-foreground/90 leading-relaxed flex-1">
|
||||
{step}
|
||||
</p>
|
||||
<p className="text-sm text-foreground/90 leading-relaxed flex-1">{step}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -149,7 +122,7 @@ export function NotificationDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопки управления */}
|
||||
{/* Кнопки */}
|
||||
<div className="border-t border-border/40 p-4 space-y-2 bg-background">
|
||||
<button
|
||||
onClick={handleComplete}
|
||||
@@ -167,13 +140,12 @@ export function NotificationDetail() {
|
||||
<Clock className="w-4 h-4" />
|
||||
Сделаю позже
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleHide}
|
||||
className="flex-1 bg-muted text-muted-foreground rounded-lg px-4 py-2.5 text-sm font-medium flex items-center justify-center gap-2 hover:bg-muted/80 transition-colors"
|
||||
>
|
||||
<EyeOff className="w-4 h-4" />
|
||||
Скрыть уведомление
|
||||
Скрыть
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user