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'
|
||||
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
|
||||
TICKETTRACKER_IMAGE=git.d-razz.de/michael/tickettracker:latest
|
||||
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("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 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_number TEXT;");
|
||||
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 $$;
|
||||
`);
|
||||
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(`
|
||||
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 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 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(`
|
||||
DO $$
|
||||
BEGIN
|
||||
@@ -799,13 +886,26 @@ export async function migrate() {
|
||||
`);
|
||||
|
||||
await query(`
|
||||
INSERT INTO projects (user_id, ticket_id, teamspace_project, description, goals)
|
||||
SELECT DISTINCT s.user_id, s.ticket_id, COALESCE(t.title, ''), '', ''
|
||||
INSERT INTO projects (user_id, zammad_owner_id, ticket_id, teamspace_project, description, goals)
|
||||
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
|
||||
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'
|
||||
AND s.user_id IS NOT NULL
|
||||
ON CONFLICT (user_id, ticket_id) DO NOTHING;
|
||||
ON CONFLICT DO NOTHING;
|
||||
`);
|
||||
|
||||
await query(`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user