import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Bell, CheckCircle, AlertCircle, Send, Settings as SettingsIcon, X } from 'lucide-react' import { notificationsApi } from '../lib/api-services' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import EmptyState from '../components/ui/EmptyState' import Pagination from '../components/ui/Pagination' const TYPE_LABELS: Record = { CONTRACT_EXPIRY: '合同到期', CONTRACT_UNSIGNED: '未签合同', OVERTIME: '加班预警', OVERTIME_ALERT: '加班预警', PAYSLIP: '工资条', PAYSLIP_READY: '工资条', RISK_ALERT: '风险预警', SOCIAL_INS: '社保提醒', HOUSING_FUND: '公积金提醒', TAX: '税务提醒', } const CHANNEL_LABELS: Record = { WECHAT: '企业微信', EMAIL: '邮件', IN_APP: '站内', } /** * 通知管理页面 */ export default function Notifications() { const queryClient = useQueryClient() const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [showSettings, setShowSettings] = useState(false) const { data, isLoading } = useQuery({ queryKey: ['notification-logs', page, pageSize], queryFn: async () => { return await notificationsApi.logs({ page, pageSize }) }, }) const { data: settings } = useQuery({ queryKey: ['notification-settings'], queryFn: async () => { return await notificationsApi.settings() }, }) const checkContractsMutation = useMutation({ mutationFn: () => notificationsApi.checkContracts(), onSuccess: () => { toast.success('合同到期检查已触发') queryClient.invalidateQueries({ queryKey: ['notification-logs'] }) }, onError: () => toast.error('检查失败'), }) return (

通知管理

通知记录查看与设置管理

{/* 通知设置概览 */} {settings && (
{[ { label: '合同到期提醒', enabled: settings.contractExpiry, days: settings.expiryDays }, { label: '未签合同提醒', enabled: settings.contractUnsigned }, { label: '加班预警', enabled: settings.overtimeAlert }, { label: '工资条通知', enabled: settings.payslipReady }, ].map(s => (
{s.enabled ? ( ) : ( )}
{s.label}
{s.days &&
提前{s.days}天
}
))}
)} {/* 通知记录列表 */} {isLoading ? (
加载中...
) : !data?.items || data.items.length === 0 ? ( ) : ( <>
{data.items.map((log: any) => (
{log.title} {TYPE_LABELS[log.type] || log.type} {CHANNEL_LABELS[log.channel] || log.channel}
{log.content}
{new Date(log.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
))}
{ setPageSize(s); setPage(1) }} /> )} {/* 通知设置弹窗 */} {showSettings && settings && ( setShowSettings(false)} onSuccess={() => { setShowSettings(false); queryClient.invalidateQueries({ queryKey: ['notification-settings'] }) }} /> )}
) } function SettingsModal({ settings, onClose, onSuccess }: { settings: any; onClose: () => void; onSuccess: () => void }) { const [form, setForm] = useState({ contractExpiry: settings.contractExpiry ?? true, expiryDays: settings.expiryDays ?? 30, contractUnsigned: settings.contractUnsigned ?? true, overtimeAlert: settings.overtimeAlert ?? true, payslipReady: settings.payslipReady ?? true, payrollDay: settings.payrollDay ?? 10, socialInsDay: settings.socialInsDay ?? 15, housingFundDay: settings.housingFundDay ?? 15, taxDay: settings.taxDay ?? 15, wechatWebhook: settings.wechatWebhook ?? '', emailNotify: settings.emailNotify ?? false, email: settings.email ?? '', }) const saveMutation = useMutation({ mutationFn: () => notificationsApi.updateSettings(form), onSuccess: () => { toast.success('通知设置已保存'); onSuccess() }, onError: () => toast.error('保存失败'), }) const toggleItem = (key: string) => setForm(prev => ({ ...prev, [key]: !prev[key as keyof typeof prev] })) return (
e.stopPropagation()}>

通知设置

{[ { key: 'contractExpiry', label: '合同到期提醒', desc: '提前提醒即将到期的合同' }, { key: 'contractUnsigned', label: '未签合同提醒', desc: '入职超过30天未签合同' }, { key: 'overtimeAlert', label: '加班预警', desc: '加班时长超过法定上限' }, { key: 'payslipReady', label: '工资条通知', desc: '工资条生成后通知员工' }, { key: 'emailNotify', label: '邮件通知', desc: '通过邮件发送通知' }, ].map(item => (
{item.label}
{item.desc}
))}
setForm(prev => ({ ...prev, expiryDays: parseInt(e.target.value) || 30 }))} className="w-full mt-1 px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-1 focus:ring-primary" min={1} max={365} />
setForm(prev => ({ ...prev, payrollDay: parseInt(e.target.value) || 10 }))} className="w-full mt-1 px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-1 focus:ring-primary" min={1} max={28} />
setForm(prev => ({ ...prev, wechatWebhook: e.target.value }))} className="w-full mt-1 px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-1 focus:ring-primary" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." />
setForm(prev => ({ ...prev, email: e.target.value }))} className="w-full mt-1 px-2 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-1 focus:ring-primary" placeholder="hr@example.com" />
) }