feat: 发薪日期多选+提前N天提醒+电子签设置区域修复
- Schema: 去掉 payrollFrequency,新增 payrollDays (JSON数组) + payrollReminderDays (Int) - 设置页: 发薪日期改为1-28号多选按钮,新增提前提醒天数设置 - 设置页: 恢复电子签署设置区域(3个开关:规章制度/工资条/入职文件) - 工作日历: 发薪日期作为 PAYROLL_DAY 事件显示 - 工作台: 提前N天提醒发薪日期,N可配置 - TaskCenter: 新增发薪提醒分类图标 - seed文件: 更新为 payrollDays 格式
This commit is contained in:
@@ -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) // 工资条电子签
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ async function main() {
|
||||
plan: 'PRO',
|
||||
maxEmployees: 50,
|
||||
city: '上海',
|
||||
payrollFrequency: 1,
|
||||
payrollDays: [10],
|
||||
},
|
||||
})
|
||||
console.log('企业已创建:', org.name)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
// 过滤空分类
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,6 +15,7 @@ const EVENT_TYPE_COLORS: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
ANNIVERSARY: '入职周年',
|
||||
RISK_DEADLINE: '风险截止',
|
||||
RETIREMENT: '退休',
|
||||
PAYROLL_DAY: '发薪日',
|
||||
CUSTOM: '自定义',
|
||||
MEETING: '会议',
|
||||
TEAM_BUILDING: '团建',
|
||||
|
||||
@@ -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:
|
||||
<Input value={form.contactPhone} onChange={(e) => setForm({ ...form, contactPhone: e.target.value })} placeholder="联系电话" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>每月发薪次数</Label>
|
||||
<Select value={String(form.payrollFrequency)} onChange={(e) => setForm({ ...form, payrollFrequency: Number(e.target.value) })}>
|
||||
<option value="1">1次(一月一批)</option>
|
||||
<option value="2">2次(半月一批)</option>
|
||||
<option value="3">3次(旬批)</option>
|
||||
<option value="4">4次(周批)</option>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500 mt-1">设置每月发薪批次数,系统将按此数量管理发薪批次</p>
|
||||
<Label>发薪日期</Label>
|
||||
<p className="text-xs text-gray-500 mt-1 mb-2">设置每月发薪日期(可多选),系统将在工作日历中显示,并提前提醒</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{Array.from({ length: 28 }, (_, i) => i + 1).map(day => (
|
||||
<button
|
||||
key={day}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const days = form.payrollDays.includes(day)
|
||||
? form.payrollDays.filter(d => d !== day)
|
||||
: [...form.payrollDays, day].sort((a, b) => a - b)
|
||||
setForm({ ...form, payrollDays: days })
|
||||
}}
|
||||
className={`w-9 h-9 rounded-md text-xs font-medium border transition-colors ${
|
||||
form.payrollDays.includes(day)
|
||||
? 'bg-primary text-white border-primary'
|
||||
: 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{form.payrollDays.length > 0 && (
|
||||
<p className="text-xs text-gray-500 mt-2">已选:每月 {form.payrollDays.map(d => `${d}号`).join('、')} 发薪</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>发薪提前提醒天数</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input type="number" min={0} max={30} value={form.payrollReminderDays} onChange={(e) => setForm({ ...form, payrollReminderDays: Number(e.target.value) })} className="!w-24" />
|
||||
<span className="text-xs text-gray-500">天前在工作台提醒发薪</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => onSave(form)} disabled={saving}>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 电子签署设置 */}
|
||||
<div className="mt-6 pt-6 border-t">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<PenTool className="w-5 h-5 text-primary" />
|
||||
<h3 className="font-medium">电子签署设置</h3>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium">规章制度电子签</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5">开启后,员工阅读规章制度时需电子签署;关闭时保持阅读确认</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = !form.esignPolicyEnabled
|
||||
setForm({ ...form, esignPolicyEnabled: next })
|
||||
onSave({ esignPolicyEnabled: next })
|
||||
}}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${form.esignPolicyEnabled ? 'bg-primary' : 'bg-gray-200'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${form.esignPolicyEnabled ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium">工资条电子签</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5">开启后,员工确认工资条时需电子签署;关闭时保持点击确认</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = !form.esignPayslipEnabled
|
||||
setForm({ ...form, esignPayslipEnabled: next })
|
||||
onSave({ esignPayslipEnabled: next })
|
||||
}}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${form.esignPayslipEnabled ? 'bg-primary' : 'bg-gray-200'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${form.esignPayslipEnabled ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium">入职文件电子签</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5">开启后,HR审批通过入职流程时自动创建入职文件签署;关闭时不签</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = !form.esignOnboardingEnabled
|
||||
setForm({ ...form, esignOnboardingEnabled: next })
|
||||
onSave({ esignOnboardingEnabled: next })
|
||||
}}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${form.esignOnboardingEnabled ? 'bg-primary' : 'bg-gray-200'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${form.esignOnboardingEnabled ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 显示设置 */}
|
||||
<div className="mt-6 pt-6 border-t">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
|
||||
@@ -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<string, typeof AlertCircle> = {
|
||||
'待办事项': AlertCircle,
|
||||
'发薪批次': Layers,
|
||||
'发薪提醒': Wallet,
|
||||
'合同到期': FileText,
|
||||
'特殊状态': Heart,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user