This commit is contained in:
michael committed 2026-09-15 09:51:30 +00:00
1 parent b7f7d7e793
commit c01d747352
14 files changed
+987 -294

No files matched your search

+21 -4
View File
@@ -5,6 +5,7 @@ import { Alert } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -32,6 +33,7 @@ import {
import { currentDay, currentMonth, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format";
import { ticketHasRecurringBilling } from "../recurring-billings";
import type { BillingStatus, PeriodType, ProjectSummary, ProjectTeamspaceEntry, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
import { ZammadPriorityBadge, ZammadStateBadge } from "../zammad-meta";
import { cn } from "@/lib/utils";
type TicketDetailPageProps = {
@@ -77,6 +79,7 @@ type ProjectTeamspaceEntryForm = {
clientId: string;
teamspaceProject: string;
hourBudget: string;
budgetSyncDisabled: boolean;
manualConsumption: string;
isDefault: boolean;
};
@@ -126,6 +129,7 @@ function projectEditForm(project: ProjectSummary): ProjectEditFormState {
clientId: entry.id || projectEntryId(),
teamspaceProject: entry.teamspace_project ?? "",
hourBudget: formatHoursInput(entry.hour_budget_minutes),
budgetSyncDisabled: entry.budget_sync_disabled ?? false,
manualConsumption: formatHoursInput(entry.manual_consumption_minutes),
isDefault: entry.is_default || index === 0
}));
@@ -262,6 +266,7 @@ function normalizeProjectTeamspaceEntryForms(entries: ProjectTeamspaceEntryForm[
clientId: projectEntryId(),
teamspaceProject: "",
hourBudget: "",
budgetSyncDisabled: false,
manualConsumption: "",
isDefault: true
}
@@ -289,6 +294,7 @@ function parseProjectTeamspaceEntryForms(entries: ProjectTeamspaceEntryForm[]) {
teamspaceProject: entry.teamspaceProject.trim() || null,
isDefault: entry.isDefault,
hourBudgetMinutes,
budgetSyncDisabled: entry.budgetSyncDisabled,
manualConsumptionMinutes: manualConsumptionMinutes ?? 0
};
});
@@ -302,6 +308,7 @@ function parseProjectTeamspaceEntryForms(entries: ProjectTeamspaceEntryForm[]) {
teamspaceProject: string | null;
isDefault: boolean;
hourBudgetMinutes: number | null;
budgetSyncDisabled: boolean;
manualConsumptionMinutes: number;
}>;
}
@@ -327,13 +334,13 @@ function ProjectTeamspaceEntriesEditor({
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<label className="text-sm font-medium">Teamspace-Einträge</label>
<Button type="button" size="sm" variant="secondary" onClick={() => onChange([...normalizedEntries, { clientId: projectEntryId(), teamspaceProject: "", hourBudget: "", manualConsumption: "", isDefault: false }])}>
<Button type="button" size="sm" variant="secondary" onClick={() => onChange([...normalizedEntries, { clientId: projectEntryId(), teamspaceProject: "", hourBudget: "", budgetSyncDisabled: false, manualConsumption: "", isDefault: false }])}>
<Plus className="size-3.5" />
Eintrag
</Button>
</div>
{normalizedEntries.map((entry, index) => (
<div key={entry.clientId} className="grid gap-2 rounded-lg border bg-muted/10 p-3 lg:grid-cols-[minmax(180px,1fr)_120px_150px_120px_36px] lg:items-end">
<div key={entry.clientId} className="grid gap-2 rounded-lg border bg-muted/10 p-3 lg:grid-cols-[minmax(180px,1fr)_120px_150px_130px_120px_36px] lg:items-end">
<div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground" htmlFor={`project-ticket-teamspace-${entry.clientId}`}>
Teamspace-Projekt {entry.isDefault ? "(Standard)" : ""}
@@ -346,8 +353,12 @@ function ProjectTeamspaceEntriesEditor({
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground" htmlFor={`project-ticket-budget-${entry.clientId}`}>Budget h</label>
<Input id={`project-ticket-budget-${entry.clientId}`} inputMode="decimal" value={entry.hourBudget} onChange={(event) => updateEntry(entry.clientId, { hourBudget: event.currentTarget.value })} />
<Input id={`project-ticket-budget-${entry.clientId}`} inputMode="decimal" value={entry.hourBudget} onChange={(event) => updateEntry(entry.clientId, { hourBudget: event.currentTarget.value, budgetSyncDisabled: true })} />
</div>
<label className="flex min-h-9 items-center gap-2 rounded-md border bg-background px-3 text-xs">
<Checkbox checked={entry.budgetSyncDisabled} onCheckedChange={(checked) => updateEntry(entry.clientId, { budgetSyncDisabled: checked === true })} />
<span>Budget schützen</span>
</label>
<div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground" htmlFor={`project-ticket-manual-${entry.clientId}`}>Manueller Verbrauch h</label>
<Input id={`project-ticket-manual-${entry.clientId}`} inputMode="decimal" value={entry.manualConsumption} onChange={(event) => updateEntry(entry.clientId, { manualConsumption: event.currentTarget.value })} />
@@ -418,6 +429,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
clientId: projectEntryId(),
teamspaceProject: "",
hourBudget: "",
budgetSyncDisabled: false,
manualConsumption: "",
isDefault: true
}
@@ -1108,6 +1120,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
<h2 className="text-xl font-semibold tracking-normal sm:text-2xl">{ticket?.title || ticket?.ticket_number || "Ticket"}</h2>
{ticket?.ticket_number ? <CopyTicketButton ticketNumber={ticket.ticket_number} /> : null}
{isProject ? <Badge variant="outline">Projekt</Badge> : null}
<ZammadStateBadge stateId={ticket?.zammad_state_id} />
<ZammadPriorityBadge priorityId={ticket?.zammad_priority_id} />
{(periodType === "month" || (isGlobalTicket && globalMonthFilter !== "all")) && data?.closed ? <Badge variant="outline">Monat geschlossen</Badge> : null}
</div>
<p className="text-sm text-muted-foreground">
@@ -1260,7 +1274,9 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
<CardTitle className="flex items-center gap-2">
<FolderKanban className="size-5 text-muted-foreground" />
Projekt
{project.closed_at ? <Badge variant="secondary">geschlossen</Badge> : <Badge variant="success">offen</Badge>}
{project.closed_at ? <Badge variant="secondary">Geschlossen</Badge> : <Badge variant="success">Offen</Badge>}
<ZammadStateBadge stateId={project.zammad_state_id} />
<ZammadPriorityBadge priorityId={project.zammad_priority_id} />
{!canEditProject ? <Badge variant="outline">geteilt</Badge> : null}
{project.close_review_required ? <Badge variant="warning">TS/TT prüfen</Badge> : null}
{(project.unassigned_teamspace_session_count ?? 0) > 0 ? <Badge variant="warning">{project.unassigned_teamspace_session_count} ohne TS</Badge> : null}
@@ -1341,6 +1357,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onO
<span className="truncate text-muted-foreground">
{label}
{entry.is_default ? " · Standard" : ""}
{entry.budget_sync_disabled ? " · Budget geschützt" : ""}
</span>
<span className="shrink-0 font-medium">
{formatMinutes(consumption)} / {entry.hour_budget_minutes ? formatMinutes(entry.hour_budget_minutes) : "offen"}