d79e3baa34
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
117 lines
7.3 KiB
TypeScript
117 lines
7.3 KiB
TypeScript
import { useState, useRef } from "react"
|
||
import { toast } from "sonner"
|
||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||
import api from "../../lib/api"
|
||
import Card from "../../components/ui/Card"
|
||
import Button from "../../components/ui/Button"
|
||
import { Input, Label, Select } from "../../components/ui/Input"
|
||
import Modal from "../../components/ui/Modal"
|
||
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus, Briefcase, FileSignature, DollarSign, Building2, RotateCcw, History } from "lucide-react"
|
||
import { fmt } from "./shared"
|
||
|
||
// ========== 违纪记录管理 ==========
|
||
|
||
export default function DisciplinaryInfo({ employeeId, records }: { employeeId: string; records: any[] }) {
|
||
const queryClient = useQueryClient()
|
||
const [showForm, setShowForm] = useState(false)
|
||
const [form, setForm] = useState({ violationDate: '', violationType: 'LATE', description: '', severity: 'WARNING', action: 'ORAL_WARNING', actionDetail: '', employeeAck: false, ackDate: '', ackMethod: 'SIGN', witness: '' })
|
||
|
||
const createMutation = useMutation({
|
||
mutationFn: (data: any) => api.post(`/roster/${employeeId}/disciplinary`, data),
|
||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) },
|
||
})
|
||
|
||
const deleteMutation = useMutation({
|
||
mutationFn: (id: string) => api.delete(`/roster/${employeeId}/disciplinary/${id}`),
|
||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }),
|
||
})
|
||
|
||
const typeMap: Record<string, string> = { LATE: '迟到', ABSENT: '旷工', INSUBORDINATION: '不服从管理', MISCONDUCT: '违纪', VIOLATE_POLICY: '违反规章制度', OTHER: '其他' }
|
||
const actionMap: Record<string, string> = { ORAL_WARNING: '口头警告', WRITTEN_WARNING: '书面警告', DEDUCTION: '扣款', DEMOTION: '降职', TERMINATION: '解除劳动合同' }
|
||
const severityMap: Record<string, string> = { WARNING: '警告', SERIOUS: '严重', SEVERE: '重度' }
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<div className="flex justify-between items-center">
|
||
<h2 className="text-xs font-medium">违纪记录({records?.length || 0}条)</h2>
|
||
<Button size="sm" onClick={() => setShowForm(!showForm)}>新增违纪记录</Button>
|
||
</div>
|
||
|
||
{showForm && (
|
||
<Card>
|
||
<div className="grid md:grid-cols-2 gap-4">
|
||
<div><Label>违纪日期</Label><Input type="date" value={form.violationDate} onChange={(e) => setForm({ ...form, violationDate: e.target.value })} /></div>
|
||
<div><Label>违纪类型</Label>
|
||
<Select value={form.violationType} onChange={(e) => setForm({ ...form, violationType: e.target.value })}>
|
||
{Object.entries(typeMap).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</Select>
|
||
</div>
|
||
<div className="md:col-span-2"><Label>违纪事实描述</Label><Input value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="详细描述违纪事实" /></div>
|
||
<div><Label>严重程度</Label>
|
||
<Select value={form.severity} onChange={(e) => setForm({ ...form, severity: e.target.value })}>
|
||
{Object.entries(severityMap).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</Select>
|
||
</div>
|
||
<div><Label>处理结果</Label>
|
||
<Select value={form.action} onChange={(e) => setForm({ ...form, action: e.target.value })}>
|
||
{Object.entries(actionMap).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</Select>
|
||
</div>
|
||
<div><Label>处理详情</Label><Input value={form.actionDetail} onChange={(e) => setForm({ ...form, actionDetail: e.target.value })} placeholder="扣款金额/降职说明等" /></div>
|
||
<div><Label>见证人</Label><Input value={form.witness} onChange={(e) => setForm({ ...form, witness: e.target.value })} /></div>
|
||
<div className="flex items-center gap-2 pt-6">
|
||
<input type="checkbox" id="empAck" checked={form.employeeAck} onChange={(e) => setForm({ ...form, employeeAck: e.target.checked })} />
|
||
<label htmlFor="empAck" className="text-xs">员工已签字确认</label>
|
||
</div>
|
||
{form.employeeAck && <div><Label>确认日期</Label><Input type="date" value={form.ackDate} onChange={(e) => setForm({ ...form, ackDate: e.target.value })} /></div>}
|
||
<div className="md:col-span-2 flex gap-2">
|
||
<Button onClick={() => createMutation.mutate(form)} disabled={createMutation.isPending || !form.violationDate || !form.description}>
|
||
{createMutation.isPending ? '保存中...' : '保存'}
|
||
</Button>
|
||
<Button variant="secondary" onClick={() => setShowForm(false)}>取消</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{records?.length === 0 ? (
|
||
<Card><div className="text-center py-8 text-gray-400">暂无违纪记录</div></Card>
|
||
) : records?.map((r) => (
|
||
<Card key={r.id} className="p-4">
|
||
<div className="flex justify-between items-start gap-3">
|
||
<div className="flex-1 space-y-2">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="text-xs font-medium text-gray-700">{r.violationDate?.toString().slice(0, 10)}</span>
|
||
<span className={`px-2 py-0.5 rounded text-xs ${r.severity === 'SEVERE' ? 'bg-red-100 text-red-700' : r.severity === 'SERIOUS' ? 'bg-orange-50 text-orange-600' : 'bg-amber-50 text-amber-600'}`}>
|
||
{severityMap[r.severity] || r.severity}
|
||
</span>
|
||
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600 text-xs">{typeMap[r.violationType] || r.violationType}</span>
|
||
</div>
|
||
<div className="text-xs text-gray-600 leading-relaxed">{r.description}</div>
|
||
<div className="flex items-center gap-2 text-xs">
|
||
<span className="text-gray-400">处理方式</span>
|
||
<span className="px-2 py-0.5 rounded bg-blue-50 text-blue-600">{actionMap[r.action] || r.action}</span>
|
||
{r.actionDetail && <span className="text-gray-500">{r.actionDetail}</span>}
|
||
</div>
|
||
<div className="flex items-center gap-4 text-xs pt-1 border-t">
|
||
{r.employeeAck ? (
|
||
<span className="text-safe flex items-center gap-1">
|
||
<Check className="w-3 h-3" />已签字({r.ackDate?.toString().slice(0, 10)})
|
||
</span>
|
||
) : (
|
||
<span className="text-warning flex items-center gap-1">
|
||
<AlertTriangle className="w-3 h-3" />未签字
|
||
</span>
|
||
)}
|
||
{r.witness && <span className="text-gray-400">见证人:{r.witness}</span>}
|
||
{r.ackMethod && <span className="text-gray-400">确认方式:{r.ackMethod === 'SIGN' ? '签字' : r.ackMethod === 'ELECTRONIC' ? '电子' : '拒绝'}</span>}
|
||
</div>
|
||
</div>
|
||
<button onClick={() => deleteMutation.mutate(r.id)} className="text-xs text-gray-300 hover:text-danger shrink-0">删除</button>
|
||
</div>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|