feat: Sondervertretungen als eigene HR-Seite (/hr/special-assignments)
- Neue Seite SpecialAssignmentsPage mit Filter, Tabelle, Add/Edit-Modal - Farbcodierung: Faktor >1.0 amber, <1.0 blau, =1.0 grau - Monat-Filterung client-seitig, paralleles Laden in Batches - Layout.tsx: Nav-Eintrag in Hauptnavigation - App.tsx: Route /hr/special-assignments - UsersPage: Sondervertretungs-Block aus Edit-Modal entfernt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { SpecialAssignmentOut, SpecialAssignmentCreate, AssignmentMode } from '../types/specialAssignment'
|
||||
import { api } from '../api/client'
|
||||
import { Spinner } from '../components/Spinner'
|
||||
import { Layout } from '../components/Layout'
|
||||
@@ -135,15 +134,6 @@ export function UsersPage() {
|
||||
// Company-Settings (für Auto-Modus / Pflicht-Anzeige)
|
||||
const [company, setCompany] = useState<CompanyOut | null>(null)
|
||||
|
||||
// Sondervertretungs-Zuweisungen im Edit-Modal
|
||||
const [assignments, setAssignments] = useState<SpecialAssignmentOut[]>([])
|
||||
const [assignmentsLoading, setAssignmentsLoading] = useState(false)
|
||||
const [newAssignment, setNewAssignment] = useState<SpecialAssignmentCreate>({
|
||||
date_from: '', date_to: '', factor: 1.5, mode: 'both',
|
||||
})
|
||||
const [assignmentSaving, setAssignmentSaving] = useState(false)
|
||||
const [assignmentError, setAssignmentError] = useState('')
|
||||
|
||||
// CSV-Import modal
|
||||
const [showImport, setShowImport] = useState(false)
|
||||
const [importFile, setImportFile] = useState<File | null>(null)
|
||||
@@ -433,12 +423,6 @@ export function UsersPage() {
|
||||
setEditUser(u); setEditRole(u.role); setEditScheduleId(u.work_schedule_id)
|
||||
setEditKuerzel(u.kuerzel ?? ''); setEditPersonnelNr(u.personnel_number ?? '')
|
||||
setEditCanManual(u.can_manual_time_entry); setPersonnelCheck('idle')
|
||||
setAssignmentError('')
|
||||
setAssignmentsLoading(true)
|
||||
api.get<SpecialAssignmentOut[]>(`/users/${u.id}/special-assignments`).then(r => {
|
||||
setAssignments(r)
|
||||
setAssignmentsLoading(false)
|
||||
}).catch(() => setAssignmentsLoading(false))
|
||||
}}
|
||||
className='text-xs text-blue-600 hover:underline'
|
||||
>
|
||||
@@ -783,96 +767,6 @@ export function UsersPage() {
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{/* ── Sondervertretungs-Zeiträume ── */}
|
||||
<div className='border-t border-gray-100 pt-3'>
|
||||
<h4 className='text-xs font-semibold text-gray-700 mb-2'>🏅 Sondervertretungs-Zeiträume</h4>
|
||||
{assignmentsLoading ? (
|
||||
<p className='text-xs text-gray-400'>Lade…</p>
|
||||
) : (
|
||||
<>
|
||||
{assignments.length === 0 && (
|
||||
<p className='text-xs text-gray-400 mb-2'>Keine Zuweisungen vorhanden.</p>
|
||||
)}
|
||||
{assignments.map(a => (
|
||||
<div key={a.id} className='flex items-center justify-between bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mb-1.5 text-xs'>
|
||||
<div>
|
||||
<span className='font-medium text-amber-800'>{a.label || 'Sondervertretung'}</span>
|
||||
<span className='ml-2 text-amber-700'>{a.date_from} – {a.date_to}</span>
|
||||
<span className='ml-2 font-semibold text-amber-900'>×{Number(a.factor).toFixed(2)}</span>
|
||||
<span className='ml-2 text-gray-500'>({a.mode})</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await api.del(`/users/${editUser!.id}/special-assignments/${a.id}`)
|
||||
setAssignments(prev => prev.filter(x => x.id !== a.id))
|
||||
}}
|
||||
className='text-red-500 hover:text-red-700 ml-2 font-bold'
|
||||
title='Löschen'
|
||||
>✕</button>
|
||||
</div>
|
||||
))}
|
||||
{/* Neue Zuweisung anlegen */}
|
||||
<div className='grid grid-cols-2 gap-2 mt-2'>
|
||||
<div>
|
||||
<label className='text-xs text-gray-600'>Von</label>
|
||||
<input type='date' value={newAssignment.date_from}
|
||||
onChange={e => setNewAssignment(p => ({ ...p, date_from: e.target.value }))}
|
||||
className='w-full border border-gray-300 rounded-lg px-2 py-1.5 text-xs mt-0.5' />
|
||||
</div>
|
||||
<div>
|
||||
<label className='text-xs text-gray-600'>Bis</label>
|
||||
<input type='date' value={newAssignment.date_to}
|
||||
onChange={e => setNewAssignment(p => ({ ...p, date_to: e.target.value }))}
|
||||
className='w-full border border-gray-300 rounded-lg px-2 py-1.5 text-xs mt-0.5' />
|
||||
</div>
|
||||
<div>
|
||||
<label className='text-xs text-gray-600'>Faktor (z.B. 1.5)</label>
|
||||
<input type='number' step='0.1' min='0.1' max='10' value={newAssignment.factor}
|
||||
onChange={e => setNewAssignment(p => ({ ...p, factor: parseFloat(e.target.value) }))}
|
||||
className='w-full border border-gray-300 rounded-lg px-2 py-1.5 text-xs mt-0.5' />
|
||||
</div>
|
||||
<div>
|
||||
<label className='text-xs text-gray-600'>Ziel</label>
|
||||
<select value={newAssignment.mode}
|
||||
onChange={e => setNewAssignment(p => ({ ...p, mode: e.target.value as AssignmentMode }))}
|
||||
className='w-full border border-gray-300 rounded-lg px-2 py-1.5 text-xs mt-0.5'>
|
||||
<option value='both'>FZA + Abrechnung</option>
|
||||
<option value='fza'>Nur FZA</option>
|
||||
<option value='payroll'>Nur Abrechnung</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<label className='text-xs text-gray-600'>Bezeichnung (optional)</label>
|
||||
<input type='text' value={newAssignment.label ?? ''}
|
||||
onChange={e => setNewAssignment(p => ({ ...p, label: e.target.value }))}
|
||||
placeholder='z.B. Schichtleiter-Vertretung'
|
||||
className='w-full border border-gray-300 rounded-lg px-2 py-1.5 text-xs mt-0.5' />
|
||||
</div>
|
||||
</div>
|
||||
{assignmentError && <p className='text-xs text-red-600 mt-1'>{assignmentError}</p>}
|
||||
<button
|
||||
onClick={async () => {
|
||||
setAssignmentError('')
|
||||
setAssignmentSaving(true)
|
||||
try {
|
||||
const r = await api.post<SpecialAssignmentOut>(`/users/${editUser!.id}/special-assignments`, newAssignment)
|
||||
setAssignments(prev => [...prev, r])
|
||||
setNewAssignment({ date_from: '', date_to: '', factor: 1.5, mode: 'both' })
|
||||
} catch (e: any) {
|
||||
setAssignmentError(e?.detail || e?.message || 'Fehler beim Speichern')
|
||||
} finally {
|
||||
setAssignmentSaving(false)
|
||||
}
|
||||
}}
|
||||
disabled={assignmentSaving || !newAssignment.date_from || !newAssignment.date_to}
|
||||
className='mt-2 px-3 py-1.5 text-xs font-medium text-amber-700 border border-amber-300 rounded-lg hover:bg-amber-50 disabled:opacity-50'
|
||||
>
|
||||
{assignmentSaving ? 'Speichere…' : '+ Zeitraum hinzufügen'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end gap-2 pt-2'>
|
||||
<button onClick={() => setEditUser(null)} className='px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50'>Abbrechen</button>
|
||||
<button onClick={handleEditRole} disabled={editLoading} className='px-4 py-2 text-sm font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50'>
|
||||
|
||||
Reference in New Issue
Block a user