feat: 培训/绩效/违纪员工端签收+管理端签字状态只读化

- 后端: portal新增培训/绩效/违纪列表查看+签收接口,签收时创建证据链
- 管理端: 弹窗去掉HR手动签字勾选,改为只读显示签字状态
- 绩效列表新增签字状态列
- 员工端: 新增MyRecords页面,支持培训签收/拒绝、绩效签字、违纪签字
- 员工端导航新增'我的记录'入口
- EvidenceCategory新增TRAINING/PERFORMANCE类型
This commit is contained in:
freedakgmail
2026-08-05 08:39:04 +08:00
parent 15dde27701
commit 3afb383172
9 changed files with 437 additions and 18 deletions
@@ -193,7 +193,6 @@ function DisciplinaryForm({ employees, record, onSubmit, onClose }: {
severity: record?.severity || 'WARNING',
action: record?.action || 'ORAL_WARNING',
actionDetail: record?.actionDetail || '',
employeeAck: record?.employeeAck || false,
witness: record?.witness || '',
})
@@ -263,10 +262,18 @@ function DisciplinaryForm({ employees, record, onSubmit, onClose }: {
<Label></Label>
<Input value={form.witness} onChange={(e) => setForm({ ...form, witness: e.target.value })} placeholder="见证人(选填)" />
</div>
<label className="flex items-center gap-2">
<input type="checkbox" checked={form.employeeAck} onChange={(e) => setForm({ ...form, employeeAck: e.target.checked })} />
<span className="text-sm"></span>
</label>
{record && (
<div>
<Label></Label>
<div className="text-sm text-gray-600">
{record.employeeAck ? (
<span className="text-green-600">{record.ackDate ? new Date(record.ackDate).toLocaleDateString('zh-CN') : ''}</span>
) : (
<span className="text-amber-600"> <span className="text-xs text-gray-400 ml-1"></span></span>
)}
</div>
</div>
)}
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" size="sm" onClick={onClose}></Button>
<Button size="sm" onClick={() => onSubmit(form)} disabled={!form.employeeId || !form.description}></Button>
@@ -99,14 +99,15 @@ export default function PerformanceRecords() {
<th className="pb-2 pr-4 font-medium"></th>
<th className="pb-2 pr-4 font-medium"></th>
<th className="pb-2 pr-4 font-medium"></th>
<th className="pb-2 pr-4 font-medium"></th>
<th className="pb-2 pr-4 font-medium"></th>
</tr>
</thead>
<tbody>
{isLoading ? (
<tr><td colSpan={8} className="py-8 text-center text-gray-400">...</td></tr>
<tr><td colSpan={9} className="py-8 text-center text-gray-400">...</td></tr>
) : records.length === 0 ? (
<tr><td colSpan={8} className="py-8 text-center text-gray-400"></td></tr>
<tr><td colSpan={9} className="py-8 text-center text-gray-400"></td></tr>
) : records.map((r: any) => (
<tr key={r.id} className="border-b hover:bg-gray-50">
<td className="py-2 pr-4">
@@ -122,6 +123,13 @@ export default function PerformanceRecords() {
</span>
</td>
<td className="py-2 pr-4 text-gray-600">{r.reviewer || '-'}</td>
<td className="py-2 pr-4">
{r.employeeAck ? (
<span className="text-xs text-green-600"></span>
) : (
<span className="text-xs text-amber-600"></span>
)}
</td>
<td className="py-2 pr-4">
<div className="flex gap-1">
<button onClick={() => setEditRecord(r)} className="p-1 hover:bg-gray-100 rounded">
@@ -179,7 +187,6 @@ function PerformanceForm({ employees, record, onSubmit, onClose }: {
summary: record?.summary || '',
improvementPlan: record?.improvementPlan || '',
reviewer: record?.reviewer || '',
employeeAck: record?.employeeAck || false,
})
return (
@@ -241,6 +248,18 @@ function PerformanceForm({ employees, record, onSubmit, onClose }: {
<Label></Label>
<Input value={form.improvementPlan} onChange={(e) => setForm({ ...form, improvementPlan: e.target.value })} placeholder="改进计划(选填)" />
</div>
{record && (
<div>
<Label></Label>
<div className="text-sm text-gray-600">
{record.employeeAck ? (
<span className="text-green-600">{record.ackDate ? new Date(record.ackDate).toLocaleDateString('zh-CN') : ''}</span>
) : (
<span className="text-amber-600"> <span className="text-xs text-gray-400 ml-1"></span></span>
)}
</div>
</div>
)}
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" size="sm" onClick={onClose}></Button>
<Button size="sm" onClick={() => onSubmit(form)} disabled={!form.employeeId || !form.period}></Button>
+11 -9
View File
@@ -197,7 +197,6 @@ function TrainingForm({ employees, record, onSubmit, onClose }: {
content: record?.content || '',
trainer: record?.trainer || '',
duration: record?.duration || 0,
ackStatus: record?.ackStatus || 'PENDING',
remark: record?.remark || '',
})
@@ -242,14 +241,17 @@ function TrainingForm({ employees, record, onSubmit, onClose }: {
<Input type="number" min={0} step={0.5} value={form.duration} onChange={(e) => setForm({ ...form, duration: Number(e.target.value) })} />
</div>
</div>
<div>
<Label></Label>
<Select value={form.ackStatus} onChange={(e) => setForm({ ...form, ackStatus: e.target.value })}>
<option value="PENDING"></option>
<option value="SIGNED"></option>
<option value="REFUSED"></option>
</Select>
</div>
{record && (
<div>
<Label></Label>
<div className="text-sm text-gray-600">
<span className={`inline-block px-2 py-0.5 rounded text-xs ${ACK_COLORS[record.ackStatus] || 'bg-gray-50 text-gray-600'}`}>
{ACK_LABELS[record.ackStatus] || record.ackStatus}
</span>
<span className="ml-2 text-xs text-gray-400"></span>
</div>
</div>
)}
<div>
<Label></Label>
<Input value={form.remark} onChange={(e) => setForm({ ...form, remark: e.target.value })} placeholder="备注" />