143 lines
8.4 KiB
TypeScript
143 lines
8.4 KiB
TypeScript
import { useState } from "react"
|
||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||
import { rosterApi } from '../../lib/api-services'
|
||
import Card from "../../components/ui/Card"
|
||
import Button from "../../components/ui/Button"
|
||
import { Input, Label, Select } from "../../components/ui/Input"
|
||
import { AlertTriangle, Check, Download } from "lucide-react"
|
||
import { toast } from "sonner"
|
||
import { useAuthStore } from '../../store/authStore'
|
||
|
||
// ========== 违纪记录管理 ==========
|
||
|
||
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) => rosterApi.createDisciplinary(employeeId, data),
|
||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) },
|
||
})
|
||
|
||
const deleteMutation = useMutation({
|
||
mutationFn: (id: string) => rosterApi.removeDisciplinary(employeeId, 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>
|
||
<div className="flex items-center gap-2">
|
||
{r.employeeAck && (
|
||
<button
|
||
onClick={async () => {
|
||
try {
|
||
const token = useAuthStore.getState().accessToken
|
||
const res = await fetch(`/api/v1/roster/${employeeId}/disciplinary/${r.id}/certificate`, {
|
||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||
})
|
||
if (!res.ok) throw new Error('导出失败')
|
||
const blob = await res.blob()
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `违纪确认证明_${r.violationDate?.toString().slice(0, 10)}.doc`
|
||
a.click()
|
||
URL.revokeObjectURL(url)
|
||
} catch {
|
||
toast.error('下载失败')
|
||
}
|
||
}}
|
||
className="text-xs text-primary hover:underline flex items-center gap-1 shrink-0"
|
||
>
|
||
<Download className="w-3 h-3" />下载确认证明
|
||
</button>
|
||
)}
|
||
<button onClick={() => deleteMutation.mutate(r.id)} className="text-xs text-gray-300 hover:text-danger shrink-0">删除</button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|