v1.0.11-rc1
This commit is contained in:
1 parent
d5952d82bb
commit
e1456ed4ca
8 files changed
+1250
-436
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
PP_PASSWORD='$ouzs4D$@RAi9'
|
PP_PASSWORD='$ouzs4D$@RAi9'
|
||||||
DATABASE_URL=postgresql://tickettracker:${PP_PASSWORD}@172.22.0.3:5432/tickettracker_dev
|
DATABASE_URL=postgresql://tickettracker:${PP_PASSWORD}@172.22.0.4:5432/tickettracker_dev
|
||||||
APP_PORT=3911
|
APP_PORT=3911
|
||||||
TICKETTRACKER_IMAGE=git.d-razz.de/michael/tickettracker:latest
|
TICKETTRACKER_IMAGE=git.d-razz.de/michael/tickettracker:latest
|
||||||
COOKIE_SECURE=false
|
COOKIE_SECURE=false
|
||||||
|
|||||||
+1061
-416
File diff suppressed because it is too large.
Load diff
+103
-3
@@ -206,6 +206,7 @@ export async function migrate() {
|
|||||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_state_id BIGINT;");
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_state_id BIGINT;");
|
||||||
await query("UPDATE tickets SET zammad_state_id = 2 WHERE zammad_state_id = 1;");
|
await query("UPDATE tickets SET zammad_state_id = 2 WHERE zammad_state_id = 1;");
|
||||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_owner_id BIGINT;");
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_owner_id BIGINT;");
|
||||||
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS user_id BIGINT REFERENCES users(id) ON DELETE SET NULL;");
|
||||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_merged_from_ticket_id BIGINT;");
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_merged_from_ticket_id BIGINT;");
|
||||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_merged_from_ticket_number TEXT;");
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_merged_from_ticket_number TEXT;");
|
||||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_preferences JSONB NOT NULL DEFAULT '{}'::jsonb;");
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_preferences JSONB NOT NULL DEFAULT '{}'::jsonb;");
|
||||||
@@ -225,6 +226,55 @@ export async function migrate() {
|
|||||||
END IF;
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
`);
|
`);
|
||||||
|
await query(`
|
||||||
|
WITH unique_zammad_users AS (
|
||||||
|
SELECT zammad_user_id, MIN(id) AS user_id
|
||||||
|
FROM users
|
||||||
|
WHERE role = 'user'
|
||||||
|
AND active
|
||||||
|
AND zammad_user_id IS NOT NULL
|
||||||
|
GROUP BY zammad_user_id
|
||||||
|
HAVING COUNT(*) = 1
|
||||||
|
),
|
||||||
|
zammad_ticket_owners AS (
|
||||||
|
SELECT t.id, uzu.user_id
|
||||||
|
FROM tickets t
|
||||||
|
LEFT JOIN unique_zammad_users uzu ON uzu.zammad_user_id = t.zammad_owner_id
|
||||||
|
WHERE t.zammad_owner_id IS NOT NULL
|
||||||
|
)
|
||||||
|
UPDATE tickets t
|
||||||
|
SET user_id = zto.user_id
|
||||||
|
FROM zammad_ticket_owners zto
|
||||||
|
WHERE t.id = zto.id
|
||||||
|
AND t.user_id IS DISTINCT FROM zto.user_id;
|
||||||
|
`);
|
||||||
|
await query(`
|
||||||
|
UPDATE tickets t
|
||||||
|
SET user_id = owner.user_id
|
||||||
|
FROM (
|
||||||
|
SELECT ticket_id, MIN(user_id) AS user_id
|
||||||
|
FROM projects
|
||||||
|
GROUP BY ticket_id
|
||||||
|
) owner
|
||||||
|
WHERE t.user_id IS NULL
|
||||||
|
AND t.zammad_owner_id IS NULL
|
||||||
|
AND t.id = owner.ticket_id;
|
||||||
|
`);
|
||||||
|
await query(`
|
||||||
|
UPDATE tickets t
|
||||||
|
SET user_id = owner.user_id
|
||||||
|
FROM (
|
||||||
|
SELECT ticket_id, MIN(user_id) AS user_id
|
||||||
|
FROM sessions
|
||||||
|
WHERE user_id IS NOT NULL
|
||||||
|
GROUP BY ticket_id
|
||||||
|
) owner
|
||||||
|
WHERE t.user_id IS NULL
|
||||||
|
AND t.zammad_owner_id IS NULL
|
||||||
|
AND t.id = owner.ticket_id;
|
||||||
|
`);
|
||||||
|
await query("CREATE INDEX IF NOT EXISTS idx_tickets_user ON tickets(user_id);");
|
||||||
|
await query("CREATE INDEX IF NOT EXISTS idx_tickets_zammad_owner ON tickets(zammad_owner_id);");
|
||||||
|
|
||||||
await query(`
|
await query(`
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
@@ -708,6 +758,43 @@ export async function migrate() {
|
|||||||
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;");
|
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;");
|
||||||
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS close_review_required BOOLEAN NOT NULL DEFAULT false;");
|
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS close_review_required BOOLEAN NOT NULL DEFAULT false;");
|
||||||
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS manual_consumption_minutes INTEGER NOT NULL DEFAULT 0;");
|
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS manual_consumption_minutes INTEGER NOT NULL DEFAULT 0;");
|
||||||
|
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS zammad_owner_id BIGINT;");
|
||||||
|
await query("ALTER TABLE projects ALTER COLUMN user_id DROP NOT NULL;");
|
||||||
|
await query(`
|
||||||
|
UPDATE projects p
|
||||||
|
SET zammad_owner_id = t.zammad_owner_id
|
||||||
|
FROM tickets t
|
||||||
|
WHERE p.ticket_id = t.id
|
||||||
|
AND p.zammad_owner_id IS NULL
|
||||||
|
AND t.zammad_owner_id IS NOT NULL;
|
||||||
|
`);
|
||||||
|
await query(`
|
||||||
|
UPDATE projects p
|
||||||
|
SET user_id = t.user_id,
|
||||||
|
zammad_owner_id = t.zammad_owner_id
|
||||||
|
FROM tickets t
|
||||||
|
WHERE p.ticket_id = t.id
|
||||||
|
AND t.zammad_owner_id IS NOT NULL
|
||||||
|
AND (
|
||||||
|
p.user_id IS DISTINCT FROM t.user_id
|
||||||
|
OR p.zammad_owner_id IS DISTINCT FROM t.zammad_owner_id
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM projects existing
|
||||||
|
WHERE existing.ticket_id = p.ticket_id
|
||||||
|
AND existing.id <> p.id
|
||||||
|
AND (
|
||||||
|
(t.user_id IS NOT NULL AND existing.user_id = t.user_id)
|
||||||
|
OR (
|
||||||
|
t.user_id IS NULL
|
||||||
|
AND existing.user_id IS NULL
|
||||||
|
AND existing.zammad_owner_id IS NOT DISTINCT FROM t.zammad_owner_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_zammad_owner_ticket ON projects(zammad_owner_id, ticket_id) WHERE user_id IS NULL AND zammad_owner_id IS NOT NULL;");
|
||||||
await query(`
|
await query(`
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -799,13 +886,26 @@ export async function migrate() {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
await query(`
|
await query(`
|
||||||
INSERT INTO projects (user_id, ticket_id, teamspace_project, description, goals)
|
INSERT INTO projects (user_id, zammad_owner_id, ticket_id, teamspace_project, description, goals)
|
||||||
SELECT DISTINCT s.user_id, s.ticket_id, COALESCE(t.title, ''), '', ''
|
SELECT DISTINCT
|
||||||
|
CASE
|
||||||
|
WHEN t.zammad_owner_id IS NOT NULL THEN zammad_owner_user.id
|
||||||
|
ELSE s.user_id
|
||||||
|
END,
|
||||||
|
t.zammad_owner_id,
|
||||||
|
s.ticket_id,
|
||||||
|
COALESCE(t.title, ''),
|
||||||
|
'',
|
||||||
|
''
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN tickets t ON t.id = s.ticket_id
|
JOIN tickets t ON t.id = s.ticket_id
|
||||||
|
LEFT JOIN users zammad_owner_user
|
||||||
|
ON zammad_owner_user.role = 'user'
|
||||||
|
AND zammad_owner_user.active
|
||||||
|
AND zammad_owner_user.zammad_user_id = t.zammad_owner_id
|
||||||
WHERE s.work_type = 'consulting'
|
WHERE s.work_type = 'consulting'
|
||||||
AND s.user_id IS NOT NULL
|
AND s.user_id IS NOT NULL
|
||||||
ON CONFLICT (user_id, ticket_id) DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await query(`
|
await query(`
|
||||||
|
|||||||
@@ -1531,7 +1531,7 @@ export function App() {
|
|||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton tooltip="Worklog">
|
<SidebarMenuButton tooltip="Worklog">
|
||||||
<FolderGit />
|
<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>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|||||||
@@ -140,6 +140,11 @@ export type TicketMeta = {
|
|||||||
zammad_priority_id?: string | null;
|
zammad_priority_id?: string | null;
|
||||||
zammad_state_id?: string | null;
|
zammad_state_id?: string | null;
|
||||||
zammad_owner_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;
|
project_id?: string | null;
|
||||||
is_project?: boolean;
|
is_project?: boolean;
|
||||||
project_name?: string | null;
|
project_name?: string | null;
|
||||||
@@ -184,6 +189,9 @@ export type ProjectSummary = {
|
|||||||
work_type: WorkType | null;
|
work_type: WorkType | null;
|
||||||
zammad_priority_id?: string | null;
|
zammad_priority_id?: string | null;
|
||||||
zammad_state_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_pending_time?: string | null;
|
||||||
zammad_merged_from_ticket_id?: string | null;
|
zammad_merged_from_ticket_id?: string | null;
|
||||||
zammad_merged_from_ticket_number?: string | null;
|
zammad_merged_from_ticket_number?: string | null;
|
||||||
@@ -376,8 +384,16 @@ export type TicketPeriod = {
|
|||||||
organization_name: string | null;
|
organization_name: string | null;
|
||||||
customer_name: string | null;
|
customer_name: string | null;
|
||||||
work_type: WorkType | null;
|
work_type: WorkType | null;
|
||||||
|
zammad_ticket_id?: string | null;
|
||||||
|
zammad_group_id?: string | null;
|
||||||
zammad_priority_id?: string | null;
|
zammad_priority_id?: string | null;
|
||||||
zammad_state_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;
|
closed_at: string | null;
|
||||||
requires_closure: boolean;
|
requires_closure: boolean;
|
||||||
recurring_session_count: number;
|
recurring_session_count: number;
|
||||||
|
|||||||
@@ -1664,7 +1664,7 @@ export function ProjectsPage({ onNavigate, onStartTimer }: ProjectsPageProps) {
|
|||||||
const remaining = projectRemaining(project, viewSettings.standardTeamspaceOnly);
|
const remaining = projectRemaining(project, viewSettings.standardTeamspaceOnly);
|
||||||
const usedPercent = projectConsumptionPercent(project, viewSettings.standardTeamspaceOnly);
|
const usedPercent = projectConsumptionPercent(project, viewSettings.standardTeamspaceOnly);
|
||||||
const showBudgetedTeamspaceEntries = !viewSettings.standardTeamspaceOnly && budgetedTeamspaceEntries.length > 1;
|
const showBudgetedTeamspaceEntries = !viewSettings.standardTeamspaceOnly && budgetedTeamspaceEntries.length > 1;
|
||||||
const canEditProject = project.can_edit !== false;
|
const canEditProject = project.can_edit === true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={project.id} className="border-b last:border-b-0">
|
<div key={project.id} className="border-b last:border-b-0">
|
||||||
@@ -1797,7 +1797,7 @@ export function ProjectsPage({ onNavigate, onStartTimer }: ProjectsPageProps) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 shrink-0 px-2 text-xs"
|
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)}
|
onClick={() => void acknowledgeProjectReviewCheck(project)}
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" />
|
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
|||||||
@@ -912,8 +912,17 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
const projectBudgetMinutes = project?.hour_budget_minutes ?? null;
|
const projectBudgetMinutes = project?.hour_budget_minutes ?? null;
|
||||||
const projectConsumptionUsagePercent = project ? projectConsumptionPercent(projectConsumptionMinutes, projectBudgetMinutes) : null;
|
const projectConsumptionUsagePercent = project ? projectConsumptionPercent(projectConsumptionMinutes, projectBudgetMinutes) : null;
|
||||||
const projectRemainingMinutes = projectBudgetMinutes && projectBudgetMinutes > 0 ? projectBudgetMinutes - projectConsumptionMinutes : null;
|
const projectRemainingMinutes = projectBudgetMinutes && projectBudgetMinutes > 0 ? projectBudgetMinutes - projectConsumptionMinutes : null;
|
||||||
const canEditProject = project?.can_edit !== false;
|
const canEditProject = project?.can_edit === true;
|
||||||
const canEditTicketData = !project || canEditProject;
|
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 isProject = Boolean(project);
|
||||||
const isGlobalTicket = periodType === "all";
|
const isGlobalTicket = periodType === "all";
|
||||||
const isPeriodClosed = Boolean(data?.closed);
|
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 periodTeamspaceMinutes = (data?.dayBillings ?? []).reduce((sum, billing) => sum + Number(billing.billed_minutes ?? 0), 0);
|
||||||
const ticketDataDirty = Boolean(
|
const ticketDataDirty = Boolean(
|
||||||
ticket &&
|
ticket &&
|
||||||
|
canEditTicketData &&
|
||||||
(editTicketNumber !== ticket.ticket_number ||
|
(editTicketNumber !== ticket.ticket_number ||
|
||||||
editTicketTitle !== (ticket.title ?? "") ||
|
editTicketTitle !== (ticket.title ?? "") ||
|
||||||
editOrganizationId !== (ticket.organization_id ?? "") ||
|
editOrganizationId !== (ticket.organization_id ?? "") ||
|
||||||
@@ -1284,8 +1294,10 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!canEditTicketData) {
|
if (!canEditTicketData) {
|
||||||
toast.error("Projekt ist geteilt", {
|
toast.error(ticketManagedByZammad ? "Ticketdaten kommen aus Zammad" : "Ticket gehört einem anderen Owner", {
|
||||||
description: "Nur der Zammad-Owner kann Ticket- und Projektdaten ändern."
|
description: ticketManagedByZammad
|
||||||
|
? "Nummer, Art, Titel und Organisation werden durch den Zammad-Sync verwaltet."
|
||||||
|
: "Nur der Owner kann Ticketdaten ändern."
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1334,7 +1346,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.can_edit === false) {
|
if (!canEditProject) {
|
||||||
toast.error("Projekt ist geteilt", {
|
toast.error("Projekt ist geteilt", {
|
||||||
description: "Nur der Zammad-Owner kann die Projektdaten bearbeiten."
|
description: "Nur der Zammad-Owner kann die Projektdaten bearbeiten."
|
||||||
});
|
});
|
||||||
@@ -1352,7 +1364,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.can_edit === false) {
|
if (!canEditProject) {
|
||||||
toast.error("Projekt ist geteilt", {
|
toast.error("Projekt ist geteilt", {
|
||||||
description: "Nur der Zammad-Owner kann die Projektdaten bearbeiten."
|
description: "Nur der Zammad-Owner kann die Projektdaten bearbeiten."
|
||||||
});
|
});
|
||||||
@@ -1407,7 +1419,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.can_edit === false) {
|
if (!canEditProject) {
|
||||||
toast.error("Projekt ist geteilt", {
|
toast.error("Projekt ist geteilt", {
|
||||||
description: "Nur der Zammad-Owner kann den Projektstatus ändern."
|
description: "Nur der Zammad-Owner kann den Projektstatus ändern."
|
||||||
});
|
});
|
||||||
@@ -1433,7 +1445,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.can_edit === false) {
|
if (!canEditProject) {
|
||||||
toast.error("Projekt ist geteilt", {
|
toast.error("Projekt ist geteilt", {
|
||||||
description: "Nur der Zammad-Owner kann die Prüfung bestätigen."
|
description: "Nur der Zammad-Owner kann die Prüfung bestätigen."
|
||||||
});
|
});
|
||||||
@@ -1811,6 +1823,26 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : 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">
|
<div className="grid gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="p-4 pb-2">
|
<CardHeader className="p-4 pb-2">
|
||||||
@@ -1820,7 +1852,9 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
<CardDescription>
|
<CardDescription>
|
||||||
{canEditTicketData
|
{canEditTicketData
|
||||||
? "Änderungen an Organisation und Art werden auf alle vorhandenen Sessions dieses Tickets übernommen."
|
? "Ä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>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<HelpLink anchor="ticketdaten" label="Hilfe zu Ticketdaten" />
|
<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}
|
{(project.unassigned_teamspace_session_count ?? 0) > 0 ? <Badge variant="warning">{project.unassigned_teamspace_session_count} ohne TS</Badge> : null}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{!canEditProject && project.owner_display_name
|
{!canEditProject && projectOwnerLabel
|
||||||
? `Eigentümer: ${project.owner_display_name}`
|
? `Eigentümer: ${projectOwnerLabel}`
|
||||||
: showBudgetedProjectEntries
|
: showBudgetedProjectEntries
|
||||||
? `${budgetedProjectEntries.length} budgetierte Teamspace-Einträge`
|
? `${budgetedProjectEntries.length} budgetierte Teamspace-Einträge`
|
||||||
: budgetedProjectEntries[0] ? teamspaceEntryDisplayLabel(budgetedProjectEntries[0]) : project.teamspace_project || "Kein Teamspace-Projekt hinterlegt"}
|
: 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"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
disabled={savingProjectStatus || project.can_edit === false}
|
disabled={savingProjectStatus || !canEditProject}
|
||||||
onClick={() => void acknowledgeProjectReviewCheck()}
|
onClick={() => void acknowledgeProjectReviewCheck()}
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -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) {
|
function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyncExistingFields) {
|
||||||
if (!item.zammad) {
|
if (!item.zammad) {
|
||||||
return item.changes.filter((change) => changeAllowedByFields(item, change, fields));
|
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 {
|
function rebuildZammadSyncResultFromItems(mode: ZammadSyncResult["mode"], items: ZammadSyncItem[], checked = items.length): ZammadSyncResult {
|
||||||
|
|||||||
Reference in new issue
Block a user