From 6ec939dc7fe5958ae74d69bb1c83d627087dfefd Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Wed, 5 Aug 2026 08:04:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E8=96=AA=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E5=A4=9A=E9=80=89+=E6=8F=90=E5=89=8DN=E5=A4=A9=E6=8F=90?= =?UTF-8?q?=E9=86=92+=E7=94=B5=E5=AD=90=E7=AD=BE=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Schema: 去掉 payrollFrequency,新增 payrollDays (JSON数组) + payrollReminderDays (Int) - 设置页: 发薪日期改为1-28号多选按钮,新增提前提醒天数设置 - 设置页: 恢复电子签署设置区域(3个开关:规章制度/工资条/入职文件) - 工作日历: 发薪日期作为 PAYROLL_DAY 事件显示 - 工作台: 提前N天提醒发薪日期,N可配置 - TaskCenter: 新增发薪提醒分类图标 - seed文件: 更新为 payrollDays 格式 --- backend/prisma/schema.prisma | 3 +- backend/prisma/seed-multi-org.ts | 10 +- backend/prisma/seed.ts | 2 +- backend/src/routes/dashboard.routes.ts | 42 ++++++++ backend/src/routes/platform.routes.ts | 2 +- backend/src/routes/settings.routes.ts | 9 +- backend/src/services/risk.service.ts | 22 +++- frontend/src/pages/Calendar.tsx | 2 + frontend/src/pages/Settings.tsx | 108 +++++++++++++++++-- frontend/src/pages/dashboard/TaskCenter.tsx | 3 +- frontend/src/pages/platform/PlatformOrgs.tsx | 2 +- 11 files changed, 180 insertions(+), 25 deletions(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index d80b5ff..6c1035d 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -130,7 +130,8 @@ model Organization { city String? contactName String? contactPhone String? - payrollFrequency Int @default(1) // 每月发薪次数(1=一次一批) + payrollDays Json @default("[]") // 每月发薪日期,如 [5, 20] 表示每月5号和20号 + payrollReminderDays Int @default(3) // 发薪提前提醒天数 retirementReminderEnabled Boolean @default(false) // 退休提醒开关 esignPolicyEnabled Boolean @default(false) // 规章制度电子签 esignPayslipEnabled Boolean @default(false) // 工资条电子签 diff --git a/backend/prisma/seed-multi-org.ts b/backend/prisma/seed-multi-org.ts index 730ad02..ecf1a14 100644 --- a/backend/prisma/seed-multi-org.ts +++ b/backend/prisma/seed-multi-org.ts @@ -49,7 +49,7 @@ interface OrgConfig { adminName: string contactName: string contactPhone: string - payrollFrequency: number + payrollDays: number[] retirementReminderEnabled: boolean socialConfig: { city: string @@ -89,7 +89,7 @@ const ORGS: OrgConfig[] = [ adminName: '王建国', contactName: '王建国', contactPhone: '13800000010', - payrollFrequency: 1, + payrollDays: [10], retirementReminderEnabled: true, socialConfig: { city: '北京', @@ -122,7 +122,7 @@ const ORGS: OrgConfig[] = [ adminName: '李明华', contactName: '李明华', contactPhone: '13800000020', - payrollFrequency: 2, + payrollDays: [5, 20], retirementReminderEnabled: true, socialConfig: { city: '深圳', @@ -157,7 +157,7 @@ const ORGS: OrgConfig[] = [ adminName: '赵雪梅', contactName: '赵雪梅', contactPhone: '13800000030', - payrollFrequency: 1, + payrollDays: [10], retirementReminderEnabled: false, socialConfig: { city: '杭州', @@ -213,7 +213,7 @@ async function createOrg(orgConfig: OrgConfig) { city: orgConfig.city, contactName: orgConfig.contactName, contactPhone: orgConfig.contactPhone, - payrollFrequency: orgConfig.payrollFrequency, + payrollDays: orgConfig.payrollDays, retirementReminderEnabled: orgConfig.retirementReminderEnabled, }, }) diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index f1b13e5..9d90552 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -78,7 +78,7 @@ async function main() { plan: 'PRO', maxEmployees: 50, city: '上海', - payrollFrequency: 1, + payrollDays: [10], }, }) console.log('企业已创建:', org.name) diff --git a/backend/src/routes/dashboard.routes.ts b/backend/src/routes/dashboard.routes.ts index 2e84e0d..d6a3f78 100644 --- a/backend/src/routes/dashboard.routes.ts +++ b/backend/src/routes/dashboard.routes.ts @@ -285,6 +285,43 @@ router.get('/workspace/next-actions', authMiddleware, async (req: AuthRequest, r take: 10, }) + // 5. 发薪日提前提醒 + const org = await prisma.organization.findUnique({ + where: { id: orgId }, + select: { payrollDays: true, payrollReminderDays: true }, + }) + const payrollDays = Array.isArray(org?.payrollDays) ? org.payrollDays as number[] : [] + const reminderDays = org?.payrollReminderDays ?? 3 + const payrollReminderItems: any[] = [] + const currentYear = now.getFullYear() + const currentMonth = now.getMonth() // 0-indexed + for (const day of payrollDays) { + // 本月发薪日 + const thisMonthPayday = new Date(currentYear, currentMonth, day) + const diffDays = Math.floor((thisMonthPayday.getTime() - now.getTime()) / 86400000) + if (diffDays >= 0 && diffDays <= reminderDays) { + payrollReminderItems.push({ + id: `payroll-${currentYear}-${currentMonth + 1}-${day}`, + title: `发薪日(每月${day}号)${diffDays === 0 ? '今天' : `${diffDays}天后`}`, + subtitle: diffDays === 0 ? '今天发薪' : `还有${diffDays}天`, + link: '/money', + }) + } + // 下月发薪日(如果当月已过,看下月) + if (diffDays < 0) { + const nextMonthPayday = new Date(currentYear, currentMonth + 1, day) + const nextDiffDays = Math.floor((nextMonthPayday.getTime() - now.getTime()) / 86400000) + if (nextDiffDays >= 0 && nextDiffDays <= reminderDays) { + payrollReminderItems.push({ + id: `payroll-${currentYear}-${currentMonth + 2}-${day}`, + title: `发薪日(下月${day}号)${nextDiffDays === 0 ? '今天' : `${nextDiffDays}天后`}`, + subtitle: `还有${nextDiffDays}天`, + link: '/money', + }) + } + } + } + // 按优先级分组 const actions: Array<{ category: string; priority: 'high' | 'medium' | 'low'; items: any[] }> = [ { @@ -333,6 +370,11 @@ router.get('/workspace/next-actions', authMiddleware, async (req: AuthRequest, r link: '/special-status', })), }, + { + category: '发薪提醒', + priority: 'medium', + items: payrollReminderItems, + }, ] // 过滤空分类 diff --git a/backend/src/routes/platform.routes.ts b/backend/src/routes/platform.routes.ts index 8d679d7..74c7127 100644 --- a/backend/src/routes/platform.routes.ts +++ b/backend/src/routes/platform.routes.ts @@ -115,7 +115,7 @@ router.get('/orgs', async (req: AuthRequest, res, next) => { select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, - payrollFrequency: true, retirementReminderEnabled: true, + payrollDays: true, retirementReminderEnabled: true, createdAt: true, updatedAt: true, _count: { select: { employees: true, users: true, contracts: true, payslips: true }, diff --git a/backend/src/routes/settings.routes.ts b/backend/src/routes/settings.routes.ts index 9b70861..02649e8 100644 --- a/backend/src/routes/settings.routes.ts +++ b/backend/src/routes/settings.routes.ts @@ -28,7 +28,7 @@ router.get('/org', async (req: AuthRequest, res, next) => { try { const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId }, - select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollFrequency: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true, createdAt: true }, + select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollDays: true, payrollReminderDays: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true, createdAt: true }, }) res.json({ success: true, data: org }) } catch (err) { @@ -39,10 +39,11 @@ router.get('/org', async (req: AuthRequest, res, next) => { // 更新企业信息 router.put('/org', requireAdmin, async (req: AuthRequest, res, next) => { try { - const { name, payrollFrequency, city, contactName, contactPhone, retirementReminderEnabled, esignPolicyEnabled, esignPayslipEnabled, esignOnboardingEnabled } = req.body as { name?: string; payrollFrequency?: number; city?: string; contactName?: string; contactPhone?: string; retirementReminderEnabled?: boolean; esignPolicyEnabled?: boolean; esignPayslipEnabled?: boolean; esignOnboardingEnabled?: boolean } + const { name, payrollDays, payrollReminderDays, city, contactName, contactPhone, retirementReminderEnabled, esignPolicyEnabled, esignPayslipEnabled, esignOnboardingEnabled } = req.body as { name?: string; payrollDays?: number[]; payrollReminderDays?: number; city?: string; contactName?: string; contactPhone?: string; retirementReminderEnabled?: boolean; esignPolicyEnabled?: boolean; esignPayslipEnabled?: boolean; esignOnboardingEnabled?: boolean } const updateData: any = {} if (name) updateData.name = name - if (payrollFrequency !== undefined) updateData.payrollFrequency = payrollFrequency + if (payrollDays !== undefined) updateData.payrollDays = payrollDays + if (payrollReminderDays !== undefined) updateData.payrollReminderDays = payrollReminderDays if (city !== undefined) updateData.city = city if (contactName !== undefined) updateData.contactName = contactName if (contactPhone !== undefined) updateData.contactPhone = contactPhone @@ -53,7 +54,7 @@ router.put('/org', requireAdmin, async (req: AuthRequest, res, next) => { const org = await prisma.organization.update({ where: { id: req.user!.orgId }, data: updateData, - select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollFrequency: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true }, + select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollDays: true, payrollReminderDays: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true }, }) res.json({ success: true, data: org }) } catch (err) { diff --git a/backend/src/services/risk.service.ts b/backend/src/services/risk.service.ts index bda1022..71dff5b 100644 --- a/backend/src/services/risk.service.ts +++ b/backend/src/services/risk.service.ts @@ -1184,7 +1184,27 @@ export async function getMonthlyCalendar(orgId: string, month: string) { } } - // 7. 自定义日历事件 + // 7. 发薪日期 + const org = await prisma.organization.findUnique({ + where: { id: orgId }, + select: { payrollDays: true }, + }) + const payrollDays = Array.isArray(org?.payrollDays) ? org.payrollDays as number[] : [] + for (const day of payrollDays) { + const dateStr = `${month}-${String(day).padStart(2, '0')}` + const payrollDate = new Date(parseInt(year), monthNum - 1, day) + if (payrollDate >= monthStart && payrollDate <= monthEnd) { + events.push({ + date: dateStr, + type: 'PAYROLL_DAY', + title: `发薪日(每月${day}号)`, + actionUrl: '/money', + priority: 'medium', + }) + } + } + + // 8. 自定义日历事件 const customEvents = await prisma.calendarEvent.findMany({ where: { orgId, diff --git a/frontend/src/pages/Calendar.tsx b/frontend/src/pages/Calendar.tsx index c8c93bf..0efe37a 100644 --- a/frontend/src/pages/Calendar.tsx +++ b/frontend/src/pages/Calendar.tsx @@ -15,6 +15,7 @@ const EVENT_TYPE_COLORS: Record = { ANNIVERSARY: 'bg-green-100 text-green-700 border-green-200', RISK_DEADLINE: 'bg-orange-100 text-orange-700 border-orange-200', RETIREMENT: 'bg-purple-100 text-purple-700 border-purple-200', + PAYROLL_DAY: 'bg-emerald-100 text-emerald-700 border-emerald-200', CUSTOM: 'bg-blue-100 text-blue-700 border-blue-200', MEETING: 'bg-cyan-100 text-cyan-700 border-cyan-200', TEAM_BUILDING: 'bg-pink-100 text-pink-700 border-pink-200', @@ -29,6 +30,7 @@ const EVENT_TYPE_LABELS: Record = { ANNIVERSARY: '入职周年', RISK_DEADLINE: '风险截止', RETIREMENT: '退休', + PAYROLL_DAY: '发薪日', CUSTOM: '自定义', MEETING: '会议', TEAM_BUILDING: '团建', diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index e3236f1..532f568 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -94,7 +94,8 @@ function OrgSettings({ orgData, onSave, saving }: { orgData: any; onSave: (data: name: '', contactName: '', contactPhone: '', - payrollFrequency: 1, + payrollDays: [] as number[], + payrollReminderDays: 3, retirementReminderEnabled: false, esignPolicyEnabled: false, esignPayslipEnabled: false, @@ -107,7 +108,8 @@ function OrgSettings({ orgData, onSave, saving }: { orgData: any; onSave: (data: name: orgData.name || '', contactName: orgData.contactName || '', contactPhone: orgData.contactPhone || '', - payrollFrequency: orgData.payrollFrequency || 1, + payrollDays: Array.isArray(orgData.payrollDays) ? orgData.payrollDays : [], + payrollReminderDays: orgData.payrollReminderDays ?? 3, retirementReminderEnabled: orgData.retirementReminderEnabled || false, esignPolicyEnabled: orgData.esignPolicyEnabled || false, esignPayslipEnabled: orgData.esignPayslipEnabled || false, @@ -133,20 +135,106 @@ function OrgSettings({ orgData, onSave, saving }: { orgData: any; onSave: (data: setForm({ ...form, contactPhone: e.target.value })} placeholder="联系电话" />
- - -

设置每月发薪批次数,系统将按此数量管理发薪批次

+ +

设置每月发薪日期(可多选),系统将在工作日历中显示,并提前提醒

+
+ {Array.from({ length: 28 }, (_, i) => i + 1).map(day => ( + + ))} +
+ {form.payrollDays.length > 0 && ( +

已选:每月 {form.payrollDays.map(d => `${d}号`).join('、')} 发薪

+ )} +
+
+ +
+ setForm({ ...form, payrollReminderDays: Number(e.target.value) })} className="!w-24" /> + 天前在工作台提醒发薪 +
+ {/* 电子签署设置 */} +
+
+ +

电子签署设置

+
+
+
+
+
规章制度电子签
+

开启后,员工阅读规章制度时需电子签署;关闭时保持阅读确认

+
+ +
+
+
+
工资条电子签
+

开启后,员工确认工资条时需电子签署;关闭时保持点击确认

+
+ +
+
+
+
入职文件电子签
+

开启后,HR审批通过入职流程时自动创建入职文件签署;关闭时不签

+
+ +
+
+
+ {/* 显示设置 */}
diff --git a/frontend/src/pages/dashboard/TaskCenter.tsx b/frontend/src/pages/dashboard/TaskCenter.tsx index dc3506e..2797560 100644 --- a/frontend/src/pages/dashboard/TaskCenter.tsx +++ b/frontend/src/pages/dashboard/TaskCenter.tsx @@ -4,7 +4,7 @@ */ import { useQuery } from '@tanstack/react-query' import { Link } from 'react-router-dom' -import { AlertCircle, Layers, FileText, Heart, ArrowRight, ListTodo } from 'lucide-react' +import { AlertCircle, Layers, FileText, Heart, ArrowRight, ListTodo, Wallet } from 'lucide-react' import { dashboardApi } from '../../lib/api-services' import { InlineAlert } from '../../components/ui/InlineAlert' @@ -27,6 +27,7 @@ interface ActionGroup { const categoryIcons: Record = { '待办事项': AlertCircle, '发薪批次': Layers, + '发薪提醒': Wallet, '合同到期': FileText, '特殊状态': Heart, } diff --git a/frontend/src/pages/platform/PlatformOrgs.tsx b/frontend/src/pages/platform/PlatformOrgs.tsx index ddd15d0..06eb4a4 100644 --- a/frontend/src/pages/platform/PlatformOrgs.tsx +++ b/frontend/src/pages/platform/PlatformOrgs.tsx @@ -12,7 +12,7 @@ import Button from '../../components/ui/Button' interface Org { id: string; name: string; plan: string; maxEmployees: number city: string | null; contactName: string | null; contactPhone: string | null - payrollFrequency: number; retirementReminderEnabled: boolean + payrollDays: number[]; retirementReminderEnabled: boolean createdAt: string; updatedAt: string employeeCount: number; userCount: number; contractCount: number; payslipCount: number }