Update
This commit is contained in:
1 parent
0c14a3acbd
commit
2105a0c5f7
11 files changed
+398
-42
No files matched your search
+5
-2
@@ -22,6 +22,7 @@ export type AuthUser = {
|
|||||||
zammad_session_note_default_enabled: boolean;
|
zammad_session_note_default_enabled: boolean;
|
||||||
zammad_time_unit_rounding_enabled: boolean;
|
zammad_time_unit_rounding_enabled: boolean;
|
||||||
zammad_time_unit_rounding_minutes: number;
|
zammad_time_unit_rounding_minutes: number;
|
||||||
|
zammad_article_type_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -87,7 +88,8 @@ function publicUser(user: AuthUser) {
|
|||||||
session_billing_prompt_enabled: Boolean(user.session_billing_prompt_enabled),
|
session_billing_prompt_enabled: Boolean(user.session_billing_prompt_enabled),
|
||||||
zammad_session_note_default_enabled: Boolean(user.zammad_session_note_default_enabled),
|
zammad_session_note_default_enabled: Boolean(user.zammad_session_note_default_enabled),
|
||||||
zammad_time_unit_rounding_enabled: Boolean(user.zammad_time_unit_rounding_enabled),
|
zammad_time_unit_rounding_enabled: Boolean(user.zammad_time_unit_rounding_enabled),
|
||||||
zammad_time_unit_rounding_minutes: Number(user.zammad_time_unit_rounding_minutes ?? 15)
|
zammad_time_unit_rounding_minutes: Number(user.zammad_time_unit_rounding_minutes ?? 15),
|
||||||
|
zammad_article_type_id: Number(user.zammad_article_type_id ?? 6)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +151,8 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
|
|||||||
u.session_billing_prompt_enabled,
|
u.session_billing_prompt_enabled,
|
||||||
u.zammad_session_note_default_enabled,
|
u.zammad_session_note_default_enabled,
|
||||||
u.zammad_time_unit_rounding_enabled,
|
u.zammad_time_unit_rounding_enabled,
|
||||||
u.zammad_time_unit_rounding_minutes
|
u.zammad_time_unit_rounding_minutes,
|
||||||
|
u.zammad_article_type_id
|
||||||
FROM auth_sessions s
|
FROM auth_sessions s
|
||||||
JOIN users u ON u.id = s.user_id
|
JOIN users u ON u.id = s.user_id
|
||||||
WHERE s.token_hash = $1
|
WHERE s.token_hash = $1
|
||||||
|
|||||||
+158
-15
@@ -240,6 +240,20 @@ function parseZammadTimeUnitRoundingMinutes(value: unknown, fallback = 15) {
|
|||||||
return minutes;
|
return minutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseZammadArticleTypeId(value: unknown, fallback = 6) {
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return fallback ?? 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeId = Number(value);
|
||||||
|
|
||||||
|
if (![1, 2, 3, 4, 5, 6].includes(typeId)) {
|
||||||
|
throw badRequest("zammadArticleTypeId must be 1, 2, 3, 4, 5 or 6");
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeId;
|
||||||
|
}
|
||||||
|
|
||||||
function optionalTicketSessionMonth(value: unknown) {
|
function optionalTicketSessionMonth(value: unknown) {
|
||||||
if (value === undefined || value === null || value === "" || value === "all") {
|
if (value === undefined || value === null || value === "" || value === "all") {
|
||||||
return null;
|
return null;
|
||||||
@@ -635,12 +649,14 @@ type ZammadSessionArticlePayload = {
|
|||||||
body: string;
|
body: string;
|
||||||
content_type: "text/plain";
|
content_type: "text/plain";
|
||||||
time_unit: number;
|
time_unit: number;
|
||||||
|
accounted_time_type_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function zammadSessionArticlePayload(input: {
|
function zammadSessionArticlePayload(input: {
|
||||||
zammadTicketId: string | number;
|
zammadTicketId: string | number;
|
||||||
activity: string;
|
activity: string;
|
||||||
timeUnit: number;
|
timeUnit: number;
|
||||||
|
articleTypeId: number;
|
||||||
}): ZammadSessionArticlePayload {
|
}): ZammadSessionArticlePayload {
|
||||||
const ticketId = Number(input.zammadTicketId);
|
const ticketId = Number(input.zammadTicketId);
|
||||||
|
|
||||||
@@ -655,7 +671,8 @@ function zammadSessionArticlePayload(input: {
|
|||||||
subject: "TicketTracker Session",
|
subject: "TicketTracker Session",
|
||||||
body: input.activity,
|
body: input.activity,
|
||||||
content_type: "text/plain",
|
content_type: "text/plain",
|
||||||
time_unit: input.timeUnit
|
time_unit: input.timeUnit,
|
||||||
|
accounted_time_type_id: input.articleTypeId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,6 +689,7 @@ async function enqueueZammadSessionArticle(client: pg.PoolClient, input: {
|
|||||||
zammadTicketId: string | number | null;
|
zammadTicketId: string | number | null;
|
||||||
activity: string;
|
activity: string;
|
||||||
timeUnit: number;
|
timeUnit: number;
|
||||||
|
articleTypeId: number;
|
||||||
error: string;
|
error: string;
|
||||||
}) {
|
}) {
|
||||||
await client.query(
|
await client.query(
|
||||||
@@ -684,14 +702,16 @@ async function enqueueZammadSessionArticle(client: pg.PoolClient, input: {
|
|||||||
zammad_ticket_id,
|
zammad_ticket_id,
|
||||||
activity,
|
activity,
|
||||||
time_unit,
|
time_unit,
|
||||||
|
zammad_article_type_id,
|
||||||
last_error
|
last_error
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
ON CONFLICT (session_id) WHERE status = 'pending'
|
ON CONFLICT (session_id) WHERE status = 'pending'
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
zammad_ticket_id = COALESCE(EXCLUDED.zammad_ticket_id, zammad_session_article_queue.zammad_ticket_id),
|
zammad_ticket_id = COALESCE(EXCLUDED.zammad_ticket_id, zammad_session_article_queue.zammad_ticket_id),
|
||||||
activity = EXCLUDED.activity,
|
activity = EXCLUDED.activity,
|
||||||
time_unit = EXCLUDED.time_unit,
|
time_unit = EXCLUDED.time_unit,
|
||||||
|
zammad_article_type_id = EXCLUDED.zammad_article_type_id,
|
||||||
last_error = EXCLUDED.last_error,
|
last_error = EXCLUDED.last_error,
|
||||||
updated_at = now();
|
updated_at = now();
|
||||||
`,
|
`,
|
||||||
@@ -703,11 +723,55 @@ async function enqueueZammadSessionArticle(client: pg.PoolClient, input: {
|
|||||||
input.zammadTicketId,
|
input.zammadTicketId,
|
||||||
input.activity,
|
input.activity,
|
||||||
input.timeUnit,
|
input.timeUnit,
|
||||||
|
input.articleTypeId,
|
||||||
input.error.slice(0, 1000)
|
input.error.slice(0, 1000)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function recordZammadSessionArticleSent(client: pg.PoolClient, input: {
|
||||||
|
userId: string;
|
||||||
|
sessionId: string | number;
|
||||||
|
ticketId: string | number;
|
||||||
|
ticketNumber: string;
|
||||||
|
zammadTicketId: string | number;
|
||||||
|
activity: string;
|
||||||
|
timeUnit: number;
|
||||||
|
articleTypeId: number;
|
||||||
|
articleId: number | null;
|
||||||
|
}) {
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO zammad_session_article_queue (
|
||||||
|
user_id,
|
||||||
|
session_id,
|
||||||
|
ticket_id,
|
||||||
|
ticket_number,
|
||||||
|
zammad_ticket_id,
|
||||||
|
activity,
|
||||||
|
time_unit,
|
||||||
|
zammad_article_type_id,
|
||||||
|
status,
|
||||||
|
sent_at,
|
||||||
|
zammad_article_id,
|
||||||
|
last_error
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'sent', now(), $9, NULL);
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.userId,
|
||||||
|
input.sessionId,
|
||||||
|
input.ticketId,
|
||||||
|
input.ticketNumber,
|
||||||
|
input.zammadTicketId,
|
||||||
|
input.activity,
|
||||||
|
input.timeUnit,
|
||||||
|
input.articleTypeId,
|
||||||
|
input.articleId
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function zammadArticleId(payload: unknown) {
|
function zammadArticleId(payload: unknown) {
|
||||||
return isPlainObject(payload) ? numberOrNull(payload.id) : null;
|
return isPlainObject(payload) ? numberOrNull(payload.id) : null;
|
||||||
}
|
}
|
||||||
@@ -721,6 +785,7 @@ type ZammadSessionArticleQueueRow = {
|
|||||||
zammad_ticket_id: string | null;
|
zammad_ticket_id: string | null;
|
||||||
activity: string;
|
activity: string;
|
||||||
time_unit: string | number;
|
time_unit: string | number;
|
||||||
|
zammad_article_type_id: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function retryZammadSessionArticleQueueRow(row: ZammadSessionArticleQueueRow) {
|
async function retryZammadSessionArticleQueueRow(row: ZammadSessionArticleQueueRow) {
|
||||||
@@ -750,7 +815,8 @@ async function retryZammadSessionArticleQueueRow(row: ZammadSessionArticleQueueR
|
|||||||
const payload = zammadSessionArticlePayload({
|
const payload = zammadSessionArticlePayload({
|
||||||
zammadTicketId,
|
zammadTicketId,
|
||||||
activity: row.activity,
|
activity: row.activity,
|
||||||
timeUnit: Number(row.time_unit)
|
timeUnit: Number(row.time_unit),
|
||||||
|
articleTypeId: Number(row.zammad_article_type_id ?? 6)
|
||||||
});
|
});
|
||||||
const zammadResponse = await postZammadSessionArticleForUser(row.user_id, payload);
|
const zammadResponse = await postZammadSessionArticleForUser(row.user_id, payload);
|
||||||
const articleId = zammadArticleId(zammadResponse);
|
const articleId = zammadArticleId(zammadResponse);
|
||||||
@@ -1013,6 +1079,7 @@ type SessionCreationInput = {
|
|||||||
requestedProjectTeamspaceEntryId: number | null;
|
requestedProjectTeamspaceEntryId: number | null;
|
||||||
billingStatus: "billable" | "billed" | "non_billable" | null;
|
billingStatus: "billable" | "billed" | "non_billable" | null;
|
||||||
createZammadArticle: boolean;
|
createZammadArticle: boolean;
|
||||||
|
zammadArticleTypeId: number;
|
||||||
startedAt: Date;
|
startedAt: Date;
|
||||||
endedAt: Date;
|
endedAt: Date;
|
||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
@@ -1034,6 +1101,7 @@ async function createSessionForUser(input: SessionCreationInput) {
|
|||||||
requestedProjectTeamspaceEntryId,
|
requestedProjectTeamspaceEntryId,
|
||||||
billingStatus,
|
billingStatus,
|
||||||
createZammadArticle,
|
createZammadArticle,
|
||||||
|
zammadArticleTypeId,
|
||||||
startedAt,
|
startedAt,
|
||||||
endedAt,
|
endedAt,
|
||||||
durationSeconds,
|
durationSeconds,
|
||||||
@@ -1251,6 +1319,18 @@ async function createSessionForUser(input: SessionCreationInput) {
|
|||||||
);
|
);
|
||||||
const session = sessionResult.rows[0];
|
const session = sessionResult.rows[0];
|
||||||
|
|
||||||
|
const zammadArticle: {
|
||||||
|
requested: boolean;
|
||||||
|
status: "not_requested" | "sent" | "queued";
|
||||||
|
error: string | null;
|
||||||
|
articleId: number | null;
|
||||||
|
} = {
|
||||||
|
requested: createZammadArticle,
|
||||||
|
status: createZammadArticle ? "queued" : "not_requested",
|
||||||
|
error: null,
|
||||||
|
articleId: null
|
||||||
|
};
|
||||||
|
|
||||||
if (createZammadArticle) {
|
if (createZammadArticle) {
|
||||||
const timeUnit = await zammadTimeUnitForUser(userId, durationSeconds);
|
const timeUnit = await zammadTimeUnitForUser(userId, durationSeconds);
|
||||||
let zammadArticleTicketId = ticket.zammad_ticket_id;
|
let zammadArticleTicketId = ticket.zammad_ticket_id;
|
||||||
@@ -1276,9 +1356,23 @@ async function createSessionForUser(input: SessionCreationInput) {
|
|||||||
const payload = zammadSessionArticlePayload({
|
const payload = zammadSessionArticlePayload({
|
||||||
zammadTicketId: zammadArticleTicketId,
|
zammadTicketId: zammadArticleTicketId,
|
||||||
activity,
|
activity,
|
||||||
timeUnit
|
timeUnit,
|
||||||
|
articleTypeId: zammadArticleTypeId
|
||||||
|
});
|
||||||
|
const zammadResponse = await postZammadSessionArticleForUser(userId, payload);
|
||||||
|
zammadArticle.status = "sent";
|
||||||
|
zammadArticle.articleId = zammadArticleId(zammadResponse);
|
||||||
|
await recordZammadSessionArticleSent(client, {
|
||||||
|
userId,
|
||||||
|
sessionId: session.id,
|
||||||
|
ticketId: ticket.id,
|
||||||
|
ticketNumber,
|
||||||
|
zammadTicketId: zammadArticleTicketId,
|
||||||
|
activity,
|
||||||
|
timeUnit,
|
||||||
|
articleTypeId: zammadArticleTypeId,
|
||||||
|
articleId: zammadArticle.articleId
|
||||||
});
|
});
|
||||||
await postZammadSessionArticleForUser(userId, payload);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
zammadArticleError = errorMessage(error);
|
zammadArticleError = errorMessage(error);
|
||||||
}
|
}
|
||||||
@@ -1287,6 +1381,8 @@ async function createSessionForUser(input: SessionCreationInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (zammadArticleError) {
|
if (zammadArticleError) {
|
||||||
|
zammadArticle.status = "queued";
|
||||||
|
zammadArticle.error = zammadArticleError;
|
||||||
await enqueueZammadSessionArticle(client, {
|
await enqueueZammadSessionArticle(client, {
|
||||||
userId,
|
userId,
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
@@ -1295,6 +1391,7 @@ async function createSessionForUser(input: SessionCreationInput) {
|
|||||||
zammadTicketId: zammadArticleTicketId,
|
zammadTicketId: zammadArticleTicketId,
|
||||||
activity,
|
activity,
|
||||||
timeUnit,
|
timeUnit,
|
||||||
|
articleTypeId: zammadArticleTypeId,
|
||||||
error: zammadArticleError
|
error: zammadArticleError
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1319,7 +1416,8 @@ async function createSessionForUser(input: SessionCreationInput) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ticket,
|
ticket,
|
||||||
session
|
session,
|
||||||
|
zammadArticle
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1675,6 +1773,7 @@ async function createZammadEmailSessionFromWebhook(payload: unknown, deliveryId:
|
|||||||
requestedProjectTeamspaceEntryId: null,
|
requestedProjectTeamspaceEntryId: null,
|
||||||
billingStatus: null,
|
billingStatus: null,
|
||||||
createZammadArticle: false,
|
createZammadArticle: false,
|
||||||
|
zammadArticleTypeId: 6,
|
||||||
startedAt,
|
startedAt,
|
||||||
endedAt,
|
endedAt,
|
||||||
durationSeconds,
|
durationSeconds,
|
||||||
@@ -4433,6 +4532,27 @@ async function getPeriodTicket(config: PeriodConfig, period: ParsedPeriod, ticke
|
|||||||
recurring_billing_id,
|
recurring_billing_id,
|
||||||
recurring_billing_slot_id,
|
recurring_billing_slot_id,
|
||||||
recurring_occurrence_date,
|
recurring_occurrence_date,
|
||||||
|
(
|
||||||
|
SELECT status
|
||||||
|
FROM zammad_session_article_queue
|
||||||
|
WHERE session_id = sessions.id
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS zammad_article_status,
|
||||||
|
(
|
||||||
|
SELECT zammad_article_id::text
|
||||||
|
FROM zammad_session_article_queue
|
||||||
|
WHERE session_id = sessions.id
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS zammad_article_id,
|
||||||
|
(
|
||||||
|
SELECT zammad_article_type_id
|
||||||
|
FROM zammad_session_article_queue
|
||||||
|
WHERE session_id = sessions.id
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS zammad_article_type_id,
|
||||||
project_teamspace_entry_id::text AS project_teamspace_entry_id,
|
project_teamspace_entry_id::text AS project_teamspace_entry_id,
|
||||||
(
|
(
|
||||||
SELECT teamspace_project
|
SELECT teamspace_project
|
||||||
@@ -4591,10 +4711,20 @@ async function getTicketDetails(ticketId: number, userId: string, filters: { mon
|
|||||||
s.recurring_billing_id,
|
s.recurring_billing_id,
|
||||||
s.recurring_billing_slot_id,
|
s.recurring_billing_slot_id,
|
||||||
s.recurring_occurrence_date,
|
s.recurring_occurrence_date,
|
||||||
|
za.status AS zammad_article_status,
|
||||||
|
za.zammad_article_id::text AS zammad_article_id,
|
||||||
|
za.zammad_article_type_id,
|
||||||
s.project_teamspace_entry_id::text AS project_teamspace_entry_id,
|
s.project_teamspace_entry_id::text AS project_teamspace_entry_id,
|
||||||
pte.teamspace_project AS project_teamspace_project
|
pte.teamspace_project AS project_teamspace_project
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
LEFT JOIN project_teamspace_entries pte ON pte.id = s.project_teamspace_entry_id
|
LEFT JOIN project_teamspace_entries pte ON pte.id = s.project_teamspace_entry_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT status, zammad_article_id, zammad_article_type_id
|
||||||
|
FROM zammad_session_article_queue
|
||||||
|
WHERE session_id = s.id
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
) za ON true
|
||||||
WHERE ${sessionWhere}
|
WHERE ${sessionWhere}
|
||||||
ORDER BY s.started_at ASC;
|
ORDER BY s.started_at ASC;
|
||||||
`,
|
`,
|
||||||
@@ -4841,11 +4971,12 @@ app.post("/api/setup/admin", async (req, res) => {
|
|||||||
zammad_session_note_default_enabled: boolean;
|
zammad_session_note_default_enabled: boolean;
|
||||||
zammad_time_unit_rounding_enabled: boolean;
|
zammad_time_unit_rounding_enabled: boolean;
|
||||||
zammad_time_unit_rounding_minutes: number;
|
zammad_time_unit_rounding_minutes: number;
|
||||||
|
zammad_article_type_id: number | null;
|
||||||
}>(
|
}>(
|
||||||
`
|
`
|
||||||
INSERT INTO users (username, display_name, password_hash, role, active)
|
INSERT INTO users (username, display_name, password_hash, role, active)
|
||||||
VALUES ($1, $2, $3, 'admin', true)
|
VALUES ($1, $2, $3, 'admin', true)
|
||||||
RETURNING id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes;
|
RETURNING id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes, zammad_article_type_id;
|
||||||
`,
|
`,
|
||||||
[username, displayName, await hashPassword(password)]
|
[username, displayName, await hashPassword(password)]
|
||||||
);
|
);
|
||||||
@@ -4906,6 +5037,7 @@ app.post("/api/auth/login", async (req, res) => {
|
|||||||
zammad_session_note_default_enabled: boolean;
|
zammad_session_note_default_enabled: boolean;
|
||||||
zammad_time_unit_rounding_enabled: boolean;
|
zammad_time_unit_rounding_enabled: boolean;
|
||||||
zammad_time_unit_rounding_minutes: number;
|
zammad_time_unit_rounding_minutes: number;
|
||||||
|
zammad_article_type_id: number | null;
|
||||||
}>(
|
}>(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -4921,7 +5053,8 @@ app.post("/api/auth/login", async (req, res) => {
|
|||||||
session_billing_prompt_enabled,
|
session_billing_prompt_enabled,
|
||||||
zammad_session_note_default_enabled,
|
zammad_session_note_default_enabled,
|
||||||
zammad_time_unit_rounding_enabled,
|
zammad_time_unit_rounding_enabled,
|
||||||
zammad_time_unit_rounding_minutes
|
zammad_time_unit_rounding_minutes,
|
||||||
|
zammad_article_type_id
|
||||||
FROM users
|
FROM users
|
||||||
WHERE lower(username) = lower($1);
|
WHERE lower(username) = lower($1);
|
||||||
`,
|
`,
|
||||||
@@ -4971,7 +5104,8 @@ app.post("/api/auth/login", async (req, res) => {
|
|||||||
session_billing_prompt_enabled: user.session_billing_prompt_enabled,
|
session_billing_prompt_enabled: user.session_billing_prompt_enabled,
|
||||||
zammad_session_note_default_enabled: user.zammad_session_note_default_enabled,
|
zammad_session_note_default_enabled: user.zammad_session_note_default_enabled,
|
||||||
zammad_time_unit_rounding_enabled: user.zammad_time_unit_rounding_enabled,
|
zammad_time_unit_rounding_enabled: user.zammad_time_unit_rounding_enabled,
|
||||||
zammad_time_unit_rounding_minutes: user.zammad_time_unit_rounding_minutes
|
zammad_time_unit_rounding_minutes: user.zammad_time_unit_rounding_minutes,
|
||||||
|
zammad_article_type_id: user.zammad_article_type_id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -5189,6 +5323,9 @@ app.patch("/api/auth/me", async (req, res) => {
|
|||||||
const zammadTimeUnitRoundingMinutes = user.role === "user"
|
const zammadTimeUnitRoundingMinutes = user.role === "user"
|
||||||
? parseZammadTimeUnitRoundingMinutes(req.body.zammadTimeUnitRoundingMinutes, user.zammad_time_unit_rounding_minutes)
|
? parseZammadTimeUnitRoundingMinutes(req.body.zammadTimeUnitRoundingMinutes, user.zammad_time_unit_rounding_minutes)
|
||||||
: 15;
|
: 15;
|
||||||
|
const zammadArticleTypeId = user.role === "user"
|
||||||
|
? parseZammadArticleTypeId(req.body.zammadArticleTypeId, user.zammad_article_type_id)
|
||||||
|
: 6;
|
||||||
|
|
||||||
if (newPassword && newPassword.length < 6) {
|
if (newPassword && newPassword.length < 6) {
|
||||||
throw badRequest("newPassword must be at least 6 characters");
|
throw badRequest("newPassword must be at least 6 characters");
|
||||||
@@ -5221,9 +5358,10 @@ app.patch("/api/auth/me", async (req, res) => {
|
|||||||
zammad_session_note_default_enabled = CASE WHEN $8::boolean THEN $11 ELSE zammad_session_note_default_enabled END,
|
zammad_session_note_default_enabled = CASE WHEN $8::boolean THEN $11 ELSE zammad_session_note_default_enabled END,
|
||||||
zammad_time_unit_rounding_enabled = CASE WHEN $8::boolean THEN $12 ELSE zammad_time_unit_rounding_enabled END,
|
zammad_time_unit_rounding_enabled = CASE WHEN $8::boolean THEN $12 ELSE zammad_time_unit_rounding_enabled END,
|
||||||
zammad_time_unit_rounding_minutes = CASE WHEN $8::boolean THEN $13 ELSE zammad_time_unit_rounding_minutes END,
|
zammad_time_unit_rounding_minutes = CASE WHEN $8::boolean THEN $13 ELSE zammad_time_unit_rounding_minutes END,
|
||||||
|
zammad_article_type_id = CASE WHEN $8::boolean THEN $14 ELSE zammad_article_type_id END,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $4
|
WHERE id = $4
|
||||||
RETURNING id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes;
|
RETURNING id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes, zammad_article_type_id;
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
username,
|
username,
|
||||||
@@ -5238,7 +5376,8 @@ app.patch("/api/auth/me", async (req, res) => {
|
|||||||
sessionBillingPromptEnabled,
|
sessionBillingPromptEnabled,
|
||||||
zammadSessionNoteDefaultEnabled,
|
zammadSessionNoteDefaultEnabled,
|
||||||
zammadTimeUnitRoundingEnabled,
|
zammadTimeUnitRoundingEnabled,
|
||||||
zammadTimeUnitRoundingMinutes
|
zammadTimeUnitRoundingMinutes,
|
||||||
|
zammadArticleTypeId
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5266,7 +5405,7 @@ app.patch("/api/auth/me", async (req, res) => {
|
|||||||
app.get("/api/admin/users", requireAdmin, async (_req, res) => {
|
app.get("/api/admin/users", requireAdmin, async (_req, res) => {
|
||||||
const result = await query(
|
const result = await query(
|
||||||
`
|
`
|
||||||
SELECT id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes, created_at, updated_at
|
SELECT id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes, zammad_article_type_id, created_at, updated_at
|
||||||
FROM users
|
FROM users
|
||||||
ORDER BY username ASC;
|
ORDER BY username ASC;
|
||||||
`
|
`
|
||||||
@@ -5290,7 +5429,7 @@ app.post("/api/admin/users", requireAdmin, async (req, res) => {
|
|||||||
`
|
`
|
||||||
INSERT INTO users (username, display_name, password_hash, role, active)
|
INSERT INTO users (username, display_name, password_hash, role, active)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes, created_at, updated_at;
|
RETURNING id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes, zammad_article_type_id, created_at, updated_at;
|
||||||
`,
|
`,
|
||||||
[username, displayName, await hashPassword(password), "user", active]
|
[username, displayName, await hashPassword(password), "user", active]
|
||||||
);
|
);
|
||||||
@@ -5351,7 +5490,7 @@ app.patch("/api/admin/users/:userId", requireAdmin, async (req, res) => {
|
|||||||
password_hash = COALESCE($4, password_hash),
|
password_hash = COALESCE($4, password_hash),
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $5
|
WHERE id = $5
|
||||||
RETURNING id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes, created_at, updated_at;
|
RETURNING id, username, display_name, role, active, zammad_user_id::text AS zammad_user_id, (zammad_api_token IS NOT NULL) AS has_zammad_api_token, zammad_email_session_webhook_enabled, session_billing_prompt_enabled, zammad_session_note_default_enabled, zammad_time_unit_rounding_enabled, zammad_time_unit_rounding_minutes, zammad_article_type_id, created_at, updated_at;
|
||||||
`,
|
`,
|
||||||
[username, displayName, active, password ? await hashPassword(password) : null, userId]
|
[username, displayName, active, password ? await hashPassword(password) : null, userId]
|
||||||
);
|
);
|
||||||
@@ -6329,6 +6468,7 @@ app.post("/api/sessions", requireUser, async (req, res) => {
|
|||||||
const requestedProjectTeamspaceEntryId = parseOptionalTeamspaceEntryId(req.body.projectTeamspaceEntryId);
|
const requestedProjectTeamspaceEntryId = parseOptionalTeamspaceEntryId(req.body.projectTeamspaceEntryId);
|
||||||
const billingStatus = req.body.billingStatus === undefined ? null : parseBillingStatus(req.body.billingStatus);
|
const billingStatus = req.body.billingStatus === undefined ? null : parseBillingStatus(req.body.billingStatus);
|
||||||
const createZammadArticle = optionalBoolean(req.body.createZammadArticle, false);
|
const createZammadArticle = optionalBoolean(req.body.createZammadArticle, false);
|
||||||
|
const zammadArticleTypeId = parseZammadArticleTypeId(req.body.zammadArticleTypeId, user.zammad_article_type_id);
|
||||||
const startedAt = parseIsoDate(req.body.startedAt, "startedAt");
|
const startedAt = parseIsoDate(req.body.startedAt, "startedAt");
|
||||||
const endedAt = parseIsoDate(req.body.endedAt, "endedAt");
|
const endedAt = parseIsoDate(req.body.endedAt, "endedAt");
|
||||||
const durationSeconds = parsePositiveInteger(req.body.durationSeconds, "durationSeconds");
|
const durationSeconds = parsePositiveInteger(req.body.durationSeconds, "durationSeconds");
|
||||||
@@ -6343,6 +6483,7 @@ app.post("/api/sessions", requireUser, async (req, res) => {
|
|||||||
requestedProjectTeamspaceEntryId,
|
requestedProjectTeamspaceEntryId,
|
||||||
billingStatus,
|
billingStatus,
|
||||||
createZammadArticle,
|
createZammadArticle,
|
||||||
|
zammadArticleTypeId,
|
||||||
startedAt,
|
startedAt,
|
||||||
endedAt,
|
endedAt,
|
||||||
durationSeconds
|
durationSeconds
|
||||||
@@ -6362,6 +6503,7 @@ async function zammadSessionArticleQueueResponse(userId: string) {
|
|||||||
q.zammad_ticket_id::text AS zammad_ticket_id,
|
q.zammad_ticket_id::text AS zammad_ticket_id,
|
||||||
q.activity,
|
q.activity,
|
||||||
q.time_unit::float AS time_unit,
|
q.time_unit::float AS time_unit,
|
||||||
|
q.zammad_article_type_id,
|
||||||
q.attempt_count,
|
q.attempt_count,
|
||||||
q.last_error,
|
q.last_error,
|
||||||
q.last_attempt_at,
|
q.last_attempt_at,
|
||||||
@@ -6398,7 +6540,8 @@ app.post("/api/zammad/session-article-queue/retry", requireUser, async (req, res
|
|||||||
ticket_number,
|
ticket_number,
|
||||||
zammad_ticket_id::text AS zammad_ticket_id,
|
zammad_ticket_id::text AS zammad_ticket_id,
|
||||||
activity,
|
activity,
|
||||||
time_unit::text AS time_unit
|
time_unit::text AS time_unit,
|
||||||
|
zammad_article_type_id
|
||||||
FROM zammad_session_article_queue
|
FROM zammad_session_article_queue
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
AND status = 'pending'
|
AND status = 'pending'
|
||||||
|
|||||||
@@ -91,11 +91,18 @@ export async function migrate() {
|
|||||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_session_note_default_enabled BOOLEAN NOT NULL DEFAULT false;");
|
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_session_note_default_enabled BOOLEAN NOT NULL DEFAULT false;");
|
||||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_time_unit_rounding_enabled BOOLEAN NOT NULL DEFAULT false;");
|
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_time_unit_rounding_enabled BOOLEAN NOT NULL DEFAULT false;");
|
||||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_time_unit_rounding_minutes INTEGER NOT NULL DEFAULT 15;");
|
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_time_unit_rounding_minutes INTEGER NOT NULL DEFAULT 15;");
|
||||||
|
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_article_type_id INTEGER DEFAULT 6;");
|
||||||
|
await query("ALTER TABLE users ALTER COLUMN zammad_article_type_id DROP NOT NULL;");
|
||||||
await query(`
|
await query(`
|
||||||
ALTER TABLE users
|
ALTER TABLE users
|
||||||
DROP CONSTRAINT IF EXISTS users_zammad_time_unit_rounding_minutes_valid,
|
DROP CONSTRAINT IF EXISTS users_zammad_time_unit_rounding_minutes_valid,
|
||||||
ADD CONSTRAINT users_zammad_time_unit_rounding_minutes_valid CHECK (zammad_time_unit_rounding_minutes IN (5, 10, 15, 30));
|
ADD CONSTRAINT users_zammad_time_unit_rounding_minutes_valid CHECK (zammad_time_unit_rounding_minutes IN (5, 10, 15, 30));
|
||||||
`);
|
`);
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE users
|
||||||
|
DROP CONSTRAINT IF EXISTS users_zammad_article_type_id_valid,
|
||||||
|
ADD CONSTRAINT users_zammad_article_type_id_valid CHECK (zammad_article_type_id IS NULL OR zammad_article_type_id IN (1, 2, 3, 4, 5, 6));
|
||||||
|
`);
|
||||||
|
|
||||||
await query(`
|
await query(`
|
||||||
CREATE TABLE IF NOT EXISTS auth_sessions (
|
CREATE TABLE IF NOT EXISTS auth_sessions (
|
||||||
@@ -266,6 +273,7 @@ export async function migrate() {
|
|||||||
zammad_ticket_id BIGINT,
|
zammad_ticket_id BIGINT,
|
||||||
activity TEXT NOT NULL,
|
activity TEXT NOT NULL,
|
||||||
time_unit NUMERIC(10, 2) NOT NULL,
|
time_unit NUMERIC(10, 2) NOT NULL,
|
||||||
|
zammad_article_type_id INTEGER DEFAULT 6,
|
||||||
status TEXT NOT NULL DEFAULT 'pending',
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
attempt_count INTEGER NOT NULL DEFAULT 0,
|
||||||
last_error TEXT,
|
last_error TEXT,
|
||||||
@@ -277,6 +285,13 @@ export async function migrate() {
|
|||||||
CONSTRAINT zammad_session_article_queue_status_valid CHECK (status IN ('pending', 'sent'))
|
CONSTRAINT zammad_session_article_queue_status_valid CHECK (status IN ('pending', 'sent'))
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
await query("ALTER TABLE zammad_session_article_queue ADD COLUMN IF NOT EXISTS zammad_article_type_id INTEGER DEFAULT 6;");
|
||||||
|
await query("ALTER TABLE zammad_session_article_queue ALTER COLUMN zammad_article_type_id DROP NOT NULL;");
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE zammad_session_article_queue
|
||||||
|
DROP CONSTRAINT IF EXISTS zammad_session_article_queue_type_id_valid,
|
||||||
|
ADD CONSTRAINT zammad_session_article_queue_type_id_valid CHECK (zammad_article_type_id IS NULL OR zammad_article_type_id IN (1, 2, 3, 4, 5, 6));
|
||||||
|
`);
|
||||||
await query(`
|
await query(`
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_zammad_session_article_queue_pending_session
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_zammad_session_article_queue_pending_session
|
||||||
ON zammad_session_article_queue(session_id)
|
ON zammad_session_article_queue(session_id)
|
||||||
|
|||||||
+6
-3
@@ -84,7 +84,7 @@ Beim Klick auf `Beenden` stoppt der Timer sofort. Die vorher im Timer erfasste T
|
|||||||
|
|
||||||
Die Bewertung hat drei Optionen: `---`, `Abrechenbar` und `Nicht abrechenbar`. `---` ist der Standard und laesst die Session offen. Wenn im Profil die Bewertungsabfrage aktiv ist, fragt TicketTracker beim Beenden eines Timers mit `---` noch einmal nach.
|
Die Bewertung hat drei Optionen: `---`, `Abrechenbar` und `Nicht abrechenbar`. `---` ist der Standard und laesst die Session offen. Wenn im Profil die Bewertungsabfrage aktiv ist, fragt TicketTracker beim Beenden eines Timers mit `---` noch einmal nach.
|
||||||
|
|
||||||
Im Abschlussdialog kann die Taetigkeit als interner Zammad-Article an das Ticket uebertragen werden. Der Article wird mit `internal: true` angelegt und enthaelt die Taetigkeit sowie den `time_unit`-Wert aus der gemessenen Zeit abzueglich Pausen.
|
Im Abschlussdialog kann die Taetigkeit als interner Zammad-Article an das Ticket uebertragen werden. Der Article wird mit `internal: true` angelegt und enthaelt die Taetigkeit sowie den `time_unit`-Wert aus der gemessenen Zeit abzueglich Pausen. Wenn die interne Zammad-Notiz aktiv ist, kann dort auch der Zammad-Zeitbuchungstyp gewaehlt werden.
|
||||||
|
|
||||||
Wenn Zammad beim Speichern nicht erreichbar ist, wird die Session trotzdem in TicketTracker gespeichert. Der Zammad-Article wird dann in einer Parktabelle vorgemerkt und kann spaeter im Profil erneut gesendet werden.
|
Wenn Zammad beim Speichern nicht erreichbar ist, wird die Session trotzdem in TicketTracker gespeichert. Der Zammad-Article wird dann in einer Parktabelle vorgemerkt und kann spaeter im Profil erneut gesendet werden.
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ Das Taetigkeitsfeld ist mehrzeilig. Zeilenumbrueche bleiben in der Auswertung er
|
|||||||
|
|
||||||
Mit Bewertung `---` bleibt eine nachgetragene Session offen. `Abrechenbar` und `Nicht abrechenbar` werden direkt an der neuen Session gespeichert.
|
Mit Bewertung `---` bleibt eine nachgetragene Session offen. `Abrechenbar` und `Nicht abrechenbar` werden direkt an der neuen Session gespeichert.
|
||||||
|
|
||||||
Wenn die interne Zammad-Notiz aktiviert ist, wird die Taetigkeit auch beim Nachtragen als interner Zammad-Article uebertragen. Bei Zammad-Fehlern wird der Eintrag wie beim Timerabschluss geparkt und kann spaeter im Profil gesendet werden.
|
Wenn die interne Zammad-Notiz aktiviert ist, wird die Taetigkeit auch beim Nachtragen als interner Zammad-Article uebertragen. Der Zammad-Typ kann dabei angepasst werden. Bei Zammad-Fehlern wird der Eintrag wie beim Timerabschluss geparkt und kann spaeter im Profil gesendet werden.
|
||||||
|
|
||||||
Das Ende muss nach dem Beginn liegen. Die Dauer wird automatisch berechnet und auf Minuten gerundet.
|
Das Ende muss nach dem Beginn liegen. Die Dauer wird automatisch berechnet und auf Minuten gerundet.
|
||||||
|
|
||||||
@@ -534,6 +534,7 @@ Unter `Profil` kann jeder Benutzer seine eigenen Daten pflegen:
|
|||||||
- persoenlicher Zammad API-Token
|
- persoenlicher Zammad API-Token
|
||||||
- Bewertungsabfrage beim Timerabschluss
|
- Bewertungsabfrage beim Timerabschluss
|
||||||
- interne Zammad-Notiz beim Timerabschluss
|
- interne Zammad-Notiz beim Timerabschluss
|
||||||
|
- Standard-Typ fuer interne Zammad-Notizen
|
||||||
- Zammad-Zeitaufrundung fuer `time_unit`
|
- Zammad-Zeitaufrundung fuer `time_unit`
|
||||||
|
|
||||||
Passwortaenderungen erfordern das aktuelle Passwort. Bleiben die Passwortfelder leer, werden nur Benutzername und voller Name gespeichert.
|
Passwortaenderungen erfordern das aktuelle Passwort. Bleiben die Passwortfelder leer, werden nur Benutzername und voller Name gespeichert.
|
||||||
@@ -546,7 +547,9 @@ Der persoenliche Zammad Token ist unabhaengig vom Admin-Token fuer den Organisat
|
|||||||
|
|
||||||
Die Bewertungsabfrage beim Timerabschluss kann im Profil ein- oder ausgeschaltet werden. Sie erscheint nur, wenn der Timer beim Beenden noch auf Bewertung `---` steht.
|
Die Bewertungsabfrage beim Timerabschluss kann im Profil ein- oder ausgeschaltet werden. Sie erscheint nur, wenn der Timer beim Beenden noch auf Bewertung `---` steht.
|
||||||
|
|
||||||
Die interne Zammad-Notiz beim Timerabschluss steuert, ob die Checkbox im Abschlussdialog standardmaessig aktiv ist. Ist sie aktiv, legt TicketTracker beim Speichern einen internen Zammad-Article zum Ticket an.
|
Die interne Zammad-Notiz beim Timerabschluss steuert, ob die Checkbox im Abschlussdialog standardmaessig aktiv ist. Ist sie aktiv, legt TicketTracker beim Speichern einen internen Zammad-Article zum Ticket an. Nur dann werden auch der Standard-Typ und die Zeitaufrundung im Profil angezeigt.
|
||||||
|
|
||||||
|
Der Standard-Typ belegt den Zammad-Zeitbuchungstyp fuer neue interne Notizen vor. Beim Timerabschluss und beim Nachtragen kann der Typ vor dem Speichern noch geaendert werden.
|
||||||
|
|
||||||
Fuer `time_unit` kann optional eine Aufrundung aktiviert werden. Dann wird die tatsaechlich getrackte Zeit immer auf 5, 10, 15 oder 30 Minuten aufgerundet und als Stundenwert an Zammad gesendet. Ohne Rundung wird der konkrete Stundenwert mit zwei Nachkommastellen gesendet.
|
Fuer `time_unit` kann optional eine Aufrundung aktiviert werden. Dann wird die tatsaechlich getrackte Zeit immer auf 5, 10, 15 oder 30 Minuten aufgerundet und als Stundenwert an Zammad gesendet. Ohne Rundung wird der konkrete Stundenwert mit zwei Nachkommastellen gesendet.
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -32,6 +32,7 @@ type CreateSessionPayload = {
|
|||||||
projectTeamspaceEntryId?: string | null;
|
projectTeamspaceEntryId?: string | null;
|
||||||
billingStatus?: BillingStatus;
|
billingStatus?: BillingStatus;
|
||||||
createZammadArticle?: boolean;
|
createZammadArticle?: boolean;
|
||||||
|
zammadArticleTypeId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeleteSessionResult = {
|
type DeleteSessionResult = {
|
||||||
@@ -46,6 +47,15 @@ type DeleteSessionResult = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateSessionResult = {
|
||||||
|
zammadArticle?: {
|
||||||
|
requested: boolean;
|
||||||
|
status: "not_requested" | "sent" | "queued";
|
||||||
|
error: string | null;
|
||||||
|
articleId: number | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type ZammadSyncExistingFields = {
|
export type ZammadSyncExistingFields = {
|
||||||
title: boolean;
|
title: boolean;
|
||||||
organization: boolean;
|
organization: boolean;
|
||||||
@@ -205,6 +215,7 @@ export function updateCurrentUser(payload: {
|
|||||||
zammadSessionNoteDefaultEnabled?: boolean;
|
zammadSessionNoteDefaultEnabled?: boolean;
|
||||||
zammadTimeUnitRoundingEnabled?: boolean;
|
zammadTimeUnitRoundingEnabled?: boolean;
|
||||||
zammadTimeUnitRoundingMinutes?: number;
|
zammadTimeUnitRoundingMinutes?: number;
|
||||||
|
zammadArticleTypeId?: number;
|
||||||
}) {
|
}) {
|
||||||
return request<{ user: AuthUser }>("/api/auth/me", {
|
return request<{ user: AuthUser }>("/api/auth/me", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@@ -682,7 +693,7 @@ export function deleteTodo(todoId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createSession(payload: CreateSessionPayload) {
|
export function createSession(payload: CreateSessionPayload) {
|
||||||
return request("/api/sessions", {
|
return request<CreateSessionResult>("/api/sessions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const helpSections: HelpSection[] = [
|
|||||||
"Wenn du einen Timer auswählst, zeigt die Timerseite dessen Details inklusive gemessener Zeit und Tätigkeitsfeld.",
|
"Wenn du einen Timer auswählst, zeigt die Timerseite dessen Details inklusive gemessener Zeit und Tätigkeitsfeld.",
|
||||||
"Pausierte Timer behalten ihre Tätigkeit. Entfernst du einen Timer, wird auch die noch nicht gespeicherte Tätigkeit verworfen.",
|
"Pausierte Timer behalten ihre Tätigkeit. Entfernst du einen Timer, wird auch die noch nicht gespeicherte Tätigkeit verworfen.",
|
||||||
"Beim Klick auf Beenden wird die Stoppuhr sofort angehalten. Die bereits im Timer erfasste Tätigkeit wird beim Speichern an die Session gehängt.",
|
"Beim Klick auf Beenden wird die Stoppuhr sofort angehalten. Die bereits im Timer erfasste Tätigkeit wird beim Speichern an die Session gehängt.",
|
||||||
"Im Abschlussdialog kann die Tätigkeit als interne Zammad-Notiz übertragen werden. Der Article wird intern erstellt und bekommt die getrackte Zeit als time_unit.",
|
"Im Abschlussdialog kann die Tätigkeit als interne Zammad-Notiz übertragen werden. Der Article wird intern erstellt und bekommt die getrackte Zeit als time_unit sowie den gewählten Zammad-Typ.",
|
||||||
"Wenn Zammad beim Speichern nicht erreichbar ist, bleibt die Session trotzdem gespeichert. Die interne Notiz wird geparkt und kann später im Profil erneut gesendet werden.",
|
"Wenn Zammad beim Speichern nicht erreichbar ist, bleibt die Session trotzdem gespeichert. Die interne Notiz wird geparkt und kann später im Profil erneut gesendet werden.",
|
||||||
"Existiert das Ticket bereits, übernimmt TicketTracker Organisation und Art.",
|
"Existiert das Ticket bereits, übernimmt TicketTracker Organisation und Art.",
|
||||||
"Bei Projekt-Tickets mit mehreren Teamspace-Einträgen zeigt der Beenden-Dialog eine Auswahl. Der Standard-Eintrag ist dort mit Standard gekennzeichnet.",
|
"Bei Projekt-Tickets mit mehreren Teamspace-Einträgen zeigt der Beenden-Dialog eine Auswahl. Der Standard-Eintrag ist dort mit Standard gekennzeichnet.",
|
||||||
@@ -146,7 +146,7 @@ export const helpSections: HelpSection[] = [
|
|||||||
"Die Organisation muss aus der synchronisierten Organisationsliste gewählt werden. Freitext ist absichtlich gesperrt.",
|
"Die Organisation muss aus der synchronisierten Organisationsliste gewählt werden. Freitext ist absichtlich gesperrt.",
|
||||||
"Die Tätigkeit ist eine Textarea. Zeilenumbrüche bleiben erhalten und werden in der Ticketdetailansicht wieder ausgegeben.",
|
"Die Tätigkeit ist eine Textarea. Zeilenumbrüche bleiben erhalten und werden in der Ticketdetailansicht wieder ausgegeben.",
|
||||||
"Mit Bewertung --- bleibt die Session offen. Abrechenbar oder Nicht abrechenbar werden direkt an der neuen Session gespeichert.",
|
"Mit Bewertung --- bleibt die Session offen. Abrechenbar oder Nicht abrechenbar werden direkt an der neuen Session gespeichert.",
|
||||||
"Die interne Zammad-Notiz kann auch beim Nachtragen gesetzt werden. Bei Zammad-Fehlern wird sie geparkt und kann später im Profil gesendet werden.",
|
"Die interne Zammad-Notiz kann auch beim Nachtragen gesetzt werden. Ist sie aktiv, kann der Zammad-Typ angepasst werden. Bei Zammad-Fehlern wird sie geparkt und kann später im Profil gesendet werden.",
|
||||||
"In der Ticketdetailansicht kann eine Session direkt zum geöffneten Ticket nachgetragen werden. Ticketnummer und Stammdaten sind dort schon vorbelegt beziehungsweise aus dem Ticket abgeleitet.",
|
"In der Ticketdetailansicht kann eine Session direkt zum geöffneten Ticket nachgetragen werden. Ticketnummer und Stammdaten sind dort schon vorbelegt beziehungsweise aus dem Ticket abgeleitet.",
|
||||||
"Bei Consulting-Projekten mit mehreren Teamspace-Einträgen ist der passende Teamspace-Eintrag Pflicht. Bei nur einem Eintrag wird er automatisch gesetzt."
|
"Bei Consulting-Projekten mit mehreren Teamspace-Einträgen ist der passende Teamspace-Eintrag Pflicht. Bei nur einem Eintrag wird er automatisch gesetzt."
|
||||||
]
|
]
|
||||||
@@ -539,7 +539,8 @@ export const helpSections: HelpSection[] = [
|
|||||||
"Die Token-Prüfung läuft nur, wenn der Token im Profil geändert oder entfernt wird. Normale Profiländerungen prüfen Zammad nicht erneut.",
|
"Die Token-Prüfung läuft nur, wenn der Token im Profil geändert oder entfernt wird. Normale Profiländerungen prüfen Zammad nicht erneut.",
|
||||||
"Der persönliche Zammad Token ist vom Admin-Token für den Organisations-Sync getrennt. Er wird für ticketbezogene Zammad-Abgleiche, Consulting-Importe und die Zuordnung zum Zammad-Owner verwendet.",
|
"Der persönliche Zammad Token ist vom Admin-Token für den Organisations-Sync getrennt. Er wird für ticketbezogene Zammad-Abgleiche, Consulting-Importe und die Zuordnung zum Zammad-Owner verwendet.",
|
||||||
"Die Bewertungsabfrage beim Timerabschluss kann im Profil ein- oder ausgeschaltet werden. Sie erscheint nur, wenn der Timer noch auf --- steht.",
|
"Die Bewertungsabfrage beim Timerabschluss kann im Profil ein- oder ausgeschaltet werden. Sie erscheint nur, wenn der Timer noch auf --- steht.",
|
||||||
"Die interne Zammad-Notiz beim Timerabschluss steuert, ob die Checkbox im Abschlussdialog standardmäßig aktiv ist.",
|
"Die interne Zammad-Notiz beim Timerabschluss steuert, ob die Checkbox im Abschlussdialog standardmäßig aktiv ist. Nur dann werden Standard-Typ und Zeitaufrundung angezeigt.",
|
||||||
|
"Der Standard-Typ belegt die Zammad-type_id für interne Notizen vor und kann beim Timerabschluss oder Nachtragen noch geändert werden.",
|
||||||
"Für die an Zammad gesendete time_unit kann optional eine Aufrundung auf 5, 10, 15 oder 30 Minuten aktiviert werden. Ohne Rundung wird die konkrete Zeit mit zwei Nachkommastellen gesendet.",
|
"Für die an Zammad gesendete time_unit kann optional eine Aufrundung auf 5, 10, 15 oder 30 Minuten aktiviert werden. Ohne Rundung wird die konkrete Zeit mit zwei Nachkommastellen gesendet.",
|
||||||
"Geparkte Zammad-Notizen listet interne Notizen, die wegen eines Zammad-Fehlers noch nicht übertragen wurden. Über Senden werden sie gesammelt nach Zammad gepusht.",
|
"Geparkte Zammad-Notizen listet interne Notizen, die wegen eines Zammad-Fehlers noch nicht übertragen wurden. Über Senden werden sie gesammelt nach Zammad gepusht.",
|
||||||
"Im Admin-Profil wird kein persönlicher Zammad-Token angezeigt.",
|
"Im Admin-Profil wird kein persönlicher Zammad-Token angezeigt.",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type AuthUser = {
|
|||||||
zammad_session_note_default_enabled: boolean;
|
zammad_session_note_default_enabled: boolean;
|
||||||
zammad_time_unit_rounding_enabled: boolean;
|
zammad_time_unit_rounding_enabled: boolean;
|
||||||
zammad_time_unit_rounding_minutes: number;
|
zammad_time_unit_rounding_minutes: number;
|
||||||
|
zammad_article_type_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardWidgetType = "dayControl" | "today" | "month" | "openRatings" | "projectBudgets" | "todos" | "teamspace" | "favorites" | "divider" | "statistic" | "notes";
|
export type DashboardWidgetType = "dayControl" | "today" | "month" | "openRatings" | "projectBudgets" | "todos" | "teamspace" | "favorites" | "divider" | "statistic" | "notes";
|
||||||
@@ -75,6 +76,7 @@ export type ZammadSessionArticleQueueItem = {
|
|||||||
zammad_ticket_id: string | null;
|
zammad_ticket_id: string | null;
|
||||||
activity: string;
|
activity: string;
|
||||||
time_unit: number;
|
time_unit: number;
|
||||||
|
zammad_article_type_id: number | null;
|
||||||
attempt_count: number;
|
attempt_count: number;
|
||||||
last_error: string | null;
|
last_error: string | null;
|
||||||
last_attempt_at: string | null;
|
last_attempt_at: string | null;
|
||||||
@@ -338,6 +340,9 @@ export type SessionEntry = {
|
|||||||
recurring_billing_id: string | null;
|
recurring_billing_id: string | null;
|
||||||
recurring_billing_slot_id: string | null;
|
recurring_billing_slot_id: string | null;
|
||||||
recurring_occurrence_date: string | null;
|
recurring_occurrence_date: string | null;
|
||||||
|
zammad_article_status?: "pending" | "sent" | null;
|
||||||
|
zammad_article_id?: string | null;
|
||||||
|
zammad_article_type_id?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TicketPeriod = {
|
export type TicketPeriod = {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { HelpLink } from "@/components/HelpLink";
|
|||||||
import { getZammadSessionArticleQueue, retryZammadSessionArticleQueue, updateCurrentUser } from "../api";
|
import { getZammadSessionArticleQueue, retryZammadSessionArticleQueue, updateCurrentUser } from "../api";
|
||||||
import { formatDateTime, formatMinutes } from "../format";
|
import { formatDateTime, formatMinutes } from "../format";
|
||||||
import type { AuthUser, ZammadSessionArticleQueueItem } from "../types";
|
import type { AuthUser, ZammadSessionArticleQueueItem } from "../types";
|
||||||
|
import { zammadArticleTypeFromValue, zammadArticleTypeLabel, zammadArticleTypeOptions, zammadArticleTypeValue, type ZammadArticleTypeId } from "../zammad-article-types";
|
||||||
|
|
||||||
type ProfilePageProps = {
|
type ProfilePageProps = {
|
||||||
currentUser: AuthUser;
|
currentUser: AuthUser;
|
||||||
@@ -28,6 +29,7 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
|
|||||||
const [zammadSessionNoteDefaultEnabled, setZammadSessionNoteDefaultEnabled] = useState(currentUser.zammad_session_note_default_enabled);
|
const [zammadSessionNoteDefaultEnabled, setZammadSessionNoteDefaultEnabled] = useState(currentUser.zammad_session_note_default_enabled);
|
||||||
const [zammadTimeUnitRoundingEnabled, setZammadTimeUnitRoundingEnabled] = useState(currentUser.zammad_time_unit_rounding_enabled);
|
const [zammadTimeUnitRoundingEnabled, setZammadTimeUnitRoundingEnabled] = useState(currentUser.zammad_time_unit_rounding_enabled);
|
||||||
const [zammadTimeUnitRoundingMinutes, setZammadTimeUnitRoundingMinutes] = useState(currentUser.zammad_time_unit_rounding_minutes);
|
const [zammadTimeUnitRoundingMinutes, setZammadTimeUnitRoundingMinutes] = useState(currentUser.zammad_time_unit_rounding_minutes);
|
||||||
|
const [zammadArticleTypeId, setZammadArticleTypeId] = useState<ZammadArticleTypeId>(currentUser.zammad_article_type_id ?? 6);
|
||||||
const [zammadQueue, setZammadQueue] = useState<ZammadSessionArticleQueueItem[]>([]);
|
const [zammadQueue, setZammadQueue] = useState<ZammadSessionArticleQueueItem[]>([]);
|
||||||
const [loadingZammadQueue, setLoadingZammadQueue] = useState(false);
|
const [loadingZammadQueue, setLoadingZammadQueue] = useState(false);
|
||||||
const [retryingZammadQueue, setRetryingZammadQueue] = useState(false);
|
const [retryingZammadQueue, setRetryingZammadQueue] = useState(false);
|
||||||
@@ -43,6 +45,7 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
|
|||||||
setZammadSessionNoteDefaultEnabled(currentUser.zammad_session_note_default_enabled);
|
setZammadSessionNoteDefaultEnabled(currentUser.zammad_session_note_default_enabled);
|
||||||
setZammadTimeUnitRoundingEnabled(currentUser.zammad_time_unit_rounding_enabled);
|
setZammadTimeUnitRoundingEnabled(currentUser.zammad_time_unit_rounding_enabled);
|
||||||
setZammadTimeUnitRoundingMinutes(currentUser.zammad_time_unit_rounding_minutes);
|
setZammadTimeUnitRoundingMinutes(currentUser.zammad_time_unit_rounding_minutes);
|
||||||
|
setZammadArticleTypeId(currentUser.zammad_article_type_id ?? 6);
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
async function loadZammadQueue() {
|
async function loadZammadQueue() {
|
||||||
@@ -125,7 +128,8 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
|
|||||||
sessionBillingPromptEnabled,
|
sessionBillingPromptEnabled,
|
||||||
zammadSessionNoteDefaultEnabled,
|
zammadSessionNoteDefaultEnabled,
|
||||||
zammadTimeUnitRoundingEnabled,
|
zammadTimeUnitRoundingEnabled,
|
||||||
zammadTimeUnitRoundingMinutes
|
zammadTimeUnitRoundingMinutes,
|
||||||
|
zammadArticleTypeId
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
};
|
};
|
||||||
@@ -334,7 +338,28 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{zammadSessionNoteDefaultEnabled ? (
|
{zammadSessionNoteDefaultEnabled ? (
|
||||||
<div className="mt-3 rounded-md border bg-background p-3">
|
<div className="mt-3 space-y-3">
|
||||||
|
<div className="rounded-md border bg-background p-3">
|
||||||
|
<div className="grid gap-2 sm:max-w-lg">
|
||||||
|
<label className="text-sm font-medium" htmlFor="profile-zammad-article-type">
|
||||||
|
Standard-Typ
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="profile-zammad-article-type"
|
||||||
|
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={zammadArticleTypeValue(zammadArticleTypeId)}
|
||||||
|
onChange={(event) => setZammadArticleTypeId(zammadArticleTypeFromValue(event.currentTarget.value))}
|
||||||
|
>
|
||||||
|
{zammadArticleTypeOptions.map((option) => (
|
||||||
|
<option key={option.value || "empty"} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-muted-foreground">Wird beim Erstellen interner Zammad-Notizen vorbelegt.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border bg-background p-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock3 className="size-4 text-muted-foreground" />
|
<Clock3 className="size-4 text-muted-foreground" />
|
||||||
@@ -370,6 +395,7 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-3 rounded-md border bg-background p-3">
|
<div className="mt-3 rounded-md border bg-background p-3">
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
@@ -401,6 +427,7 @@ export function ProfilePage({ currentUser, onUserUpdated }: ProfilePageProps) {
|
|||||||
<span className="text-muted-foreground">{formatDateTime(item.started_at)}</span>
|
<span className="text-muted-foreground">{formatDateTime(item.started_at)}</span>
|
||||||
<span className="text-muted-foreground">{formatMinutes(item.rounded_minutes)}</span>
|
<span className="text-muted-foreground">{formatMinutes(item.rounded_minutes)}</span>
|
||||||
<span className="text-muted-foreground">time_unit {item.time_unit}</span>
|
<span className="text-muted-foreground">time_unit {item.time_unit}</span>
|
||||||
|
<span className="text-muted-foreground">{zammadArticleTypeLabel(item.zammad_article_type_id)}</span>
|
||||||
</div>
|
</div>
|
||||||
{item.last_error ? <p className="mt-1 line-clamp-2 text-destructive">{item.last_error}</p> : null}
|
{item.last_error ? <p className="mt-1 line-clamp-2 text-destructive">{item.last_error}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Play, Plus, RotateCcw, Save, Star, Trash2 } from "lucide-react";
|
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Copy, EyeOff, FolderKanban, Pencil, Play, Plus, RotateCcw, Save, Send, Star, Trash2 } from "lucide-react";
|
||||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Alert } from "@/components/ui/alert";
|
import { Alert } from "@/components/ui/alert";
|
||||||
@@ -30,10 +30,12 @@ import {
|
|||||||
updateProject,
|
updateProject,
|
||||||
updateProjectStatus
|
updateProjectStatus
|
||||||
} from "../api";
|
} from "../api";
|
||||||
|
import type { CreateSessionResult } from "../api";
|
||||||
import { currentDay, currentMonth, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format";
|
import { currentDay, currentMonth, formatDate, formatDateTime, formatMinutes, formatTimeRange } from "../format";
|
||||||
import { ticketHasRecurringBilling } from "../recurring-billings";
|
import { ticketHasRecurringBilling } from "../recurring-billings";
|
||||||
import type { AuthUser, BillingStatus, PeriodType, ProjectSummary, ProjectTeamspaceEntry, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
|
import type { AuthUser, BillingStatus, PeriodType, ProjectSummary, ProjectTeamspaceEntry, SessionEntry, TicketDetailPeriodType, TicketPeriod, WorkType } from "../types";
|
||||||
import { ZammadPriorityBadge, ZammadStateBadge } from "../zammad-meta";
|
import { ZammadPriorityBadge, ZammadStateBadge } from "../zammad-meta";
|
||||||
|
import { zammadArticleTypeFromValue, zammadArticleTypeLabel, zammadArticleTypeOptions, zammadArticleTypeValue, type ZammadArticleTypeId } from "../zammad-article-types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type TicketDetailPageProps = {
|
type TicketDetailPageProps = {
|
||||||
@@ -63,6 +65,7 @@ type TicketSessionFormState = {
|
|||||||
activity: string;
|
activity: string;
|
||||||
billingStatus: BillingStatus;
|
billingStatus: BillingStatus;
|
||||||
createZammadArticle: boolean;
|
createZammadArticle: boolean;
|
||||||
|
zammadArticleTypeId: ZammadArticleTypeId;
|
||||||
workType: WorkType;
|
workType: WorkType;
|
||||||
day: string;
|
day: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
@@ -96,6 +99,7 @@ function emptyTicketSessionForm(): TicketSessionFormState {
|
|||||||
activity: "",
|
activity: "",
|
||||||
billingStatus: null,
|
billingStatus: null,
|
||||||
createZammadArticle: false,
|
createZammadArticle: false,
|
||||||
|
zammadArticleTypeId: 6,
|
||||||
workType: "support",
|
workType: "support",
|
||||||
day: currentDay(),
|
day: currentDay(),
|
||||||
startTime: "09:00",
|
startTime: "09:00",
|
||||||
@@ -103,6 +107,19 @@ function emptyTicketSessionForm(): TicketSessionFormState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notifyZammadArticleResult(result: CreateSessionResult) {
|
||||||
|
if (result.zammadArticle?.status === "sent") {
|
||||||
|
toast.success("Zammad-Notiz übertragen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.zammadArticle?.status === "queued") {
|
||||||
|
toast.warning("Zammad-Notiz geparkt", {
|
||||||
|
description: result.zammadArticle.error ?? "Die Session wurde gespeichert. Die Notiz kann im Profil später erneut gesendet werden."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function projectEntryId() {
|
function projectEntryId() {
|
||||||
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
@@ -599,16 +616,21 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTicketSession((current) => {
|
setTicketSession((current) => {
|
||||||
if (current.activity || current.billingStatus || current.createZammadArticle === currentUser.zammad_session_note_default_enabled) {
|
if (
|
||||||
|
current.activity ||
|
||||||
|
current.billingStatus ||
|
||||||
|
(current.createZammadArticle === currentUser.zammad_session_note_default_enabled && current.zammadArticleTypeId === currentUser.zammad_article_type_id)
|
||||||
|
) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
createZammadArticle: currentUser.zammad_session_note_default_enabled
|
createZammadArticle: currentUser.zammad_session_note_default_enabled,
|
||||||
|
zammadArticleTypeId: currentUser.zammad_article_type_id
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [currentUser.zammad_session_note_default_enabled]);
|
}, [currentUser.zammad_article_type_id, currentUser.zammad_session_note_default_enabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProjectDescriptionExpanded(false);
|
setProjectDescriptionExpanded(false);
|
||||||
@@ -1031,7 +1053,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
setSavingTicketSession(true);
|
setSavingTicketSession(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createSession({
|
const result = await createSession({
|
||||||
ticketNumber: ticket.ticket_number,
|
ticketNumber: ticket.ticket_number,
|
||||||
organizationId: effectiveOrganizationId,
|
organizationId: effectiveOrganizationId,
|
||||||
activity: ticketSession.activity,
|
activity: ticketSession.activity,
|
||||||
@@ -1041,11 +1063,13 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
durationSeconds,
|
durationSeconds,
|
||||||
projectTeamspaceEntryId: effectiveWorkType === "consulting" && requiresProjectTeamspaceEntry ? ticketSession.projectTeamspaceEntryId : null,
|
projectTeamspaceEntryId: effectiveWorkType === "consulting" && requiresProjectTeamspaceEntry ? ticketSession.projectTeamspaceEntryId : null,
|
||||||
billingStatus: ticketSession.billingStatus,
|
billingStatus: ticketSession.billingStatus,
|
||||||
createZammadArticle: ticketSession.createZammadArticle
|
createZammadArticle: ticketSession.createZammadArticle,
|
||||||
|
zammadArticleTypeId: ticketSession.zammadArticleTypeId
|
||||||
});
|
});
|
||||||
toast.success("Session nachgetragen", {
|
toast.success("Session nachgetragen", {
|
||||||
description: "Der Eintrag wurde diesem Ticket zugeordnet. Betroffene Abschlüsse wurden wieder geöffnet."
|
description: "Der Eintrag wurde diesem Ticket zugeordnet. Betroffene Abschlüsse wurden wieder geöffnet."
|
||||||
});
|
});
|
||||||
|
notifyZammadArticleResult(result);
|
||||||
setTicketSession({
|
setTicketSession({
|
||||||
...emptyTicketSessionForm(),
|
...emptyTicketSessionForm(),
|
||||||
organizationId: ticket.organization_id ?? "",
|
organizationId: ticket.organization_id ?? "",
|
||||||
@@ -1054,7 +1078,8 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
workType: ticket.work_type ?? "support",
|
workType: ticket.work_type ?? "support",
|
||||||
day: ticketSession.day,
|
day: ticketSession.day,
|
||||||
billingStatus: null,
|
billingStatus: null,
|
||||||
createZammadArticle: currentUser.zammad_session_note_default_enabled
|
createZammadArticle: currentUser.zammad_session_note_default_enabled,
|
||||||
|
zammadArticleTypeId: currentUser.zammad_article_type_id
|
||||||
});
|
});
|
||||||
await load();
|
await load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1842,7 +1867,16 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
<p className="whitespace-pre-wrap break-words text-sm font-medium">{session.activity}</p>
|
<p className="whitespace-pre-wrap break-words text-sm font-medium">{session.activity}</p>
|
||||||
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||||
<span>{session.organization_name ?? session.customer_name}</span>
|
<span>{session.organization_name ?? session.customer_name}</span>
|
||||||
|
{session.zammad_article_status ? (
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
{session.zammad_article_status === "sent" ? (
|
||||||
|
<Send className="size-3.5 text-emerald-500" aria-hidden="true" />
|
||||||
|
) : null}
|
||||||
|
<span>{zammadArticleTypeLabel(session.zammad_article_type_id)}</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span>{session.work_type === "support" ? "Support" : "Consulting"}</span>
|
<span>{session.work_type === "support" ? "Support" : "Consulting"}</span>
|
||||||
|
)}
|
||||||
<span>Erfasst {formatDateTime(session.created_at)}</span>
|
<span>Erfasst {formatDateTime(session.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2009,6 +2043,25 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
<span className="block text-xs text-muted-foreground">Überträgt die Tätigkeit inklusive time_unit nach Zammad oder parkt sie bei Fehlern.</span>
|
<span className="block text-xs text-muted-foreground">Überträgt die Tätigkeit inklusive time_unit nach Zammad oder parkt sie bei Fehlern.</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
{ticketSession.createZammadArticle ? (
|
||||||
|
<div className="space-y-2 lg:col-span-5">
|
||||||
|
<label className="text-sm font-medium" htmlFor="ticket-session-zammad-article-type">
|
||||||
|
Zammad-Typ
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="ticket-session-zammad-article-type"
|
||||||
|
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={zammadArticleTypeValue(ticketSession.zammadArticleTypeId)}
|
||||||
|
onChange={(event) => setTicketSession({ ...ticketSession, zammadArticleTypeId: zammadArticleTypeFromValue(event.currentTarget.value) })}
|
||||||
|
>
|
||||||
|
{zammadArticleTypeOptions.map((option) => (
|
||||||
|
<option key={option.value || "empty"} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<Button type="submit" disabled={savingTicketSession}>
|
<Button type="submit" disabled={savingTicketSession}>
|
||||||
<Save className="size-4" />
|
<Save className="size-4" />
|
||||||
{savingTicketSession ? "Speichert..." : "Nachtragen"}
|
{savingTicketSession ? "Speichert..." : "Nachtragen"}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { HelpLink } from "@/components/HelpLink";
|
import { HelpLink } from "@/components/HelpLink";
|
||||||
import { OrganizationSelect } from "@/components/OrganizationSelect";
|
import { OrganizationSelect } from "@/components/OrganizationSelect";
|
||||||
import { createSession, getRecurringBillings, lookupTicket } from "../api";
|
import { createSession, getRecurringBillings, lookupTicket } from "../api";
|
||||||
|
import type { CreateSessionResult } from "../api";
|
||||||
import { formatTimer } from "../format";
|
import { formatTimer } from "../format";
|
||||||
import { isRecurringBillingTicketNumber, ticketHasRecurringBilling } from "../recurring-billings";
|
import { isRecurringBillingTicketNumber, ticketHasRecurringBilling } from "../recurring-billings";
|
||||||
import { activeElapsedMs, createClientId, pauseEntry, resumeEntry, ticketPattern, type TimerEntry } from "../timers";
|
import { activeElapsedMs, createClientId, pauseEntry, resumeEntry, ticketPattern, type TimerEntry } from "../timers";
|
||||||
import type { AuthUser, BillingStatus, RecurringBilling, TicketMeta, WorkType } from "../types";
|
import type { AuthUser, BillingStatus, RecurringBilling, TicketMeta, WorkType } from "../types";
|
||||||
|
import { zammadArticleTypeFromValue, zammadArticleTypeOptions, zammadArticleTypeValue, type ZammadArticleTypeId } from "../zammad-article-types";
|
||||||
|
|
||||||
type ManualSessionFormState = {
|
type ManualSessionFormState = {
|
||||||
ticketNumber: string;
|
ticketNumber: string;
|
||||||
@@ -25,6 +27,7 @@ type ManualSessionFormState = {
|
|||||||
activity: string;
|
activity: string;
|
||||||
billingStatus: BillingStatus;
|
billingStatus: BillingStatus;
|
||||||
createZammadArticle: boolean;
|
createZammadArticle: boolean;
|
||||||
|
zammadArticleTypeId: ZammadArticleTypeId;
|
||||||
workType: WorkType;
|
workType: WorkType;
|
||||||
day: string;
|
day: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
@@ -45,6 +48,7 @@ function emptyManualSession(): ManualSessionFormState {
|
|||||||
activity: "",
|
activity: "",
|
||||||
billingStatus: null,
|
billingStatus: null,
|
||||||
createZammadArticle: false,
|
createZammadArticle: false,
|
||||||
|
zammadArticleTypeId: 6,
|
||||||
workType: "support",
|
workType: "support",
|
||||||
day: currentDay(),
|
day: currentDay(),
|
||||||
startTime: "09:00",
|
startTime: "09:00",
|
||||||
@@ -60,6 +64,7 @@ type FinishedSession = {
|
|||||||
billingStatus: BillingStatus;
|
billingStatus: BillingStatus;
|
||||||
promptBillingStatus: boolean;
|
promptBillingStatus: boolean;
|
||||||
createZammadArticle: boolean;
|
createZammadArticle: boolean;
|
||||||
|
zammadArticleTypeId: ZammadArticleTypeId;
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
endedAt: string;
|
endedAt: string;
|
||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
@@ -115,6 +120,19 @@ function BillingStatusSelect({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notifyZammadArticleResult(result: CreateSessionResult) {
|
||||||
|
if (result.zammadArticle?.status === "sent") {
|
||||||
|
toast.success("Zammad-Notiz übertragen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.zammadArticle?.status === "queued") {
|
||||||
|
toast.warning("Zammad-Notiz geparkt", {
|
||||||
|
description: result.zammadArticle.error ?? "Die Session wurde gespeichert. Die Notiz kann im Profil später erneut gesendet werden."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimerId, tick, currentUser }: TimerPageProps) {
|
export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimerId, tick, currentUser }: TimerPageProps) {
|
||||||
const [ticketNumber, setTicketNumber] = useState("");
|
const [ticketNumber, setTicketNumber] = useState("");
|
||||||
const [finished, setFinished] = useState<FinishedSession | null>(null);
|
const [finished, setFinished] = useState<FinishedSession | null>(null);
|
||||||
@@ -225,16 +243,23 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setManualSession((current) => {
|
setManualSession((current) => {
|
||||||
if (current.ticketNumber || current.activity || current.organizationId || current.billingStatus || current.createZammadArticle === currentUser.zammad_session_note_default_enabled) {
|
if (
|
||||||
|
current.ticketNumber ||
|
||||||
|
current.activity ||
|
||||||
|
current.organizationId ||
|
||||||
|
current.billingStatus ||
|
||||||
|
(current.createZammadArticle === currentUser.zammad_session_note_default_enabled && current.zammadArticleTypeId === currentUser.zammad_article_type_id)
|
||||||
|
) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
createZammadArticle: currentUser.zammad_session_note_default_enabled
|
createZammadArticle: currentUser.zammad_session_note_default_enabled,
|
||||||
|
zammadArticleTypeId: currentUser.zammad_article_type_id
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [currentUser.zammad_session_note_default_enabled]);
|
}, [currentUser.zammad_article_type_id, currentUser.zammad_session_note_default_enabled]);
|
||||||
|
|
||||||
async function addTimer(source: "local" | "zammad" = "local") {
|
async function addTimer(source: "local" | "zammad" = "local") {
|
||||||
const normalizedTicketNumber = ticketNumber.trim();
|
const normalizedTicketNumber = ticketNumber.trim();
|
||||||
@@ -464,6 +489,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
billingStatus: timer.billingStatus,
|
billingStatus: timer.billingStatus,
|
||||||
promptBillingStatus: currentUser.session_billing_prompt_enabled && timer.billingStatus === null,
|
promptBillingStatus: currentUser.session_billing_prompt_enabled && timer.billingStatus === null,
|
||||||
createZammadArticle: currentUser.zammad_session_note_default_enabled,
|
createZammadArticle: currentUser.zammad_session_note_default_enabled,
|
||||||
|
zammadArticleTypeId: currentUser.zammad_article_type_id,
|
||||||
startedAt: new Date(stoppedTimer.startedAt).toISOString(),
|
startedAt: new Date(stoppedTimer.startedAt).toISOString(),
|
||||||
endedAt: new Date(now).toISOString(),
|
endedAt: new Date(now).toISOString(),
|
||||||
durationSeconds,
|
durationSeconds,
|
||||||
@@ -522,7 +548,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createSession({
|
const result = await createSession({
|
||||||
ticketNumber: finished.ticketNumber,
|
ticketNumber: finished.ticketNumber,
|
||||||
startedAt: finished.startedAt,
|
startedAt: finished.startedAt,
|
||||||
endedAt: finished.endedAt,
|
endedAt: finished.endedAt,
|
||||||
@@ -533,12 +559,14 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
zammadLookup: Boolean(finished.ticket?.zammad_ticket_id),
|
zammadLookup: Boolean(finished.ticket?.zammad_ticket_id),
|
||||||
projectTeamspaceEntryId: finishedRequiresTeamspaceEntry ? projectTeamspaceEntryId : null,
|
projectTeamspaceEntryId: finishedRequiresTeamspaceEntry ? projectTeamspaceEntryId : null,
|
||||||
billingStatus: finished.billingStatus,
|
billingStatus: finished.billingStatus,
|
||||||
createZammadArticle: finished.createZammadArticle
|
createZammadArticle: finished.createZammadArticle,
|
||||||
|
zammadArticleTypeId: finished.zammadArticleTypeId
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success("Session gespeichert", {
|
toast.success("Session gespeichert", {
|
||||||
description: `${finished.ticketNumber} wurde mit ${finished.roundedMinutes} Minute(n) erfasst.`
|
description: `${finished.ticketNumber} wurde mit ${finished.roundedMinutes} Minute(n) erfasst.`
|
||||||
});
|
});
|
||||||
|
notifyZammadArticleResult(result);
|
||||||
setTimers((current) => current.filter((timer) => timer.id !== finished.timerId));
|
setTimers((current) => current.filter((timer) => timer.id !== finished.timerId));
|
||||||
setFinished(null);
|
setFinished(null);
|
||||||
setOrganizationId("");
|
setOrganizationId("");
|
||||||
@@ -601,7 +629,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
setSavingManualSession(true);
|
setSavingManualSession(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createSession({
|
const result = await createSession({
|
||||||
ticketNumber: manualSession.ticketNumber.trim(),
|
ticketNumber: manualSession.ticketNumber.trim(),
|
||||||
organizationId: manualSession.organizationId,
|
organizationId: manualSession.organizationId,
|
||||||
activity: manualSession.activity,
|
activity: manualSession.activity,
|
||||||
@@ -612,15 +640,18 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
zammadLookup: Boolean(manualLookupTicket?.zammad_ticket_id),
|
zammadLookup: Boolean(manualLookupTicket?.zammad_ticket_id),
|
||||||
projectTeamspaceEntryId: requiresTeamspaceEntry(manualLookupTicket) ? manualSession.projectTeamspaceEntryId : null,
|
projectTeamspaceEntryId: requiresTeamspaceEntry(manualLookupTicket) ? manualSession.projectTeamspaceEntryId : null,
|
||||||
billingStatus: manualSession.billingStatus,
|
billingStatus: manualSession.billingStatus,
|
||||||
createZammadArticle: manualSession.createZammadArticle
|
createZammadArticle: manualSession.createZammadArticle,
|
||||||
|
zammadArticleTypeId: manualSession.zammadArticleTypeId
|
||||||
});
|
});
|
||||||
toast.success("Session nachgetragen", {
|
toast.success("Session nachgetragen", {
|
||||||
description: manualLookupTicket?.is_project ? "Der Eintrag wurde dem Projekt zugeordnet." : "Der Eintrag ist in deiner Ticketliste offen."
|
description: manualLookupTicket?.is_project ? "Der Eintrag wurde dem Projekt zugeordnet." : "Der Eintrag ist in deiner Ticketliste offen."
|
||||||
});
|
});
|
||||||
|
notifyZammadArticleResult(result);
|
||||||
setManualSession({
|
setManualSession({
|
||||||
...emptyManualSession(),
|
...emptyManualSession(),
|
||||||
day: manualSession.day,
|
day: manualSession.day,
|
||||||
createZammadArticle: currentUser.zammad_session_note_default_enabled
|
createZammadArticle: currentUser.zammad_session_note_default_enabled,
|
||||||
|
zammadArticleTypeId: currentUser.zammad_article_type_id
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Session konnte nicht nachgetragen werden", {
|
toast.error("Session konnte nicht nachgetragen werden", {
|
||||||
@@ -935,6 +966,25 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
<span className="block text-xs text-muted-foreground">Überträgt die Tätigkeit inklusive time_unit nach Zammad oder parkt sie bei Fehlern.</span>
|
<span className="block text-xs text-muted-foreground">Überträgt die Tätigkeit inklusive time_unit nach Zammad oder parkt sie bei Fehlern.</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
{manualSession.createZammadArticle ? (
|
||||||
|
<div className="space-y-2 lg:col-span-5">
|
||||||
|
<label className="text-sm font-medium" htmlFor="manual-zammad-article-type">
|
||||||
|
Zammad-Typ
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="manual-zammad-article-type"
|
||||||
|
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={zammadArticleTypeValue(manualSession.zammadArticleTypeId)}
|
||||||
|
onChange={(event) => setManualSession({ ...manualSession, zammadArticleTypeId: zammadArticleTypeFromValue(event.currentTarget.value) })}
|
||||||
|
>
|
||||||
|
{zammadArticleTypeOptions.map((option) => (
|
||||||
|
<option key={option.value || "empty"} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<Button type="submit" disabled={savingManualSession || manualTicketIsRecurringBilling}>
|
<Button type="submit" disabled={savingManualSession || manualTicketIsRecurringBilling}>
|
||||||
<Upload className="size-4" />
|
<Upload className="size-4" />
|
||||||
{savingManualSession ? "Speichert..." : "Nachtragen"}
|
{savingManualSession ? "Speichert..." : "Nachtragen"}
|
||||||
@@ -1006,7 +1056,8 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{finished ? (
|
{finished ? (
|
||||||
<div className="flex items-start gap-3 rounded-md border bg-muted/30 p-3">
|
<div className="space-y-3 rounded-md border bg-muted/30 p-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="finished-zammad-article"
|
id="finished-zammad-article"
|
||||||
checked={finished.createZammadArticle}
|
checked={finished.createZammadArticle}
|
||||||
@@ -1024,6 +1075,26 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{finished.createZammadArticle ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium" htmlFor="finished-zammad-article-type">
|
||||||
|
Zammad-Typ
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="finished-zammad-article-type"
|
||||||
|
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={zammadArticleTypeValue(finished.zammadArticleTypeId)}
|
||||||
|
onChange={(event) => setFinished((current) => (current ? { ...current, zammadArticleTypeId: zammadArticleTypeFromValue(event.currentTarget.value) } : current))}
|
||||||
|
>
|
||||||
|
{zammadArticleTypeOptions.map((option) => (
|
||||||
|
<option key={option.value || "empty"} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{finished?.promptBillingStatus ? (
|
{finished?.promptBillingStatus ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export const zammadArticleTypeOptions = [
|
||||||
|
{ value: "4", label: "Dienstleistung" },
|
||||||
|
{ value: "1", label: "Koordination, Kommunikation und Dokumentation" },
|
||||||
|
{ value: "6", label: "Nicht kategorisiert" },
|
||||||
|
{ value: "2", label: "Schulungen, Workshops und Meetings" },
|
||||||
|
{ value: "5", label: "Support" },
|
||||||
|
{ value: "3", label: "Vor- und Nacharbeiten" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ZammadArticleTypeId = number;
|
||||||
|
|
||||||
|
export function zammadArticleTypeValue(typeId: ZammadArticleTypeId | null | undefined) {
|
||||||
|
return String(typeId ?? 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zammadArticleTypeFromValue(value: string): ZammadArticleTypeId {
|
||||||
|
return Number(value || 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zammadArticleTypeLabel(typeId: ZammadArticleTypeId | null | undefined) {
|
||||||
|
const value = zammadArticleTypeValue(typeId);
|
||||||
|
|
||||||
|
return zammadArticleTypeOptions.find((option) => option.value === value)?.label ?? "Nicht kategorisiert";
|
||||||
|
}
|
||||||
Reference in new issue
Block a user