Files
TurboHR/frontend/src/pages/roster/DisciplinaryInfo.tsx
T

143 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}