Update
This commit is contained in:
1 parent
39690b1d2b
commit
35cb578a96
8 files changed
+1081
-36
No files matched your search
+944
-24
File diff suppressed because it is too large.
Load diff
@@ -191,6 +191,8 @@ export async function migrate() {
|
|||||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_state_id BIGINT;");
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_state_id BIGINT;");
|
||||||
await query("UPDATE tickets SET zammad_state_id = 2 WHERE zammad_state_id = 1;");
|
await query("UPDATE tickets SET zammad_state_id = 2 WHERE zammad_state_id = 1;");
|
||||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_owner_id BIGINT;");
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_owner_id BIGINT;");
|
||||||
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_merged_from_ticket_id BIGINT;");
|
||||||
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_merged_from_ticket_number TEXT;");
|
||||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_preferences JSONB NOT NULL DEFAULT '{}'::jsonb;");
|
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS zammad_preferences JSONB NOT NULL DEFAULT '{}'::jsonb;");
|
||||||
await query(`
|
await query(`
|
||||||
ALTER TABLE tickets
|
ALTER TABLE tickets
|
||||||
|
|||||||
@@ -672,6 +672,13 @@ export function updateProjectStatus(projectId: string, closed: boolean) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function acknowledgeProjectReview(projectId: string) {
|
||||||
|
return request<{ project: { id: string; ticket_id: string; close_review_required: boolean } }>(`/api/projects/${projectId}/review`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ acknowledged: true })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteProject(projectId: string) {
|
export function deleteProject(projectId: string) {
|
||||||
return request<{ deleted: { id: string; ticket_id: string } }>(`/api/projects/${projectId}`, {
|
return request<{ deleted: { id: string; ticket_id: string } }>(`/api/projects/${projectId}`, {
|
||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ export type ProjectSummary = {
|
|||||||
zammad_priority_id?: string | null;
|
zammad_priority_id?: string | null;
|
||||||
zammad_state_id?: string | null;
|
zammad_state_id?: string | null;
|
||||||
zammad_pending_time?: string | null;
|
zammad_pending_time?: string | null;
|
||||||
|
zammad_merged_from_ticket_id?: string | null;
|
||||||
|
zammad_merged_from_ticket_number?: string | null;
|
||||||
teamspace_project: string | null;
|
teamspace_project: string | null;
|
||||||
description: string;
|
description: string;
|
||||||
goals: string;
|
goals: string;
|
||||||
|
|||||||
@@ -1861,12 +1861,14 @@ function DayControlWidget({ stats, projects, todos, onNavigate }: WidgetContentP
|
|||||||
const percent = projectConsumptionPercent(project);
|
const percent = projectConsumptionPercent(project);
|
||||||
const hasUnassignedSessions = Number(project.unassigned_teamspace_session_count ?? 0) > 0;
|
const hasUnassignedSessions = Number(project.unassigned_teamspace_session_count ?? 0) > 0;
|
||||||
const detail = project.close_review_required
|
const detail = project.close_review_required
|
||||||
? "Abschluss prüfen"
|
? project.zammad_merged_from_ticket_id || project.zammad_state_id !== "4" ? "Merge prüfen" : "Abschluss prüfen"
|
||||||
: hasUnassignedSessions
|
: hasUnassignedSessions
|
||||||
? `${project.unassigned_teamspace_session_count} Session(s) ohne TS-Zuordnung`
|
? `${project.unassigned_teamspace_session_count} Session(s) ohne TS-Zuordnung`
|
||||||
: `${percent ?? 0}% Kontingent verbraucht`;
|
: `${percent ?? 0}% Kontingent verbraucht`;
|
||||||
const note = project.close_review_required
|
const note = project.close_review_required
|
||||||
? "Abschluss in TT/Teamspace prüfen und ggf. Umbuchungen klären."
|
? project.zammad_merged_from_ticket_id || project.zammad_state_id !== "4"
|
||||||
|
? "Lokale Projektdaten aus dem alten Ticket prüfen und in Zammad nachtragen."
|
||||||
|
: "Abschluss in TT/Teamspace prüfen und ggf. Umbuchungen klären."
|
||||||
: hasUnassignedSessions
|
: hasUnassignedSessions
|
||||||
? "Sessions einem Teamspace-Projekt zuordnen."
|
? "Sessions einem Teamspace-Projekt zuordnen."
|
||||||
: "Kontingent kritisch: Budget, Verbrauch und weitere Arbeit prüfen.";
|
: "Kontingent kritisch: Budget, Verbrauch und weitere Arbeit prüfen.";
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { HelpLink } from "@/components/HelpLink";
|
|||||||
import { MultiFilterDropdown, type MultiFilterOption } from "@/components/MultiFilterDropdown";
|
import { MultiFilterDropdown, type MultiFilterOption } from "@/components/MultiFilterDropdown";
|
||||||
import { OrganizationSelect } from "@/components/OrganizationSelect";
|
import { OrganizationSelect } from "@/components/OrganizationSelect";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { addFavoriteTicket, createProject, getProjects, hideProject, lookupTicket, unhideProject, updateProject, updateProjectStatus } from "../api";
|
import { acknowledgeProjectReview, addFavoriteTicket, createProject, getProjects, hideProject, lookupTicket, unhideProject, updateProject, updateProjectStatus } from "../api";
|
||||||
import { currentMonth, formatMinutes } from "../format";
|
import { currentMonth, formatMinutes } from "../format";
|
||||||
import { projectHasRecurringBilling } from "../recurring-billings";
|
import { projectHasRecurringBilling } from "../recurring-billings";
|
||||||
import { ticketPattern } from "../timers";
|
import { ticketPattern } from "../timers";
|
||||||
@@ -1359,6 +1359,21 @@ export function ProjectsPage({ onNavigate, onStartTimer }: ProjectsPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function acknowledgeProjectReviewCheck(project: ProjectSummary) {
|
||||||
|
setSavingProjectStatusId(project.id);
|
||||||
|
try {
|
||||||
|
await acknowledgeProjectReview(project.id);
|
||||||
|
toast.success("Prüfung bestätigt");
|
||||||
|
await load();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Prüfung konnte nicht bestätigt werden", {
|
||||||
|
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSavingProjectStatusId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveProjectFavorite(project: ProjectSummary) {
|
async function saveProjectFavorite(project: ProjectSummary) {
|
||||||
try {
|
try {
|
||||||
await addFavoriteTicket(project.ticket_number);
|
await addFavoriteTicket(project.ticket_number);
|
||||||
@@ -1769,9 +1784,26 @@ export function ProjectsPage({ onNavigate, onStartTimer }: ProjectsPageProps) {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{project.close_review_required ? (
|
{project.close_review_required ? (
|
||||||
<p className="rounded-md border border-amber-500/30 bg-amber-500/10 px-2 py-1 text-xs text-amber-700 dark:text-amber-300">
|
<div className="flex flex-col gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-2 py-1 text-xs text-amber-700 dark:text-amber-300 sm:flex-row sm:items-center sm:justify-between">
|
||||||
Zammad meldet das Consulting-Ticket als geschlossen. Bitte Zeiten in Teamspace und TicketTracker prüfen und das Projekt danach schließen.
|
<span>
|
||||||
</p>
|
{project.zammad_merged_from_ticket_id || project.zammad_state_id !== "4"
|
||||||
|
? project.zammad_merged_from_ticket_id
|
||||||
|
? `Dieses Projekt wurde aus ${project.zammad_merged_from_ticket_number ?? `Zammad-ID ${project.zammad_merged_from_ticket_id}`} übernommen. Teamspace-Projekte und Budget bleiben lokal erhalten; bitte in Zammad prüfen und nachtragen.`
|
||||||
|
: "Dieses Projekt steht zur TS/TT-Prüfung, obwohl das Zammad-Ticket offen ist. Teamspace-Projekte und Budget bleiben lokal erhalten; bitte in Zammad prüfen und nachtragen."
|
||||||
|
: "Zammad meldet das Consulting-Ticket als geschlossen. Bitte Zeiten in Teamspace und TicketTracker prüfen und das Projekt danach schließen."}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 shrink-0 px-2 text-xs"
|
||||||
|
disabled={savingProjectStatusId === project.id || project.can_edit === false}
|
||||||
|
onClick={() => void acknowledgeProjectReviewCheck(project)}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
Geprüft
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{showBudgetedTeamspaceEntries ? (
|
{showBudgetedTeamspaceEntries ? (
|
||||||
<div className="grid gap-1.5 md:grid-cols-2">
|
<div className="grid gap-1.5 md:grid-cols-2">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { OrganizationSelect } from "@/components/OrganizationSelect";
|
|||||||
import { readRichActivityEditorValue, RichActivityEditor } from "@/components/RichActivityEditor";
|
import { readRichActivityEditorValue, RichActivityEditor } from "@/components/RichActivityEditor";
|
||||||
import { TodoPanel } from "@/components/TodoPanel";
|
import { TodoPanel } from "@/components/TodoPanel";
|
||||||
import {
|
import {
|
||||||
|
acknowledgeProjectReview,
|
||||||
createSession,
|
createSession,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
@@ -1406,6 +1407,32 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function acknowledgeProjectReviewCheck() {
|
||||||
|
if (!project) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.can_edit === false) {
|
||||||
|
toast.error("Projekt ist geteilt", {
|
||||||
|
description: "Nur der Zammad-Owner kann die Prüfung bestätigen."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSavingProjectStatus(true);
|
||||||
|
try {
|
||||||
|
await acknowledgeProjectReview(project.id);
|
||||||
|
toast.success("Prüfung bestätigt");
|
||||||
|
await load();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Prüfung konnte nicht bestätigt werden", {
|
||||||
|
description: error instanceof Error ? error.message : "Unbekannter Fehler"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSavingProjectStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitTicketSession(event: FormEvent) {
|
async function submitTicketSession(event: FormEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -1879,9 +1906,28 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
|||||||
) : null}
|
) : null}
|
||||||
{project.close_review_required ? (
|
{project.close_review_required ? (
|
||||||
<Alert variant="warning">
|
<Alert variant="warning">
|
||||||
<div className="font-medium">Zeiten prüfen</div>
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="font-medium">{project.zammad_merged_from_ticket_id || project.zammad_state_id !== "4" ? "Merge prüfen" : "Zeiten prüfen"}</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Zammad meldet dieses Consulting-Ticket als geschlossen. Bitte Zeiten in Teamspace und TicketTracker prüfen und das Projekt danach schließen.
|
{project.zammad_merged_from_ticket_id || project.zammad_state_id !== "4"
|
||||||
|
? project.zammad_merged_from_ticket_id
|
||||||
|
? `Dieses Projekt wurde aus ${project.zammad_merged_from_ticket_number ?? `Zammad-ID ${project.zammad_merged_from_ticket_id}`} übernommen. Teamspace-Projekte, Budget und Zuordnungen bleiben lokal erhalten; bitte in Zammad prüfen und nachtragen.`
|
||||||
|
: "Dieses Projekt steht zur TS/TT-Prüfung, obwohl das Zammad-Ticket offen ist. Teamspace-Projekte, Budget und Zuordnungen bleiben lokal erhalten; bitte in Zammad prüfen und nachtragen."
|
||||||
|
: "Zammad meldet dieses Consulting-Ticket als geschlossen. Bitte Zeiten in Teamspace und TicketTracker prüfen und das Projekt danach schließen."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0"
|
||||||
|
disabled={savingProjectStatus || project.can_edit === false}
|
||||||
|
onClick={() => void acknowledgeProjectReviewCheck()}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||||
|
Geprüft
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ function formatChangeValue(field: string, value: unknown) {
|
|||||||
return formatMinutes(value);
|
return formatMinutes(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === "Teamspace-Projekte" || field === "Teamspace-Prüfung") {
|
if (field === "Teamspace-Projekte" || field === "Teamspace-Prüfung" || field === "Merge-Projektdaten") {
|
||||||
return formatTeamspaceProjectList(value) || "-";
|
return formatTeamspaceProjectList(value) || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +493,10 @@ function changeAllowedByFields(item: ZammadSyncItem, change: ZammadSyncChange, f
|
|||||||
return fields.projectName && consultingProjectCanBeTouched;
|
return fields.projectName && consultingProjectCanBeTouched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (change.field === "Merge-Projektdaten" || change.field === "Merge-Prüfung") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (change.field === "Teamspace-Budget") {
|
if (change.field === "Teamspace-Budget") {
|
||||||
return fields.budget && consultingProjectCanBeTouched;
|
return fields.budget && consultingProjectCanBeTouched;
|
||||||
}
|
}
|
||||||
@@ -578,6 +582,10 @@ function addCachedTeamspaceProjectChanges(changes: ZammadSyncChange[], localProj
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeReviewMatchesCachedZammad(localTeamspaceProjects: unknown, zammadTeamspaceProjects: unknown) {
|
||||||
|
return !cachedValueChanged("Teamspace-Projekte", localTeamspaceProjects, zammadTeamspaceProjects);
|
||||||
|
}
|
||||||
|
|
||||||
function zammadPendingTodoPreview(zammad: Record<string, unknown>) {
|
function zammadPendingTodoPreview(zammad: Record<string, unknown>) {
|
||||||
const stateId = numericValue(cachedValue(zammad, "stateId"));
|
const stateId = numericValue(cachedValue(zammad, "stateId"));
|
||||||
const pendingTime = cachedValue(zammad, "pendingTime");
|
const pendingTime = cachedValue(zammad, "pendingTime");
|
||||||
@@ -666,6 +674,11 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
|||||||
const localWorkType = cachedValue(local, "workType");
|
const localWorkType = cachedValue(local, "workType");
|
||||||
const zammadWorkType = cachedValue(zammad, "workType");
|
const zammadWorkType = cachedValue(zammad, "workType");
|
||||||
const zammadStateId = numericValue(cachedValue(zammad, "stateId"));
|
const zammadStateId = numericValue(cachedValue(zammad, "stateId"));
|
||||||
|
const mergeProtected = Boolean(
|
||||||
|
cachedValue(local, "zammadMergedFromTicketId")
|
||||||
|
|| cachedValue(zammad, "mergedFromTicketId")
|
||||||
|
|| (cachedValue(project, "closeReviewRequired") === true && zammadStateId !== 4)
|
||||||
|
);
|
||||||
const pendingTodoExists = cachedValue(local, "zammadPendingTodoExists");
|
const pendingTodoExists = cachedValue(local, "zammadPendingTodoExists");
|
||||||
const localTeamspaceProjects = cachedStringList(project, "teamspaceProjects");
|
const localTeamspaceProjects = cachedStringList(project, "teamspaceProjects");
|
||||||
const zammadTeamspaceProjects = cachedStringList(zammad, "teamspaceProjects");
|
const zammadTeamspaceProjects = cachedStringList(zammad, "teamspaceProjects");
|
||||||
@@ -694,15 +707,15 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
|||||||
addCachedChange(changes, "Organisation", cachedValue(local, "organizationId"), cachedValue(zammad, "organizationId"));
|
addCachedChange(changes, "Organisation", cachedValue(local, "organizationId"), cachedValue(zammad, "organizationId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fields.type) {
|
if (fields.type && !mergeProtected) {
|
||||||
addCachedChange(changes, "Typ", localWorkType, zammadWorkType);
|
addCachedChange(changes, "Typ", localWorkType, zammadWorkType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fields.type && zammadWorkType === "support" && project) {
|
if (!mergeProtected && fields.type && zammadWorkType === "support" && project) {
|
||||||
changes.push({ field: "Projekt/Teamspace-Daten", from: "vorhanden", to: "werden gelöscht" });
|
changes.push({ field: "Projekt/Teamspace-Daten", from: "vorhanden", to: "werden gelöscht" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const consultingProjectCanBeTouched = zammadWorkType === "consulting" && (fields.type || localWorkType === "consulting");
|
const consultingProjectCanBeTouched = !mergeProtected && zammadWorkType === "consulting" && (fields.type || localWorkType === "consulting");
|
||||||
|
|
||||||
if (consultingProjectCanBeTouched) {
|
if (consultingProjectCanBeTouched) {
|
||||||
if (fields.type && !project) {
|
if (fields.type && !project) {
|
||||||
@@ -722,6 +735,27 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mergeProtected && project && cachedValue(project, "closeReviewRequired") === true && zammadStateId !== 4) {
|
||||||
|
if (mergeReviewMatchesCachedZammad(localTeamspaceProjectList, zammadTeamspaceProjectList)) {
|
||||||
|
changes.push({ field: "Merge-Prüfung", from: true, to: false });
|
||||||
|
} else {
|
||||||
|
if (localTeamspaceProjectList.length > 0 && zammadTeamspaceProjectList.length === 0) {
|
||||||
|
changes.push({
|
||||||
|
field: "Merge-Projektdaten",
|
||||||
|
from: normalizedTeamspaceProjectList(localTeamspaceProjectList),
|
||||||
|
to: "in Zammad fehlen; lokale Projektdaten bleiben erhalten und muessen dort nachgetragen werden"
|
||||||
|
});
|
||||||
|
} else if (cachedValueChanged("Teamspace-Projekte", localTeamspaceProjectList, zammadTeamspaceProjectList)) {
|
||||||
|
changes.push({
|
||||||
|
field: "Merge-Projektdaten",
|
||||||
|
from: normalizedTeamspaceProjectList(localTeamspaceProjectList),
|
||||||
|
to: normalizedTeamspaceProjectList(zammadTeamspaceProjectList)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user