Weiter
This commit is contained in:
1 parent
c2063fe1ed
commit
07ef8ef025
10 files changed
+1624
-93
No files matched your search
@@ -14,6 +14,7 @@ import { OrganizationSelect } from "@/components/OrganizationSelect";
|
||||
import { TodoPanel } from "@/components/TodoPanel";
|
||||
import {
|
||||
createSession,
|
||||
deleteProject,
|
||||
deleteSession,
|
||||
addFavoriteTicket,
|
||||
getDashboardPreference,
|
||||
@@ -53,6 +54,7 @@ type TicketDetailPageProps = {
|
||||
type TicketSessionFormState = {
|
||||
organizationId: string;
|
||||
organizationName: string | null;
|
||||
projectTeamspaceEntryId: string;
|
||||
activity: string;
|
||||
workType: WorkType;
|
||||
day: string;
|
||||
@@ -82,6 +84,7 @@ function emptyTicketSessionForm(): TicketSessionFormState {
|
||||
return {
|
||||
organizationId: "",
|
||||
organizationName: null,
|
||||
projectTeamspaceEntryId: "",
|
||||
activity: "",
|
||||
workType: "support",
|
||||
day: currentDay(),
|
||||
@@ -281,6 +284,7 @@ function parseProjectTeamspaceEntryForms(entries: ProjectTeamspaceEntryForm[]) {
|
||||
}
|
||||
|
||||
return {
|
||||
id: /^\d+$/.test(entry.clientId) ? entry.clientId : null,
|
||||
teamspaceProject: entry.teamspaceProject.trim() || null,
|
||||
isDefault: entry.isDefault,
|
||||
hourBudgetMinutes,
|
||||
@@ -293,6 +297,7 @@ function parseProjectTeamspaceEntryForms(entries: ProjectTeamspaceEntryForm[]) {
|
||||
}
|
||||
|
||||
return parsedEntries as Array<{
|
||||
id?: string | null;
|
||||
teamspaceProject: string | null;
|
||||
isDefault: boolean;
|
||||
hourBudgetMinutes: number | null;
|
||||
@@ -379,10 +384,12 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
const [editOrganizationName, setEditOrganizationName] = useState<string | null>(null);
|
||||
const [editWorkType, setEditWorkType] = useState<WorkType>("support");
|
||||
const [editingSession, setEditingSession] = useState<SessionEntry | null>(null);
|
||||
const [pendingProjectDelete, setPendingProjectDelete] = useState<{ projectId: string } | null>(null);
|
||||
const [editSessionOrganizationId, setEditSessionOrganizationId] = useState("");
|
||||
const [editSessionOrganizationName, setEditSessionOrganizationName] = useState<string | null>(null);
|
||||
const [editSessionActivity, setEditSessionActivity] = useState("");
|
||||
const [editSessionWorkType, setEditSessionWorkType] = useState<WorkType>("support");
|
||||
const [editSessionProjectTeamspaceEntryId, setEditSessionProjectTeamspaceEntryId] = useState("");
|
||||
const [editSessionDay, setEditSessionDay] = useState("");
|
||||
const [editSessionStartTime, setEditSessionStartTime] = useState("");
|
||||
const [editSessionEndTime, setEditSessionEndTime] = useState("");
|
||||
@@ -396,6 +403,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
const [globalSourceFilter, setGlobalSourceFilter] = useState<"all" | "manual" | "recurring">("all");
|
||||
const [editingProject, setEditingProject] = useState(false);
|
||||
const [savingProject, setSavingProject] = useState(false);
|
||||
const [deletingEmptyProject, setDeletingEmptyProject] = useState(false);
|
||||
const [savingProjectStatus, setSavingProjectStatus] = useState(false);
|
||||
const [projectForm, setProjectForm] = useState<ProjectEditFormState>({
|
||||
title: "",
|
||||
@@ -463,13 +471,15 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
setEditOrganizationId(data.ticket.organization_id ?? "");
|
||||
setEditOrganizationName(data.ticket.organization_name ?? null);
|
||||
setEditWorkType(data.ticket.work_type ?? "support");
|
||||
const loadedProjectEntries = data.project ? projectTeamspaceEntries(data.project) : [];
|
||||
setTicketSession((current) => ({
|
||||
...current,
|
||||
organizationId: data.ticket.organization_id ?? current.organizationId,
|
||||
organizationName: data.ticket.organization_name ?? current.organizationName,
|
||||
projectTeamspaceEntryId: loadedProjectEntries.length === 1 ? loadedProjectEntries[0].id : current.projectTeamspaceEntryId,
|
||||
workType: data.ticket.work_type ?? current.workType
|
||||
}));
|
||||
}, [data?.ticket]);
|
||||
}, [data?.ticket, data?.project]);
|
||||
|
||||
useEffect(() => {
|
||||
setProjectDescriptionExpanded(false);
|
||||
@@ -481,9 +491,12 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
const projectDescription = project ? combinedProjectDescription(project) : "";
|
||||
const projectEntries = useMemo(() => (project ? projectTeamspaceEntries(project) : []), [project]);
|
||||
const hasMultipleProjectTeamspaceEntries = projectEntries.length > 1;
|
||||
const requiresProjectTeamspaceEntry = Boolean(project && hasMultipleProjectTeamspaceEntries);
|
||||
const projectConsumptionMinutes = project?.consumption_minutes ?? 0;
|
||||
const projectBudgetMinutes = project?.hour_budget_minutes ?? null;
|
||||
const projectConsumptionUsagePercent = project ? projectConsumptionPercent(projectConsumptionMinutes, projectBudgetMinutes) : null;
|
||||
const canEditProject = project?.can_edit !== false;
|
||||
const canEditTicketData = !project || canEditProject;
|
||||
const isProject = Boolean(project);
|
||||
const isGlobalTicket = periodType === "all";
|
||||
const isPeriodClosed = Boolean(data?.closed);
|
||||
@@ -684,6 +697,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canEditTicketData) {
|
||||
toast.error("Projekt ist geteilt", {
|
||||
description: "Nur der Zammad-Owner kann Ticket- und Projektdaten ändern."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editOrganizationId) {
|
||||
toast.error("Organisation wählen");
|
||||
return;
|
||||
@@ -728,6 +748,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.can_edit === false) {
|
||||
toast.error("Projekt ist geteilt", {
|
||||
description: "Nur der Zammad-Owner kann die Projektdaten bearbeiten."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setProjectForm(projectEditForm(project));
|
||||
setEditingProject(true);
|
||||
}
|
||||
@@ -739,6 +766,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.can_edit === false) {
|
||||
toast.error("Projekt ist geteilt", {
|
||||
description: "Nur der Zammad-Owner kann die Projektdaten bearbeiten."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!projectForm.organizationId) {
|
||||
toast.error("Organisation wählen");
|
||||
return;
|
||||
@@ -787,6 +821,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.can_edit === false) {
|
||||
toast.error("Projekt ist geteilt", {
|
||||
description: "Nur der Zammad-Owner kann den Projektstatus ändern."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingProjectStatus(true);
|
||||
try {
|
||||
await updateProjectStatus(project.id, closed);
|
||||
@@ -823,6 +864,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
return;
|
||||
}
|
||||
|
||||
if (effectiveWorkType === "consulting" && requiresProjectTeamspaceEntry && !ticketSession.projectTeamspaceEntryId) {
|
||||
toast.error("Teamspace-Eintrag wählen", {
|
||||
description: "Dieses Projekt hat mehrere Teamspace-Einträge."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const startedAt = new Date(`${ticketSession.day}T${ticketSession.startTime}:00`);
|
||||
const endedAt = new Date(`${ticketSession.day}T${ticketSession.endTime}:00`);
|
||||
|
||||
@@ -836,6 +884,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
return;
|
||||
}
|
||||
|
||||
if (editSessionWorkType === "consulting" && requiresProjectTeamspaceEntry && !editSessionProjectTeamspaceEntryId) {
|
||||
toast.error("Teamspace-Eintrag wählen", {
|
||||
description: "Dieses Projekt hat mehrere Teamspace-Einträge."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const durationSeconds = Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000);
|
||||
|
||||
setSavingTicketSession(true);
|
||||
@@ -848,7 +903,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
workType: effectiveWorkType,
|
||||
startedAt: startedAt.toISOString(),
|
||||
endedAt: endedAt.toISOString(),
|
||||
durationSeconds
|
||||
durationSeconds,
|
||||
projectTeamspaceEntryId: effectiveWorkType === "consulting" && requiresProjectTeamspaceEntry ? ticketSession.projectTeamspaceEntryId : null
|
||||
});
|
||||
toast.success("Session nachgetragen", {
|
||||
description: "Der Eintrag wurde diesem Ticket zugeordnet. Betroffene Abschlüsse wurden wieder geöffnet."
|
||||
@@ -857,6 +913,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
...emptyTicketSessionForm(),
|
||||
organizationId: ticket.organization_id ?? "",
|
||||
organizationName: ticket.organization_name ?? null,
|
||||
projectTeamspaceEntryId: "",
|
||||
workType: ticket.work_type ?? "support",
|
||||
day: ticketSession.day
|
||||
});
|
||||
@@ -876,6 +933,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
setEditSessionOrganizationName(session.organization_name ?? null);
|
||||
setEditSessionActivity(session.activity);
|
||||
setEditSessionWorkType(session.work_type);
|
||||
setEditSessionProjectTeamspaceEntryId(session.project_teamspace_entry_id ?? "");
|
||||
setEditSessionDay(toDateLocalValue(session.started_at));
|
||||
setEditSessionStartTime(toTimeLocalValue(session.started_at));
|
||||
setEditSessionEndTime(toTimeLocalValue(session.ended_at));
|
||||
@@ -925,6 +983,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
activity: editSessionActivity,
|
||||
workType: editSessionWorkType,
|
||||
removeProjectData,
|
||||
projectTeamspaceEntryId: editSessionWorkType === "consulting" && requiresProjectTeamspaceEntry ? editSessionProjectTeamspaceEntryId : null,
|
||||
startedAt: startedAt.toISOString(),
|
||||
endedAt: endedAt.toISOString()
|
||||
});
|
||||
@@ -951,6 +1010,14 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
try {
|
||||
const result = await deleteSession(session.id);
|
||||
|
||||
if (result.deleted.projectDeleteAvailable && result.deleted.project_id) {
|
||||
setPendingProjectDelete({ projectId: result.deleted.project_id });
|
||||
toast.success("Session gelöscht", {
|
||||
description: "Für dieses Projekt sind keine Sessions mehr vorhanden."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.deleted.ticketDeleted || result.deleted.userTicketEmpty) {
|
||||
toast.success("Letzte Session gelöscht", {
|
||||
description: result.deleted.ticketDeleted ? "Das leere Ticket wurde entfernt." : "Für dich gibt es zu diesem Ticket keine Einträge mehr."
|
||||
@@ -972,6 +1039,32 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
}
|
||||
}
|
||||
|
||||
async function keepEmptyProject() {
|
||||
setPendingProjectDelete(null);
|
||||
toast.success("Projekt behalten");
|
||||
await load();
|
||||
}
|
||||
|
||||
async function deleteEmptyProject() {
|
||||
if (!pendingProjectDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingEmptyProject(true);
|
||||
try {
|
||||
await deleteProject(pendingProjectDelete.projectId);
|
||||
toast.success("Projekt gelöscht");
|
||||
setPendingProjectDelete(null);
|
||||
onNavigate(returnPath);
|
||||
} catch (error) {
|
||||
toast.error("Projekt konnte nicht gelöscht werden", {
|
||||
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||
});
|
||||
} finally {
|
||||
setDeletingEmptyProject(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTicketFavorite() {
|
||||
if (!ticket?.ticket_number) {
|
||||
return;
|
||||
@@ -1101,7 +1194,11 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle>Ticketdaten</CardTitle>
|
||||
<CardDescription>Änderungen an Organisation und Art werden auf alle vorhandenen Sessions dieses Tickets übernommen.</CardDescription>
|
||||
<CardDescription>
|
||||
{canEditTicketData
|
||||
? "Änderungen an Organisation und Art werden auf alle vorhandenen Sessions dieses Tickets übernommen."
|
||||
: "Dieses Projekt ist geteilt. Nur der Zammad-Owner kann Ticket- und Projektdaten ändern."}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<HelpLink anchor="ticketdaten" label="Hilfe zu Ticketdaten" />
|
||||
</div>
|
||||
@@ -1110,11 +1207,11 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
<form className="grid gap-3 lg:grid-cols-[180px_minmax(220px,1fr)_minmax(220px,1fr)_160px_auto] lg:items-end" onSubmit={saveTicketData}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="edit-ticket-number">Ticketnummer</label>
|
||||
<Input id="edit-ticket-number" value={editTicketNumber} onChange={(event) => setEditTicketNumber(event.currentTarget.value)} required />
|
||||
<Input id="edit-ticket-number" value={editTicketNumber} disabled={!canEditTicketData} onChange={(event) => setEditTicketNumber(event.currentTarget.value)} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="edit-ticket-title">Titel</label>
|
||||
<Input id="edit-ticket-title" value={editTicketTitle} onChange={(event) => setEditTicketTitle(event.currentTarget.value)} placeholder="optional" />
|
||||
<Input id="edit-ticket-title" value={editTicketTitle} disabled={!canEditTicketData} onChange={(event) => setEditTicketTitle(event.currentTarget.value)} placeholder="optional" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="edit-ticket-organization">Organisation</label>
|
||||
@@ -1124,6 +1221,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
<OrganizationSelect
|
||||
value={editOrganizationId}
|
||||
selectedName={editOrganizationName}
|
||||
disabled={!canEditTicketData}
|
||||
onChange={(organization) => {
|
||||
setEditOrganizationId(organization.id);
|
||||
setEditOrganizationName(organization.name);
|
||||
@@ -1137,13 +1235,14 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
id="edit-ticket-work-type"
|
||||
className="h-8 w-full rounded-lg border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
value={editWorkType}
|
||||
disabled={!canEditTicketData}
|
||||
onChange={(event) => setEditWorkType(event.currentTarget.value as WorkType)}
|
||||
>
|
||||
<option value="support">Support</option>
|
||||
<option value="consulting">Consulting</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button type="submit" disabled={savingTicket}>
|
||||
<Button type="submit" disabled={!canEditTicketData || savingTicket}>
|
||||
<Save className="size-4" />
|
||||
{savingTicket ? "Speichert..." : "Speichern"}
|
||||
</Button>
|
||||
@@ -1161,11 +1260,15 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
<FolderKanban className="size-5 text-muted-foreground" />
|
||||
Projekt
|
||||
{project.closed_at ? <Badge variant="secondary">geschlossen</Badge> : <Badge variant="success">offen</Badge>}
|
||||
{!canEditProject ? <Badge variant="outline">geteilt</Badge> : null}
|
||||
{(project.unassigned_teamspace_session_count ?? 0) > 0 ? <Badge variant="warning">{project.unassigned_teamspace_session_count} ohne TS</Badge> : null}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{hasMultipleProjectTeamspaceEntries
|
||||
? `${projectEntries.length} Teamspace-Einträge`
|
||||
: project.teamspace_project || "Kein Teamspace-Projekt hinterlegt"}
|
||||
{!canEditProject && project.owner_display_name
|
||||
? `Eigentümer: ${project.owner_display_name}`
|
||||
: hasMultipleProjectTeamspaceEntries
|
||||
? `${projectEntries.length} Teamspace-Einträge`
|
||||
: project.teamspace_project || "Kein Teamspace-Projekt hinterlegt"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
@@ -1173,13 +1276,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={savingProjectStatus}
|
||||
disabled={!canEditProject || savingProjectStatus}
|
||||
onClick={() => void setProjectClosed(!project.closed_at)}
|
||||
>
|
||||
{project.closed_at ? <RotateCcw className="size-4" /> : <CheckCircle2 className="size-4" />}
|
||||
{project.closed_at ? "Öffnen" : "Schließen"}
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="secondary" onClick={openProjectEditor}>
|
||||
<Button type="button" size="sm" variant="secondary" disabled={!canEditProject} onClick={openProjectEditor}>
|
||||
<Pencil className="size-4" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
@@ -1188,6 +1291,14 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 px-4 pb-4">
|
||||
{(project.unassigned_teamspace_session_count ?? 0) > 0 ? (
|
||||
<Alert variant="warning">
|
||||
<div className="font-medium">Nicht zugeordnete Sessions</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{project.unassigned_teamspace_session_count} Session(s) haben keinen Teamspace-Eintrag. Bitte über die Session-Bearbeitung neu zuordnen.
|
||||
</div>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="flex flex-wrap gap-2 text-sm">
|
||||
<Badge variant="secondary">Zeitraum {project.starts_on || "offen"} - {project.ends_on || "offen"}</Badge>
|
||||
<Badge variant="secondary">Kontingent {projectBudgetMinutes ? formatMinutes(projectBudgetMinutes) : "offen"}</Badge>
|
||||
@@ -1497,6 +1608,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="w-fit">{formatMinutes(session.rounded_minutes)}</Badge>
|
||||
{session.recurring_billing_id ? <Badge variant="secondary">fix</Badge> : null}
|
||||
{requiresProjectTeamspaceEntry ? (
|
||||
session.project_teamspace_entry_id ? (
|
||||
<Badge variant="info">{session.project_teamspace_project || "Teamspace"}</Badge>
|
||||
) : (
|
||||
<Badge variant="warning">ohne Teamspace</Badge>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<BillingButtons
|
||||
session={session}
|
||||
@@ -1604,6 +1722,26 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requiresProjectTeamspaceEntry ? (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="ticket-session-teamspace-entry">Teamspace-Eintrag</label>
|
||||
<select
|
||||
id="ticket-session-teamspace-entry"
|
||||
className="h-9 w-full rounded-lg border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
value={ticketSession.projectTeamspaceEntryId}
|
||||
onChange={(event) => setTicketSession({ ...ticketSession, projectTeamspaceEntryId: event.currentTarget.value })}
|
||||
required
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{projectEntries.map((entry) => (
|
||||
<option key={entry.id} value={entry.id}>
|
||||
{entry.teamspace_project || (entry.is_default ? "Standard" : "Ohne Teamspace-ID")}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-[150px_120px_120px_minmax(220px,1fr)_auto] lg:items-end">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="ticket-session-day">
|
||||
@@ -1670,6 +1808,24 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<Dialog open={Boolean(pendingProjectDelete)} onOpenChange={(open) => !open && void keepEmptyProject()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Keine Sessions mehr vorhanden</DialogTitle>
|
||||
<DialogDescription>Projekt löschen?</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<Button type="button" variant="secondary" disabled={deletingEmptyProject} onClick={() => void keepEmptyProject()}>
|
||||
Projekt behalten
|
||||
</Button>
|
||||
<Button type="button" variant="destructive" disabled={deletingEmptyProject} onClick={() => void deleteEmptyProject()}>
|
||||
<Trash2 className="size-4" />
|
||||
{deletingEmptyProject ? "Löscht..." : "Projekt löschen"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={Boolean(editingSession)} onOpenChange={(open) => !open && setEditingSession(null)}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] overflow-y-auto p-5 sm:max-w-2xl">
|
||||
<DialogHeader className="pr-8">
|
||||
@@ -1747,6 +1903,25 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
|
||||
<option value="consulting">Consulting</option>
|
||||
</select>
|
||||
</div>
|
||||
{editSessionWorkType === "consulting" && requiresProjectTeamspaceEntry ? (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="session-teamspace-entry">Teamspace-Eintrag</label>
|
||||
<select
|
||||
id="session-teamspace-entry"
|
||||
className="h-8 w-full rounded-lg border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
value={editSessionProjectTeamspaceEntryId}
|
||||
onChange={(event) => setEditSessionProjectTeamspaceEntryId(event.currentTarget.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{projectEntries.map((entry) => (
|
||||
<option key={entry.id} value={entry.id}>
|
||||
{entry.teamspace_project || (entry.is_default ? "Standard" : "Ohne Teamspace-ID")}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={savingSessionDetails}>
|
||||
<Save className="size-4" />
|
||||
|
||||
Reference in new issue
Block a user