v1.0.11-rc1

This commit is contained in:
michael committed 2026-10-02 08:50:10 +02:00
1 parent d5952d82bb
commit e1456ed4ca
8 files changed
+1315 -501

No files matched your search

+1 -1
View File
@@ -1531,7 +1531,7 @@ export function App() {
<SidebarMenuItem>
<SidebarMenuButton tooltip="Worklog">
<FolderGit />
<span className="text-xs text-muted-foreground">v1.0.10-rc1</span>
<span className="text-xs text-muted-foreground">v1.0.11-rc1</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
+16
View File
@@ -140,6 +140,11 @@ export type TicketMeta = {
zammad_priority_id?: string | null;
zammad_state_id?: string | null;
zammad_owner_id?: string | null;
zammad_owner_user_id?: string | null;
zammad_owner_display_name?: string | null;
zammad_owner_username?: string | null;
is_foreign_zammad_owner?: boolean;
can_edit_ticket_data?: boolean;
project_id?: string | null;
is_project?: boolean;
project_name?: string | null;
@@ -184,6 +189,9 @@ export type ProjectSummary = {
work_type: WorkType | null;
zammad_priority_id?: string | null;
zammad_state_id?: string | null;
zammad_ticket_id?: string | null;
zammad_owner_id?: string | null;
ticket_zammad_owner_id?: string | null;
zammad_pending_time?: string | null;
zammad_merged_from_ticket_id?: string | null;
zammad_merged_from_ticket_number?: string | null;
@@ -376,8 +384,16 @@ export type TicketPeriod = {
organization_name: string | null;
customer_name: string | null;
work_type: WorkType | null;
zammad_ticket_id?: string | null;
zammad_group_id?: string | null;
zammad_priority_id?: string | null;
zammad_state_id?: string | null;
zammad_owner_id?: string | null;
zammad_owner_user_id?: string | null;
zammad_owner_display_name?: string | null;
zammad_owner_username?: string | null;
is_foreign_zammad_owner?: boolean;
can_edit_ticket_data?: boolean;
closed_at: string | null;
requires_closure: boolean;
recurring_session_count: number;
+2 -2
View File
@@ -1664,7 +1664,7 @@ export function ProjectsPage({ onNavigate, onStartTimer }: ProjectsPageProps) {
const remaining = projectRemaining(project, viewSettings.standardTeamspaceOnly);
const usedPercent = projectConsumptionPercent(project, viewSettings.standardTeamspaceOnly);
const showBudgetedTeamspaceEntries = !viewSettings.standardTeamspaceOnly && budgetedTeamspaceEntries.length > 1;
const canEditProject = project.can_edit !== false;
const canEditProject = project.can_edit === true;
return (
<div key={project.id} className="border-b last:border-b-0">
@@ -1797,7 +1797,7 @@ export function ProjectsPage({ onNavigate, onStartTimer }: ProjectsPageProps) {
variant="outline"
size="sm"
className="h-7 shrink-0 px-2 text-xs"
disabled={savingProjectStatusId === project.id || project.can_edit === false}
disabled={savingProjectStatusId === project.id || !canEditProject}
onClick={() => void acknowledgeProjectReviewCheck(project)}
>
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" />
+46 -12
View File
@@ -912,8 +912,17 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
const projectBudgetMinutes = project?.hour_budget_minutes ?? null;
const projectConsumptionUsagePercent = project ? projectConsumptionPercent(projectConsumptionMinutes, projectBudgetMinutes) : null;
const projectRemainingMinutes = projectBudgetMinutes && projectBudgetMinutes > 0 ? projectBudgetMinutes - projectConsumptionMinutes : null;
const canEditProject = project?.can_edit !== false;
const canEditTicketData = !project || canEditProject;
const canEditProject = project?.can_edit === true;
const ticketManagedByZammad = Boolean(ticket?.zammad_ticket_id);
const canEditTicketData = ticket?.can_edit_ticket_data === true;
const zammadOwnerLabel = ticket?.zammad_owner_display_name
|| ticket?.zammad_owner_username
|| (ticket?.zammad_owner_id ? `Zammad-Owner ${ticket.zammad_owner_id}` : null);
const projectOwnerLabel = project?.owner_display_name
|| project?.owner_username
|| (project?.zammad_owner_id ? `Zammad-Owner ${project.zammad_owner_id}` : null)
|| zammadOwnerLabel;
const currentZammadUserLabel = currentUser.zammad_user_id ? `Zammad-User ${currentUser.zammad_user_id}` : "dein Zammad-User";
const isProject = Boolean(project);
const isGlobalTicket = periodType === "all";
const isPeriodClosed = Boolean(data?.closed);
@@ -1045,6 +1054,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
const periodTeamspaceMinutes = (data?.dayBillings ?? []).reduce((sum, billing) => sum + Number(billing.billed_minutes ?? 0), 0);
const ticketDataDirty = Boolean(
ticket &&
canEditTicketData &&
(editTicketNumber !== ticket.ticket_number ||
editTicketTitle !== (ticket.title ?? "") ||
editOrganizationId !== (ticket.organization_id ?? "") ||
@@ -1284,8 +1294,10 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
}
if (!canEditTicketData) {
toast.error("Projekt ist geteilt", {
description: "Nur der Zammad-Owner kann Ticket- und Projektdaten ändern."
toast.error(ticketManagedByZammad ? "Ticketdaten kommen aus Zammad" : "Ticket gehört einem anderen Owner", {
description: ticketManagedByZammad
? "Nummer, Art, Titel und Organisation werden durch den Zammad-Sync verwaltet."
: "Nur der Owner kann Ticketdaten ändern."
});
return;
}
@@ -1334,7 +1346,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
return;
}
if (project.can_edit === false) {
if (!canEditProject) {
toast.error("Projekt ist geteilt", {
description: "Nur der Zammad-Owner kann die Projektdaten bearbeiten."
});
@@ -1352,7 +1364,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
return;
}
if (project.can_edit === false) {
if (!canEditProject) {
toast.error("Projekt ist geteilt", {
description: "Nur der Zammad-Owner kann die Projektdaten bearbeiten."
});
@@ -1407,7 +1419,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
return;
}
if (project.can_edit === false) {
if (!canEditProject) {
toast.error("Projekt ist geteilt", {
description: "Nur der Zammad-Owner kann den Projektstatus ändern."
});
@@ -1433,7 +1445,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
return;
}
if (project.can_edit === false) {
if (!canEditProject) {
toast.error("Projekt ist geteilt", {
description: "Nur der Zammad-Owner kann die Prüfung bestätigen."
});
@@ -1811,6 +1823,26 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
</Alert>
) : null}
{ticket?.is_foreign_zammad_owner ? (
<Alert className="py-3 text-sm">
<AlertCircle className="size-4" />
<AlertTitle>Anderer Zammad-Owner</AlertTitle>
<AlertDescription>
Dieses Ticket gehört in Zammad {zammadOwnerLabel ? `${zammadOwnerLabel}` : "einem anderen Owner"}; du bist {currentZammadUserLabel}. Du kannst deine Sessions sehen, Projekt- und Ticketdaten sind für dich geschützt.
</AlertDescription>
</Alert>
) : null}
{ticketManagedByZammad ? (
<Alert className="py-3 text-sm">
<AlertCircle className="size-4" />
<AlertTitle>Ticketdaten werden von Zammad verwaltet</AlertTitle>
<AlertDescription>
Nummer, Art, Titel und Organisation wurden durch den Zammad-Sync übernommen und können in TicketTracker nicht manuell geändert werden.
</AlertDescription>
</Alert>
) : null}
<div className="grid gap-4">
<Card>
<CardHeader className="p-4 pb-2">
@@ -1820,7 +1852,9 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
<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."}
: ticketManagedByZammad
? "Nummer, Art, Titel und Organisation kommen aus Zammad und sind lokal gesperrt."
: "Nur der Owner kann Ticketdaten ändern."}
</CardDescription>
</div>
<HelpLink anchor="ticketdaten" label="Hilfe zu Ticketdaten" />
@@ -1891,8 +1925,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
{(project.unassigned_teamspace_session_count ?? 0) > 0 ? <Badge variant="warning">{project.unassigned_teamspace_session_count} ohne TS</Badge> : null}
</CardTitle>
<CardDescription>
{!canEditProject && project.owner_display_name
? `Eigentümer: ${project.owner_display_name}`
{!canEditProject && projectOwnerLabel
? `Eigentümer: ${projectOwnerLabel}`
: showBudgetedProjectEntries
? `${budgetedProjectEntries.length} budgetierte Teamspace-Einträge`
: budgetedProjectEntries[0] ? teamspaceEntryDisplayLabel(budgetedProjectEntries[0]) : project.teamspace_project || "Kein Teamspace-Projekt hinterlegt"}
@@ -1944,7 +1978,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
variant="outline"
size="sm"
className="shrink-0"
disabled={savingProjectStatus || project.can_edit === false}
disabled={savingProjectStatus || !canEditProject}
onClick={() => void acknowledgeProjectReviewCheck()}
>
<CheckCircle2 className="mr-2 h-4 w-4" />
+20 -1
View File
@@ -659,6 +659,25 @@ function addCachedChange(changes: ZammadSyncChange[], field: string, from: unkno
}
}
const preservedExistingPreviewChangeFields = new Set([
"Fremdes Ticket",
"Fremder Zammad-Owner",
"Owner",
"Ticket-Zuordnung",
"Hinweis",
"Ticketnummer",
"TicketTracker-Zusammenführung"
]);
function preserveExistingPreviewChanges(item: ZammadSyncItem, changes: ZammadSyncChange[], fields: ZammadSyncExistingFields) {
const existingFields = new Set(changes.map((change) => change.field));
return [
...item.changes.filter((change) => preservedExistingPreviewChangeFields.has(change.field) && !existingFields.has(change.field) && changeAllowedByFields(item, change, fields)),
...changes
];
}
function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyncExistingFields) {
if (!item.zammad) {
return item.changes.filter((change) => changeAllowedByFields(item, change, fields));
@@ -756,7 +775,7 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
}
}
return changes;
return preserveExistingPreviewChanges(item, changes, fields);
}
function rebuildZammadSyncResultFromItems(mode: ZammadSyncResult["mode"], items: ZammadSyncItem[], checked = items.length): ZammadSyncResult {