Tagessteuerung
This commit is contained in:
1 parent
560b747d5d
commit
904b123884
8 files changed
+744
-80
No files matched your search
+136
-13
@@ -299,6 +299,7 @@ type ProjectRow = {
|
||||
work_type: "support" | "consulting" | null;
|
||||
zammad_priority_id?: string | null;
|
||||
zammad_state_id?: string | null;
|
||||
zammad_pending_time?: string | null;
|
||||
teamspace_project: string | null;
|
||||
description: string;
|
||||
goals: string;
|
||||
@@ -457,6 +458,11 @@ function numberOrNull(value: unknown) {
|
||||
return Number.isInteger(number) && number > 0 ? number : null;
|
||||
}
|
||||
|
||||
function zammadStateIdOrNull(value: unknown) {
|
||||
const stateId = numberOrNull(value);
|
||||
return stateId === 1 ? 2 : stateId;
|
||||
}
|
||||
|
||||
const zammadConsultingGroupId = 2;
|
||||
const zammadSupportGroupIds = [3, 4];
|
||||
|
||||
@@ -699,22 +705,24 @@ function zammadTicketImportFromRaw(rawTicket: Record<string, unknown>, fallbackT
|
||||
|
||||
const preferences = isPlainObject(rawTicket.preferences) ? rawTicket.preferences : {};
|
||||
const teamspaceProjects = parseZammadTeamspaceProjects(zammadPreferenceValue(rawTicket, "project"));
|
||||
const pendingTime = parseZammadPendingTime(zammadPreferenceValue(rawTicket, "pending_time"));
|
||||
const storedPreferences = pendingTime ? { ...preferences, pending_time: pendingTime } : preferences;
|
||||
|
||||
return {
|
||||
zammadTicketId,
|
||||
ticketNumber,
|
||||
zammadGroupId: groupId,
|
||||
zammadPriorityId: numberOrNull(rawTicket.priority_id),
|
||||
zammadStateId: numberOrNull(rawTicket.state_id),
|
||||
zammadStateId: zammadStateIdOrNull(rawTicket.state_id),
|
||||
organizationId,
|
||||
title: typeof rawTicket.title === "string" && rawTicket.title.trim() ? rawTicket.title.trim() : null,
|
||||
zammadOwnerId: numberOrNull(rawTicket.owner_id),
|
||||
workType,
|
||||
preferences,
|
||||
preferences: storedPreferences,
|
||||
teamspaceBudgetMinutes: parseZammadBudgetMinutes(zammadPreferenceValue(rawTicket, "gesamtbudget")),
|
||||
teamspaceProject: teamspaceProjects[0] ?? null,
|
||||
teamspaceProjects,
|
||||
pendingTime: parseZammadPendingTime(zammadPreferenceValue(rawTicket, "pending_time"))
|
||||
pendingTime
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1622,7 +1630,8 @@ async function getProjectRowForTicket(client: QueryClient, ticketId: string, pro
|
||||
COALESCE(o.name, t.customer_name) AS customer_name,
|
||||
t.work_type,
|
||||
t.zammad_priority_id::text AS zammad_priority_id,
|
||||
t.zammad_state_id::text AS zammad_state_id,
|
||||
CASE WHEN t.zammad_state_id = 1 THEN '2' ELSE t.zammad_state_id::text END AS zammad_state_id,
|
||||
t.zammad_preferences ->> 'pending_time' AS zammad_pending_time,
|
||||
p.teamspace_project,
|
||||
p.description,
|
||||
p.goals,
|
||||
@@ -2020,7 +2029,12 @@ function syncIdComparableValue(value: unknown) {
|
||||
}
|
||||
|
||||
function syncComparableValueForField(field: string, value: unknown) {
|
||||
if (field === "Organisation" || field === "Zammad-Priorität" || field === "Zammad-Status") {
|
||||
if (field === "Zammad-Status") {
|
||||
const stateId = zammadStateIdOrNull(value);
|
||||
return stateId === null ? null : String(stateId);
|
||||
}
|
||||
|
||||
if (field === "Organisation" || field === "Zammad-Priorität") {
|
||||
return syncIdComparableValue(value);
|
||||
}
|
||||
|
||||
@@ -2072,7 +2086,7 @@ function zammadTicketFromSyncSnapshot(snapshot: Record<string, unknown> | null):
|
||||
const ticketNumber = typeof snapshot.ticketNumber === "string" ? snapshot.ticketNumber : null;
|
||||
const zammadGroupId = numberOrNull(snapshot.groupId);
|
||||
const zammadPriorityId = numberOrNull(snapshot.priorityId);
|
||||
const zammadStateId = numberOrNull(snapshot.stateId);
|
||||
const zammadStateId = zammadStateIdOrNull(snapshot.stateId);
|
||||
const organizationId = numberOrNull(snapshot.organizationId);
|
||||
const zammadOwnerId = numberOrNull(snapshot.ownerId);
|
||||
const workType = zammadWorkType(zammadGroupId);
|
||||
@@ -2870,6 +2884,7 @@ async function getPeriodOverview(config: PeriodConfig, period: ParsedPeriod, use
|
||||
COUNT(*) FILTER (WHERE s.recurring_billing_id IS NOT NULL)::int AS recurring_session_count,
|
||||
COUNT(*) FILTER (WHERE s.recurring_billing_id IS NULL)::int AS manual_session_count,
|
||||
COALESCE(SUM(s.rounded_minutes) FILTER (WHERE s.billing_status = 'billed'), 0)::int AS total_minutes,
|
||||
ticket_crm.crm_billed_minutes,
|
||||
COALESCE(SUM(s.rounded_minutes) FILTER (WHERE s.billing_status = 'non_billable'), 0)::int AS non_billable_minutes,
|
||||
COALESCE(SUM(s.rounded_minutes) FILTER (WHERE s.billing_status IS NULL), 0)::int AS open_minutes,
|
||||
COUNT(*) FILTER (WHERE s.billing_status IS NULL)::int AS open_count,
|
||||
@@ -2883,6 +2898,14 @@ async function getPeriodOverview(config: PeriodConfig, period: ParsedPeriod, use
|
||||
JOIN sessions s ON s.ticket_id = t.id
|
||||
LEFT JOIN organizations o ON o.zammad_id = t.organization_id
|
||||
LEFT JOIN projects p ON p.ticket_id = t.id AND p.user_id = $3
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(SUM(tdb.billed_minutes), 0)::int AS crm_billed_minutes
|
||||
FROM ticket_day_billings tdb
|
||||
WHERE tdb.ticket_id = t.id
|
||||
AND tdb.user_id = $3
|
||||
AND tdb.day >= ($1::timestamptz AT TIME ZONE 'Europe/Berlin')::date
|
||||
AND tdb.day < ($2::timestamptz AT TIME ZONE 'Europe/Berlin')::date
|
||||
) ticket_crm ON true
|
||||
WHERE s.started_at >= $1::timestamptz AND s.started_at < $2::timestamptz
|
||||
AND s.user_id = $3
|
||||
GROUP BY
|
||||
@@ -2893,7 +2916,8 @@ async function getPeriodOverview(config: PeriodConfig, period: ParsedPeriod, use
|
||||
o.name,
|
||||
t.customer_name,
|
||||
t.work_type,
|
||||
p.id
|
||||
p.id,
|
||||
ticket_crm.crm_billed_minutes
|
||||
ORDER BY t.ticket_number ASC;
|
||||
`,
|
||||
[period.startIso, period.endIso, userId]
|
||||
@@ -3390,7 +3414,7 @@ async function getPeriodTicket(config: PeriodConfig, period: ParsedPeriod, ticke
|
||||
COALESCE(o.name, t.customer_name) AS customer_name,
|
||||
t.work_type,
|
||||
t.zammad_priority_id::text AS zammad_priority_id,
|
||||
t.zammad_state_id::text AS zammad_state_id,
|
||||
CASE WHEN t.zammad_state_id = 1 THEN '2' ELSE t.zammad_state_id::text END AS zammad_state_id,
|
||||
NULL::timestamptz AS closed_at,
|
||||
pc.closed_at AS period_closed_at,
|
||||
COALESCE(period_sessions.session_count, 0)::int AS ticket_session_count,
|
||||
@@ -3527,7 +3551,7 @@ async function getTicketDetails(ticketId: number, userId: string, filters: { mon
|
||||
COALESCE(o.name, t.customer_name) AS customer_name,
|
||||
t.work_type,
|
||||
t.zammad_priority_id::text AS zammad_priority_id,
|
||||
t.zammad_state_id::text AS zammad_state_id,
|
||||
CASE WHEN t.zammad_state_id = 1 THEN '2' ELSE t.zammad_state_id::text END AS zammad_state_id,
|
||||
NULL::timestamptz AS closed_at,
|
||||
NULL::timestamptz AS period_closed_at,
|
||||
COALESCE(all_sessions.session_count, 0)::int AS ticket_session_count,
|
||||
@@ -3670,7 +3694,7 @@ async function getTicketDetails(ticketId: number, userId: string, filters: { mon
|
||||
COALESCE(o.name, t.customer_name) AS customer_name,
|
||||
t.work_type,
|
||||
t.zammad_priority_id::text AS zammad_priority_id,
|
||||
t.zammad_state_id::text AS zammad_state_id,
|
||||
CASE WHEN t.zammad_state_id = 1 THEN '2' ELSE t.zammad_state_id::text END AS zammad_state_id,
|
||||
p.teamspace_project,
|
||||
p.description,
|
||||
p.goals,
|
||||
@@ -3995,6 +4019,103 @@ app.put("/api/dashboard/preferences", requireUser, async (req, res) => {
|
||||
res.json({ preference: saved });
|
||||
});
|
||||
|
||||
app.get("/api/dashboard/day-control", requireUser, async (req, res) => {
|
||||
const userId = currentUser(req).id;
|
||||
const result = await query(
|
||||
`
|
||||
SELECT
|
||||
t.id::text AS ticket_id,
|
||||
t.ticket_number,
|
||||
t.title,
|
||||
COALESCE(o.name, t.customer_name) AS organization_name,
|
||||
t.work_type,
|
||||
t.zammad_priority_id::text AS zammad_priority_id,
|
||||
CASE WHEN t.zammad_state_id = 1 THEN '2' ELSE t.zammad_state_id::text END AS zammad_state_id,
|
||||
t.zammad_preferences ->> 'pending_time' AS zammad_pending_time,
|
||||
MAX(s.started_at) FILTER (WHERE s.user_id = $1)::text AS latest_started_at,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM projects own_projects
|
||||
WHERE own_projects.ticket_id = t.id
|
||||
AND own_projects.user_id = $1
|
||||
) AS has_project,
|
||||
COUNT(s.id) FILTER (WHERE s.user_id = $1)::int AS own_session_count,
|
||||
COUNT(s.id) FILTER (WHERE s.user_id = $1 AND s.billing_status IS NULL)::int AS open_session_count,
|
||||
COALESCE(todo_stats.open_todo_count, 0)::int AS open_todo_count,
|
||||
COALESCE(todo_stats.due_todo_count, 0)::int AS due_todo_count
|
||||
FROM tickets t
|
||||
LEFT JOIN organizations o ON o.zammad_id = t.organization_id
|
||||
LEFT JOIN sessions s ON s.ticket_id = t.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE tt.status = 'open')::int AS open_todo_count,
|
||||
COUNT(*) FILTER (
|
||||
WHERE tt.status = 'open'
|
||||
AND (
|
||||
tt.ends_on < (now() AT TIME ZONE 'Europe/Berlin')::date
|
||||
OR tt.ends_on = (now() AT TIME ZONE 'Europe/Berlin')::date
|
||||
OR tt.starts_on = (now() AT TIME ZONE 'Europe/Berlin')::date
|
||||
OR (tt.starts_on <= (now() AT TIME ZONE 'Europe/Berlin')::date AND tt.ends_on >= (now() AT TIME ZONE 'Europe/Berlin')::date)
|
||||
)
|
||||
)::int AS due_todo_count
|
||||
FROM ticket_todos tt
|
||||
WHERE tt.ticket_id = t.id
|
||||
AND tt.user_id = $1
|
||||
) todo_stats ON true
|
||||
WHERE (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM sessions own_sessions
|
||||
WHERE own_sessions.ticket_id = t.id
|
||||
AND own_sessions.user_id = $1
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM projects own_projects
|
||||
WHERE own_projects.ticket_id = t.id
|
||||
AND own_projects.user_id = $1
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM ticket_todos own_todos
|
||||
WHERE own_todos.ticket_id = t.id
|
||||
AND own_todos.user_id = $1
|
||||
)
|
||||
)
|
||||
AND (
|
||||
t.zammad_state_id IN (1, 2, 3, 7)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM sessions open_sessions
|
||||
WHERE open_sessions.ticket_id = t.id
|
||||
AND open_sessions.user_id = $1
|
||||
AND open_sessions.billing_status IS NULL
|
||||
)
|
||||
)
|
||||
GROUP BY
|
||||
t.id,
|
||||
t.ticket_number,
|
||||
t.title,
|
||||
o.name,
|
||||
t.customer_name,
|
||||
t.work_type,
|
||||
t.zammad_priority_id,
|
||||
t.zammad_state_id,
|
||||
t.zammad_preferences,
|
||||
todo_stats.open_todo_count,
|
||||
todo_stats.due_todo_count
|
||||
ORDER BY
|
||||
CASE t.zammad_priority_id WHEN 3 THEN 0 WHEN 2 THEN 1 WHEN 1 THEN 2 ELSE 3 END,
|
||||
MAX(s.started_at) FILTER (WHERE s.user_id = $1) DESC NULLS LAST,
|
||||
t.ticket_number ASC
|
||||
LIMIT 200;
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
res.json({ tickets: result.rows });
|
||||
});
|
||||
|
||||
app.post("/api/dashboard/favorites", requireUser, async (req, res) => {
|
||||
const userId = currentUser(req).id;
|
||||
const ticketNumber = parseTrackableTicketNumber(req.body.ticketNumber);
|
||||
@@ -5360,7 +5481,7 @@ app.get("/api/tickets/lookup", requireUser, async (req, res) => {
|
||||
t.zammad_ticket_id::text AS zammad_ticket_id,
|
||||
t.zammad_group_id::text AS zammad_group_id,
|
||||
t.zammad_priority_id::text AS zammad_priority_id,
|
||||
t.zammad_state_id::text AS zammad_state_id,
|
||||
CASE WHEN t.zammad_state_id = 1 THEN '2' ELSE t.zammad_state_id::text END AS zammad_state_id,
|
||||
t.zammad_owner_id::text AS zammad_owner_id,
|
||||
p.id::text AS project_id,
|
||||
(p.id IS NOT NULL) AS is_project,
|
||||
@@ -6074,7 +6195,8 @@ app.get("/api/projects", requireUser, async (req, res) => {
|
||||
COALESCE(o.name, t.customer_name) AS customer_name,
|
||||
t.work_type,
|
||||
t.zammad_priority_id::text AS zammad_priority_id,
|
||||
t.zammad_state_id::text AS zammad_state_id,
|
||||
CASE WHEN t.zammad_state_id = 1 THEN '2' ELSE t.zammad_state_id::text END AS zammad_state_id,
|
||||
t.zammad_preferences ->> 'pending_time' AS zammad_pending_time,
|
||||
p.teamspace_project,
|
||||
p.description,
|
||||
p.goals,
|
||||
@@ -6234,7 +6356,8 @@ app.get("/api/projects/:projectId", requireUser, async (req, res) => {
|
||||
COALESCE(o.name, t.customer_name) AS customer_name,
|
||||
t.work_type,
|
||||
t.zammad_priority_id::text AS zammad_priority_id,
|
||||
t.zammad_state_id::text AS zammad_state_id,
|
||||
CASE WHEN t.zammad_state_id = 1 THEN '2' ELSE t.zammad_state_id::text END AS zammad_state_id,
|
||||
t.zammad_preferences ->> 'pending_time' AS zammad_pending_time,
|
||||
p.teamspace_project,
|
||||
p.description,
|
||||
p.goals,
|
||||
|
||||
@@ -169,6 +169,7 @@ export async function migrate() {
|
||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_group_id BIGINT;");
|
||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_priority_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("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_owner_id BIGINT;");
|
||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_preferences JSONB NOT NULL DEFAULT '{}'::jsonb;");
|
||||
await query(`
|
||||
|
||||
Reference in new issue
Block a user