feat(export): Nachweis fuer andere Berater mit deren Projekten

Wer in der Buchung einen Kollegen ausgewaehlt hatte und dann exportierte,
bekam im Dialog die eigenen Kunden und Projekte zur Auswahl - fuer einen
Nachweis, der jemand anderem gehoert.

Der Dialog laedt die Projekte jetzt zu der Person, fuer die der Nachweis
entsteht, und laedt neu, wenn man die Person darin wechselt; die alte
Kunden- und Projektauswahl wird dabei verworfen, weil sie nicht mehr
passt. Bei "Alle Mitarbeiter" stehen alle sichtbaren Projekte zur Wahl.

Dafuer nimmt GET /projects ein ?user_id an. Wer fremde Projekte sehen
darf, entscheidet dieselbe Sicht wie bei den Zeiten: Mitarbeiter bekommen
403, Vorgesetzte nur ihr Team, Management alle. Die Sammelauswahl heisst
fuer Management "Alle Mitarbeiter" statt "Alle meine Mitarbeiter" - sie
sieht ohnehin jeden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-22 10:44:49 +02:00
co-authored by Claude Opus 5
parent ff71cb40e1
commit dd42fba1cb
3 changed files with 60 additions and 10 deletions
+42 -6
View File
@@ -1,9 +1,9 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Modal } from '../ui/Modal';
import { Button, Field, inputClass, Toggle } from '../ui/controls';
import { Icon, type IconName } from '../ui/Icon';
import { useAuth } from '../../lib/auth';
import { download, qs } from '../../lib/api';
import { download, projectsApi, qs } from '../../lib/api';
import { MONTHS } from '../../lib/format';
import { useToast } from '../Toast';
import { atLeast, type DirectoryUser, type Project } from '../../types';
@@ -33,6 +33,35 @@ export const ExportDialog: React.FC<{
const [busy, setBusy] = useState(false);
const isLead = atLeast(user, 'lead');
/*
* Kunde und Projekt gehoeren zu der Person, fuer die der Nachweis entsteht -
* sonst baut eine Fuehrungskraft den Export ihres Mitarbeiters mit den
* eigenen Projekten. Fuer sich selbst reicht die Liste, die die Buchung
* ohnehin geladen hat; fuer andere wird sie nachgeladen.
*/
const [fremdeProjekte, setFremdeProjekte] = useState<Project[] | null>(null);
const [laden, setLaden] = useState(false);
useEffect(() => {
if (userId === String(user!.id)) {
setFremdeProjekte(null);
return;
}
let abgebrochen = false;
setLaden(true);
// "Alle": die Projekte, die der Fragende ueberhaupt sehen darf.
const anfrage = userId === 'all' ? projectsApi.list() : projectsApi.list('mine', Number(userId));
anfrage
.then((list) => !abgebrochen && setFremdeProjekte(list))
.catch(() => !abgebrochen && setFremdeProjekte([]))
.finally(() => !abgebrochen && setLaden(false));
return () => {
abgebrochen = true;
};
}, [userId, user]);
const basis = fremdeProjekte ?? projects;
/*
* Zur Auswahl stehen nur laufende Projekte aktiver Kunden. Archivierte
* Projekte und stillgelegte Kunden wuerden die Liste mit Aufträgen fuellen,
@@ -40,12 +69,18 @@ export const ExportDialog: React.FC<{
* nicht verloren: ohne Filter exportiert der Nachweis alles, was die Person
* im Monat gebucht hat - und der Projektreport hat einen eigenen Export.
*/
const waehlbar = useMemo(() => projects.filter((p) => p.is_active && p.customer.is_active !== false), [projects]);
const waehlbar = useMemo(() => basis.filter((p) => p.is_active && p.customer.is_active !== false), [basis]);
const customers = useMemo(() => {
const map = new Map<number, string>();
waehlbar.forEach((p) => map.set(p.customer.id, p.customer.name));
return [...map.entries()].sort((a, b) => a[1].localeCompare(b[1], 'de'));
}, [waehlbar]);
// Wechselt die Person, passt die alte Auswahl nicht mehr.
useEffect(() => {
setCustomerId('');
setProjectId('');
}, [userId]);
const projectOptions = waehlbar.filter((p) => !customerId || p.customer_id === customerId);
// Monate: 18 zurueck, 2 voraus.
@@ -137,13 +172,14 @@ export const ExportDialog: React.FC<{
{t.is_external ? ' (extern)' : ''}
</option>
))}
<option value="all">Alle meine Mitarbeiter</option>
<option value="all">{atLeast(user, 'management') ? 'Alle Mitarbeiter' : 'Alle meine Mitarbeiter'}</option>
</select>
</Field>
)}
<Field label="Kunde">
<Field label="Kunde" hint={laden ? 'Lädt die Projekte dieser Person…' : undefined}>
<select
className={inputClass}
disabled={laden}
value={customerId}
onChange={(e) => {
setCustomerId(e.target.value ? Number(e.target.value) : '');
@@ -159,7 +195,7 @@ export const ExportDialog: React.FC<{
</select>
</Field>
<Field label="Projekt">
<select className={inputClass} value={projectId} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : '')}>
<select className={inputClass} disabled={laden} value={projectId} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : '')}>
<option value="">Alle Projekte</option>
{projectOptions.map((p) => (
<option key={p.id} value={p.id}>
+1 -1
View File
@@ -311,7 +311,7 @@ export type PotInput = Partial<
>;
export const projectsApi = {
list: (scope?: 'mine') => get<Project[]>(`/projects${qs({ scope })}`),
list: (scope?: 'mine', userId?: number) => get<Project[]>(`/projects${qs({ scope, user_id: userId })}`),
get: (id: number) => get<Project>(`/projects/${id}`),
create: (data: Record<string, unknown>) => send<Project>('POST', '/projects', data),
update: (id: number, data: Record<string, unknown>) => send<Project>('PUT', `/projects/${id}`, data),