Update
This commit is contained in:
1 parent
c01d747352
commit
235c19e51b
10 files changed
+175
-31
No files matched your search
+87
-8
@@ -354,6 +354,7 @@ type ZammadTicketImport = {
|
|||||||
teamspaceBudgetMinutes: number | null;
|
teamspaceBudgetMinutes: number | null;
|
||||||
teamspaceProject: string | null;
|
teamspaceProject: string | null;
|
||||||
teamspaceProjects: string[];
|
teamspaceProjects: string[];
|
||||||
|
pendingTime: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ZammadSyncExistingFields = {
|
type ZammadSyncExistingFields = {
|
||||||
@@ -363,6 +364,7 @@ type ZammadSyncExistingFields = {
|
|||||||
projectName: boolean;
|
projectName: boolean;
|
||||||
budget: boolean;
|
budget: boolean;
|
||||||
overrideBudget: boolean;
|
overrideBudget: boolean;
|
||||||
|
pendingTodo: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ZammadSyncChange = {
|
type ZammadSyncChange = {
|
||||||
@@ -490,10 +492,17 @@ function parseZammadTeamspaceProjects(value: unknown) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value.match(/[^\s+]+/g) ?? [];
|
||||||
.split("+")
|
}
|
||||||
.map((part) => part.trim())
|
|
||||||
.filter(Boolean);
|
function parseZammadPendingTime(value: unknown) {
|
||||||
|
if (typeof value !== "string" || !value.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function zammadPreferenceValue(ticket: Record<string, unknown>, key: string) {
|
function zammadPreferenceValue(ticket: Record<string, unknown>, key: string) {
|
||||||
@@ -700,7 +709,8 @@ function zammadTicketImportFromRaw(rawTicket: Record<string, unknown>, fallbackT
|
|||||||
preferences,
|
preferences,
|
||||||
teamspaceBudgetMinutes: parseZammadBudgetMinutes(zammadPreferenceValue(rawTicket, "gesamtbudget")),
|
teamspaceBudgetMinutes: parseZammadBudgetMinutes(zammadPreferenceValue(rawTicket, "gesamtbudget")),
|
||||||
teamspaceProject: teamspaceProjects[0] ?? null,
|
teamspaceProject: teamspaceProjects[0] ?? null,
|
||||||
teamspaceProjects
|
teamspaceProjects,
|
||||||
|
pendingTime: parseZammadPendingTime(zammadPreferenceValue(rawTicket, "pending_time"))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1883,7 +1893,8 @@ function zammadSyncExistingFields(value: unknown): ZammadSyncExistingFields {
|
|||||||
type: optionalBoolean(source.type, false),
|
type: optionalBoolean(source.type, false),
|
||||||
projectName: optionalBoolean(source.projectName, false),
|
projectName: optionalBoolean(source.projectName, false),
|
||||||
budget: optionalBoolean(source.budget, false),
|
budget: optionalBoolean(source.budget, false),
|
||||||
overrideBudget: optionalBoolean(source.overrideBudget, false)
|
overrideBudget: optionalBoolean(source.overrideBudget, false),
|
||||||
|
pendingTodo: optionalBoolean(source.pendingTodo, false)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2014,7 +2025,8 @@ function zammadSyncSnapshot(zammadTicket: ZammadTicketImport) {
|
|||||||
preferences: zammadTicket.preferences,
|
preferences: zammadTicket.preferences,
|
||||||
teamspaceProject: zammadTicket.teamspaceProject,
|
teamspaceProject: zammadTicket.teamspaceProject,
|
||||||
teamspaceProjects: zammadTicket.teamspaceProjects,
|
teamspaceProjects: zammadTicket.teamspaceProjects,
|
||||||
teamspaceBudgetMinutes: zammadTicket.teamspaceBudgetMinutes
|
teamspaceBudgetMinutes: zammadTicket.teamspaceBudgetMinutes,
|
||||||
|
pendingTime: zammadTicket.pendingTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2051,7 +2063,8 @@ function zammadTicketFromSyncSnapshot(snapshot: Record<string, unknown> | null):
|
|||||||
teamspaceProject: typeof snapshot.teamspaceProject === "string" && snapshot.teamspaceProject.trim() ? snapshot.teamspaceProject.trim() : null,
|
teamspaceProject: typeof snapshot.teamspaceProject === "string" && snapshot.teamspaceProject.trim() ? snapshot.teamspaceProject.trim() : null,
|
||||||
teamspaceProjects: Array.isArray(snapshot.teamspaceProjects)
|
teamspaceProjects: Array.isArray(snapshot.teamspaceProjects)
|
||||||
? snapshot.teamspaceProjects.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean)
|
? snapshot.teamspaceProjects.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean)
|
||||||
: parseZammadTeamspaceProjects(snapshot.teamspaceProject)
|
: parseZammadTeamspaceProjects(snapshot.teamspaceProject),
|
||||||
|
pendingTime: parseZammadPendingTime(snapshot.pendingTime)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2160,6 +2173,63 @@ function formatTeamspaceProjectList(projects: Array<string | null | undefined>)
|
|||||||
return projects.map((project) => project?.trim()).filter(Boolean).join(" + ");
|
return projects.map((project) => project?.trim()).filter(Boolean).join(" + ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function zammadPendingTodoPayload(zammadTicket: ZammadTicketImport) {
|
||||||
|
if ((zammadTicket.zammadStateId !== 3 && zammadTicket.zammadStateId !== 7) || !zammadTicket.pendingTime) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = zammadTicket.zammadStateId === 7 ? "Abschluss" : "Erinnerung";
|
||||||
|
const day = zammadTicket.pendingTime.slice(0, 10);
|
||||||
|
const ticketLabel = zammadTicket.ticketNumber.replace(/^Ticket#?/i, "").replace(/^#/, "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
day,
|
||||||
|
title: `${kind} ${ticketLabel ? `Ticket#${ticketLabel}` : "Ticket"}`,
|
||||||
|
description: `${kind} für dieses Ticket prüfen.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function zammadPendingTodoExists(client: QueryClient, ticketId: string, userId: string, zammadTicket: ZammadTicketImport) {
|
||||||
|
const payload = zammadPendingTodoPayload(zammadTicket);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await client.query<{ exists: boolean }>(
|
||||||
|
`
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM ticket_todos
|
||||||
|
WHERE ticket_id = $1
|
||||||
|
AND user_id = $2
|
||||||
|
AND title = $3
|
||||||
|
AND starts_on = $4::date
|
||||||
|
AND ends_on = $4::date
|
||||||
|
) AS exists;
|
||||||
|
`,
|
||||||
|
[ticketId, userId, payload.title, payload.day]
|
||||||
|
);
|
||||||
|
|
||||||
|
return Boolean(result.rows[0]?.exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureZammadPendingTodo(client: QueryClient, ticketId: string, userId: string, zammadTicket: ZammadTicketImport) {
|
||||||
|
const payload = zammadPendingTodoPayload(zammadTicket);
|
||||||
|
|
||||||
|
if (!payload || await zammadPendingTodoExists(client, ticketId, userId, zammadTicket)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO ticket_todos (ticket_id, user_id, title, description, priority, starts_on, ends_on, sort_order)
|
||||||
|
VALUES ($1, $2, $3, $4, 'high', $5::date, $5::date, (SELECT COALESCE(MAX(sort_order), 0) + 1024 FROM ticket_todos WHERE user_id = $2));
|
||||||
|
`,
|
||||||
|
[ticketId, userId, payload.title, payload.description, payload.day]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function syncProjectTeamspaceEntriesFromZammad(
|
async function syncProjectTeamspaceEntriesFromZammad(
|
||||||
client: QueryClient,
|
client: QueryClient,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -2251,6 +2321,11 @@ async function planExistingZammadTicketSync(
|
|||||||
addSyncChange(changes, "Zammad-Priorität", localTicket.zammad_priority_id, zammadTicket.zammadPriorityId);
|
addSyncChange(changes, "Zammad-Priorität", localTicket.zammad_priority_id, zammadTicket.zammadPriorityId);
|
||||||
addSyncChange(changes, "Zammad-Status", localTicket.zammad_state_id, zammadTicket.zammadStateId);
|
addSyncChange(changes, "Zammad-Status", localTicket.zammad_state_id, zammadTicket.zammadStateId);
|
||||||
|
|
||||||
|
const pendingTodo = zammadPendingTodoPayload(zammadTicket);
|
||||||
|
if (fields.pendingTodo && pendingTodo && !(await zammadPendingTodoExists(client, localTicket.id, userId, zammadTicket))) {
|
||||||
|
changes.push({ field: "ToDo", from: null, to: `${pendingTodo.title} (${pendingTodo.day})` });
|
||||||
|
}
|
||||||
|
|
||||||
if (zammadTicket.workType === "support" && zammadTicket.zammadStateId === 4) {
|
if (zammadTicket.workType === "support" && zammadTicket.zammadStateId === 4) {
|
||||||
return changes.length > 0
|
return changes.length > 0
|
||||||
? {
|
? {
|
||||||
@@ -2579,6 +2654,10 @@ async function applyZammadTicketSync(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fields.pendingTodo) {
|
||||||
|
await ensureZammadPendingTodo(client, localTicket.id, userId, zammadTicket);
|
||||||
|
}
|
||||||
|
|
||||||
if (!metadataOnly && fields.type && zammadTicket.workType === "support") {
|
if (!metadataOnly && fields.type && zammadTicket.workType === "support") {
|
||||||
await deleteAllProjectDataForTicket(client, localTicket.id);
|
await deleteAllProjectDataForTicket(client, localTicket.id);
|
||||||
await client.query(
|
await client.query(
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ import { cn } from "./lib/utils";
|
|||||||
import { ApiError, deleteTodo, getCurrentUser, getRecurringBillings, getSetupStatus, getTodos, logout, lookupTicket, searchTickets, updateTodo } from "./api";
|
import { ApiError, deleteTodo, getCurrentUser, getRecurringBillings, getSetupStatus, getTodos, logout, lookupTicket, searchTickets, updateTodo } from "./api";
|
||||||
import { currentDay, formatDateTime, formatMinutes, formatTimer } from "./format";
|
import { currentDay, formatDateTime, formatMinutes, formatTimer } from "./format";
|
||||||
import { isRecurringBillingTicketNumber, ticketHasRecurringBilling } from "./recurring-billings";
|
import { isRecurringBillingTicketNumber, ticketHasRecurringBilling } from "./recurring-billings";
|
||||||
import { activeElapsedMs, pauseEntry, readStoredTimers, resumeEntry, storageKeyForUser, ticketPattern, type TimerEntry } from "./timers";
|
import { activeElapsedMs, createClientId, pauseEntry, readStoredTimers, resumeEntry, storageKeyForUser, ticketPattern, type TimerEntry } from "./timers";
|
||||||
import { formatTodoPeriod, isTodoActiveOnDay, isTodoFixedOnDay, isTodoOverdue } from "./todos";
|
import { formatTodoPeriod, isTodoActiveOnDay, isTodoFixedOnDay, isTodoOverdue } from "./todos";
|
||||||
import { AdminUsersPage } from "./views/AdminUsersPage";
|
import { AdminUsersPage } from "./views/AdminUsersPage";
|
||||||
import { AnalysisPage } from "./views/AnalysisPage";
|
import { AnalysisPage } from "./views/AnalysisPage";
|
||||||
@@ -679,7 +679,7 @@ function SidebarTodayTodos({ todos, onNavigate }: SidebarTodayTodosProps) {
|
|||||||
<button
|
<button
|
||||||
key={`fixed-${todo.id}`}
|
key={`fixed-${todo.id}`}
|
||||||
type="button"
|
type="button"
|
||||||
className="block w-full rounded-md border bg-background/70 px-2 py-1.5 text-left hover:bg-muted"
|
className={`block w-full rounded-md border px-2 py-1.5 text-left hover:brightness-110 ${sidebarTodoCalendarClasses[todo.priority]}`}
|
||||||
onClick={() => onNavigate(`/tickets/${todo.ticket_id}`)}
|
onClick={() => onNavigate(`/tickets/${todo.ticket_id}`)}
|
||||||
>
|
>
|
||||||
<span className="block truncate text-[11px] font-semibold">{todo.title}</span>
|
<span className="block truncate text-[11px] font-semibold">{todo.title}</span>
|
||||||
@@ -1056,7 +1056,7 @@ export function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newTimer: TimerEntry = {
|
const newTimer: TimerEntry = {
|
||||||
id: crypto.randomUUID(),
|
id: createClientId("timer"),
|
||||||
ticketNumber,
|
ticketNumber,
|
||||||
organizationName: ticketResult.ticket?.organization_name ?? ticketResult.ticket?.customer_name ?? null,
|
organizationName: ticketResult.ticket?.organization_name ?? ticketResult.ticket?.customer_name ?? null,
|
||||||
workType: ticketResult.ticket?.work_type ?? null,
|
workType: ticketResult.ticket?.work_type ?? null,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type ZammadSyncExistingFields = {
|
|||||||
projectName: boolean;
|
projectName: boolean;
|
||||||
budget: boolean;
|
budget: boolean;
|
||||||
overrideBudget: boolean;
|
overrideBudget: boolean;
|
||||||
|
pendingTodo: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ZammadSyncChange = {
|
export type ZammadSyncChange = {
|
||||||
|
|||||||
@@ -8,9 +8,32 @@ type CopyTicketButtonProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function CopyTicketButton({ ticketNumber }: CopyTicketButtonProps) {
|
export function CopyTicketButton({ ticketNumber }: CopyTicketButtonProps) {
|
||||||
|
async function writeToClipboard(value: string) {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = value;
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.left = "-9999px";
|
||||||
|
textarea.setAttribute("readonly", "");
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!document.execCommand("copy")) {
|
||||||
|
throw new Error("Zwischenablage nicht verfügbar");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyTicketNumber() {
|
async function copyTicketNumber() {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(ticketNumber);
|
await writeToClipboard(ticketNumber);
|
||||||
toast.success("Ticketnummer kopiert", {
|
toast.success("Ticketnummer kopiert", {
|
||||||
description: ticketNumber
|
description: ticketNumber
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ const legacyStorageKey = "tickettracker.activeTimer";
|
|||||||
|
|
||||||
export const ticketPattern = /^Ticket#\d{6}$/;
|
export const ticketPattern = /^Ticket#\d{6}$/;
|
||||||
|
|
||||||
|
export function createClientId(prefix = "client") {
|
||||||
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function pauseEntry(timer: TimerEntry, now = Date.now()): TimerEntry {
|
export function pauseEntry(timer: TimerEntry, now = Date.now()): TimerEntry {
|
||||||
if (timer.phase === "paused") {
|
if (timer.phase === "paused") {
|
||||||
return timer;
|
return timer;
|
||||||
@@ -104,7 +112,7 @@ export function readStoredTimers(userId: string): TimerEntry[] {
|
|||||||
return normalizeTimers([
|
return normalizeTimers([
|
||||||
{
|
{
|
||||||
...parsed,
|
...parsed,
|
||||||
id: crypto.randomUUID(),
|
id: createClientId("timer"),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ function stableSort<T>(items: T[], compare: (left: T, right: T) => number) {
|
|||||||
|
|
||||||
function createNote(title: string): DashboardNote {
|
function createNote(title: string): DashboardNote {
|
||||||
return {
|
return {
|
||||||
id: typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
title,
|
title,
|
||||||
content: ""
|
content: ""
|
||||||
};
|
};
|
||||||
@@ -1746,10 +1746,10 @@ function ProjectBudgetWidget({ widget, editing, projects, stats, onNavigate, onU
|
|||||||
{ value: "critical90", label: "Ab 90%" },
|
{ value: "critical90", label: "Ab 90%" },
|
||||||
{ value: "overBudget", label: "Ab 100%" },
|
{ value: "overBudget", label: "Ab 100%" },
|
||||||
{ value: "withoutBudget", label: "Ohne Kontingent" },
|
{ value: "withoutBudget", label: "Ohne Kontingent" },
|
||||||
{ value: "open", label: "Offene Projekte" },
|
{ value: "open", label: "TT Offen" },
|
||||||
{ value: "closed", label: "Geschlossene Projekte" },
|
{ value: "closed", label: "TT Geschl." },
|
||||||
...zammadStateFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadState:${option.value}`, label: `Status: ${option.label}` })),
|
...zammadStateFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadState:${option.value}`, label: `ZA ${option.label}` })),
|
||||||
...zammadPriorityFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadPriority:${option.value}`, label: `Prio: ${option.label}` })),
|
...zammadPriorityFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadPriority:${option.value}`, label: `ZA Prio ${option.label}` })),
|
||||||
{ value: "openTodos", label: "Mit offenen ToDos" },
|
{ value: "openTodos", label: "Mit offenen ToDos" },
|
||||||
{ value: "overdueTodos", label: "Mit überfälligen ToDos" },
|
{ value: "overdueTodos", label: "Mit überfälligen ToDos" },
|
||||||
{ value: "fixedBilling", label: "Fixe Abrechnungen" },
|
{ value: "fixedBilling", label: "Fixe Abrechnungen" },
|
||||||
@@ -1766,8 +1766,8 @@ function ProjectBudgetWidget({ widget, editing, projects, stats, onNavigate, onU
|
|||||||
{ value: "title", label: "Titel" },
|
{ value: "title", label: "Titel" },
|
||||||
{ value: "ticket", label: "Ticket" },
|
{ value: "ticket", label: "Ticket" },
|
||||||
{ value: "organization", label: "Organisation" },
|
{ value: "organization", label: "Organisation" },
|
||||||
{ value: "zammadStatus", label: "Zammad-Status" },
|
{ value: "zammadStatus", label: "ZA Status" },
|
||||||
{ value: "zammadPriority", label: "Zammad-Prio" }
|
{ value: "zammadPriority", label: "ZA Prio" }
|
||||||
]}
|
]}
|
||||||
onChange={onUpdateConfig}
|
onChange={onUpdateConfig}
|
||||||
/>
|
/>
|
||||||
@@ -2357,8 +2357,8 @@ function FavoritesWidget({
|
|||||||
{ value: "projects", label: "Nur Projekte" },
|
{ value: "projects", label: "Nur Projekte" },
|
||||||
{ value: "openTodos", label: "Mit offenen ToDos" },
|
{ value: "openTodos", label: "Mit offenen ToDos" },
|
||||||
{ value: "overdueTodos", label: "Mit überfälligen ToDos" },
|
{ value: "overdueTodos", label: "Mit überfälligen ToDos" },
|
||||||
...zammadStateFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadState:${option.value}`, label: `Status: ${option.label}` })),
|
...zammadStateFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadState:${option.value}`, label: `ZA ${option.label}` })),
|
||||||
...zammadPriorityFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadPriority:${option.value}`, label: `Prio: ${option.label}` })),
|
...zammadPriorityFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadPriority:${option.value}`, label: `ZA Prio ${option.label}` })),
|
||||||
{ value: "missingMeta", label: "Nicht gefunden" },
|
{ value: "missingMeta", label: "Nicht gefunden" },
|
||||||
{ value: "fixedBilling", label: "Fixe Abrechnungen" },
|
{ value: "fixedBilling", label: "Fixe Abrechnungen" },
|
||||||
{ value: "manualBilling", label: "Ohne fixe Abrechnung" }
|
{ value: "manualBilling", label: "Ohne fixe Abrechnung" }
|
||||||
@@ -2370,8 +2370,8 @@ function FavoritesWidget({
|
|||||||
{ value: "ticket", label: "Ticket" },
|
{ value: "ticket", label: "Ticket" },
|
||||||
{ value: "title", label: "Titel" },
|
{ value: "title", label: "Titel" },
|
||||||
{ value: "recent", label: "Neu" },
|
{ value: "recent", label: "Neu" },
|
||||||
{ value: "zammadStatus", label: "Zammad-Status" },
|
{ value: "zammadStatus", label: "ZA Status" },
|
||||||
{ value: "zammadPriority", label: "Zammad-Prio" }
|
{ value: "zammadPriority", label: "ZA Prio" }
|
||||||
]}
|
]}
|
||||||
onChange={onUpdateConfig}
|
onChange={onUpdateConfig}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const projectSortOptions: Array<{ value: ProjectSortBy; label: string }> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function projectEntryId() {
|
function projectEntryId() {
|
||||||
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ function emptyTicketSessionForm(): TicketSessionFormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projectEntryId() {
|
function projectEntryId() {
|
||||||
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { OrganizationSelect } from "@/components/OrganizationSelect";
|
|||||||
import { createSession, getRecurringBillings, lookupTicket } from "../api";
|
import { createSession, getRecurringBillings, lookupTicket } from "../api";
|
||||||
import { formatTimer } from "../format";
|
import { formatTimer } from "../format";
|
||||||
import { isRecurringBillingTicketNumber, ticketHasRecurringBilling } from "../recurring-billings";
|
import { isRecurringBillingTicketNumber, ticketHasRecurringBilling } from "../recurring-billings";
|
||||||
import { activeElapsedMs, pauseEntry, resumeEntry, ticketPattern, type TimerEntry } from "../timers";
|
import { activeElapsedMs, createClientId, pauseEntry, resumeEntry, ticketPattern, type TimerEntry } from "../timers";
|
||||||
import type { RecurringBilling, TicketMeta, WorkType } from "../types";
|
import type { RecurringBilling, TicketMeta, WorkType } from "../types";
|
||||||
|
|
||||||
type ManualSessionFormState = {
|
type ManualSessionFormState = {
|
||||||
@@ -234,7 +234,7 @@ export function TimerPage({ timers, setTimers, selectedTimerId, setSelectedTimer
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const newTimer: TimerEntry = {
|
const newTimer: TimerEntry = {
|
||||||
id: crypto.randomUUID(),
|
id: createClientId("timer"),
|
||||||
ticketNumber: normalizedTicketNumber,
|
ticketNumber: normalizedTicketNumber,
|
||||||
organizationName: ticketResult.ticket?.organization_name ?? ticketResult.ticket?.customer_name ?? null,
|
organizationName: ticketResult.ticket?.organization_name ?? ticketResult.ticket?.customer_name ?? null,
|
||||||
workType: ticketResult.ticket?.work_type ?? null,
|
workType: ticketResult.ticket?.work_type ?? null,
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ const defaultFields: ZammadSyncExistingFields = {
|
|||||||
type: true,
|
type: true,
|
||||||
projectName: true,
|
projectName: true,
|
||||||
budget: true,
|
budget: true,
|
||||||
overrideBudget: false
|
overrideBudget: false,
|
||||||
|
pendingTodo: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldOptions: Array<{ key: Exclude<keyof ZammadSyncExistingFields, "overrideBudget">; label: string; description: string }> = [
|
const fieldOptions: Array<{ key: Exclude<keyof ZammadSyncExistingFields, "overrideBudget">; label: string; description: string }> = [
|
||||||
@@ -42,7 +43,8 @@ const fieldOptions: Array<{ key: Exclude<keyof ZammadSyncExistingFields, "overri
|
|||||||
{ key: "organization", label: "Organisation", description: "Lokale Organisation anhand der Zammad-ID setzen." },
|
{ key: "organization", label: "Organisation", description: "Lokale Organisation anhand der Zammad-ID setzen." },
|
||||||
{ key: "type", label: "Typ", description: "Support/Consulting übernehmen, Projekte anlegen oder entfernen." },
|
{ key: "type", label: "Typ", description: "Support/Consulting übernehmen, Projekte anlegen oder entfernen." },
|
||||||
{ key: "projectName", label: "Projektbezeichnung", description: "Teamspace-Projekt bei Consulting überschreiben." },
|
{ key: "projectName", label: "Projektbezeichnung", description: "Teamspace-Projekt bei Consulting überschreiben." },
|
||||||
{ key: "budget", label: "Budget", description: "Teamspace-Stundenkontingent überschreiben." }
|
{ key: "budget", label: "Budget", description: "Teamspace-Stundenkontingent überschreiben." },
|
||||||
|
{ key: "pendingTodo", label: "Termin-ToDo", description: "Aus Wartend/Abschluss mit Datum ein ToDo erzeugen." }
|
||||||
];
|
];
|
||||||
|
|
||||||
type ZammadSyncPageState = {
|
type ZammadSyncPageState = {
|
||||||
@@ -264,6 +266,10 @@ function changeAllowedByFields(item: ZammadSyncItem, change: ZammadSyncChange, f
|
|||||||
return fields.budget && consultingProjectCanBeTouched;
|
return fields.budget && consultingProjectCanBeTouched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (change.field === "ToDo") {
|
||||||
|
return fields.pendingTodo;
|
||||||
|
}
|
||||||
|
|
||||||
if (change.field === "TS/TT-Prüfung") {
|
if (change.field === "TS/TT-Prüfung") {
|
||||||
return consultingProjectCanBeTouched;
|
return consultingProjectCanBeTouched;
|
||||||
}
|
}
|
||||||
@@ -294,7 +300,28 @@ function formatTeamspaceProjectList(value: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function splitTeamspaceProjects(value: unknown) {
|
function splitTeamspaceProjects(value: unknown) {
|
||||||
return typeof value === "string" ? value.split("+").map((entry) => entry.trim()).filter(Boolean) : [];
|
return typeof value === "string" ? value.match(/[^\s+]+/g) ?? [] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function zammadPendingTodoPreview(zammad: Record<string, unknown>) {
|
||||||
|
const stateId = numericValue(cachedValue(zammad, "stateId"));
|
||||||
|
const pendingTime = cachedValue(zammad, "pendingTime");
|
||||||
|
|
||||||
|
if ((stateId !== 3 && stateId !== 7) || typeof pendingTime !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(pendingTime);
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = stateId === 7 ? "Abschluss" : "Erinnerung";
|
||||||
|
const ticketNumber = cachedValue(zammad, "ticketNumber");
|
||||||
|
const ticketLabel = typeof ticketNumber === "string" ? ticketNumber.replace(/^Ticket#?/i, "").replace(/^#/, "") : "";
|
||||||
|
|
||||||
|
return `${kind} ${ticketLabel ? `Ticket#${ticketLabel}` : "Ticket"} (${date.toISOString().slice(0, 10)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function comparablePreviewValue(value: unknown) {
|
function comparablePreviewValue(value: unknown) {
|
||||||
@@ -370,6 +397,11 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
|||||||
addCachedChange(changes, "Zammad-Priorität", cachedValue(local, "zammadPriorityId"), cachedValue(zammad, "priorityId"));
|
addCachedChange(changes, "Zammad-Priorität", cachedValue(local, "zammadPriorityId"), cachedValue(zammad, "priorityId"));
|
||||||
addCachedChange(changes, "Zammad-Status", cachedValue(local, "zammadStateId"), cachedValue(zammad, "stateId"));
|
addCachedChange(changes, "Zammad-Status", cachedValue(local, "zammadStateId"), cachedValue(zammad, "stateId"));
|
||||||
|
|
||||||
|
const pendingTodo = zammadPendingTodoPreview(zammad);
|
||||||
|
if (fields.pendingTodo && pendingTodo) {
|
||||||
|
changes.push({ field: "ToDo", from: null, to: pendingTodo });
|
||||||
|
}
|
||||||
|
|
||||||
if (zammadWorkType === "support" && zammadStateId === 4) {
|
if (zammadWorkType === "support" && zammadStateId === 4) {
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
@@ -532,6 +564,7 @@ function zammadDetails(item: ZammadSyncItem) {
|
|||||||
`Typ ${formatWorkType(zammad.workType)}`,
|
`Typ ${formatWorkType(zammad.workType)}`,
|
||||||
`Status ${formatZammadState(zammad.stateId)}`,
|
`Status ${formatZammadState(zammad.stateId)}`,
|
||||||
`Priorität ${formatZammadPriority(zammad.priorityId)}`,
|
`Priorität ${formatZammadPriority(zammad.priorityId)}`,
|
||||||
|
`Warten ${formatValue(zammad.pendingTime)}`,
|
||||||
`Owner ${formatValue(zammad.ownerId)}`,
|
`Owner ${formatValue(zammad.ownerId)}`,
|
||||||
`Org ${formatValue(zammad.organizationId)}`,
|
`Org ${formatValue(zammad.organizationId)}`,
|
||||||
`Titel ${formatValue(zammad.title)}`,
|
`Titel ${formatValue(zammad.title)}`,
|
||||||
|
|||||||
Reference in new issue
Block a user