Update
This commit is contained in:
1 parent
39690b1d2b
commit
35cb578a96
8 files changed
+1091
-46
No files matched your search
+947
-27
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("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_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
|
||||
|
||||
@@ -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) {
|
||||
return request<{ deleted: { id: string; ticket_id: string } }>(`/api/projects/${projectId}`, {
|
||||
method: "DELETE"
|
||||
|
||||
@@ -180,6 +180,8 @@ export type ProjectSummary = {
|
||||
zammad_priority_id?: string | null;
|
||||
zammad_state_id?: 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;
|
||||
description: string;
|
||||
goals: string;
|
||||
|
||||
@@ -1859,14 +1859,16 @@ function DayControlWidget({ stats, projects, todos, onNavigate }: WidgetContentP
|
||||
empty="Keine Budget-Hinweise."
|
||||
items={budgetAttention.slice(0, 4).map((project) => {
|
||||
const percent = projectConsumptionPercent(project);
|
||||
const hasUnassignedSessions = Number(project.unassigned_teamspace_session_count ?? 0) > 0;
|
||||
const detail = project.close_review_required
|
||||
? "Abschluss prüfen"
|
||||
: hasUnassignedSessions
|
||||
? `${project.unassigned_teamspace_session_count} Session(s) ohne TS-Zuordnung`
|
||||
: `${percent ?? 0}% Kontingent verbraucht`;
|
||||
const note = project.close_review_required
|
||||
? "Abschluss in TT/Teamspace prüfen und ggf. Umbuchungen klären."
|
||||
const hasUnassignedSessions = Number(project.unassigned_teamspace_session_count ?? 0) > 0;
|
||||
const detail = project.close_review_required
|
||||
? project.zammad_merged_from_ticket_id || project.zammad_state_id !== "4" ? "Merge prüfen" : "Abschluss prüfen"
|
||||
: hasUnassignedSessions
|
||||
? `${project.unassigned_teamspace_session_count} Session(s) ohne TS-Zuordnung`
|
||||
: `${percent ?? 0}% Kontingent verbraucht`;
|
||||
const note = project.close_review_required
|
||||
? 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
|
||||
? "Sessions einem Teamspace-Projekt zuordnen."
|
||||
: "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 { OrganizationSelect } from "@/components/OrganizationSelect";
|
||||
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 { projectHasRecurringBilling } from "../recurring-billings";
|
||||
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) {
|
||||
try {
|
||||
await addFavoriteTicket(project.ticket_number);
|
||||
@@ -1769,9 +1784,26 @@ export function ProjectsPage({ onNavigate, onStartTimer }: ProjectsPageProps) {
|
||||
</p>
|
||||
) : null}
|
||||
{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">
|
||||
Zammad meldet das Consulting-Ticket als geschlossen. Bitte Zeiten in Teamspace und TicketTracker prüfen und das Projekt danach schließen.
|
||||
</p>
|
||||
<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">
|
||||
<span>
|
||||
{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}
|
||||
{showBudgetedTeamspaceEntries ? (
|
||||
<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 { TodoPanel } from "@/components/TodoPanel";
|
||||
import {
|
||||
acknowledgeProjectReview,
|
||||
createSession,
|
||||
deleteProject,
|
||||
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) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1879,9 +1906,28 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate, onS
|
||||
) : null}
|
||||
{project.close_review_required ? (
|
||||
<Alert variant="warning">
|
||||
<div className="font-medium">Zeiten prüfen</div>
|
||||
<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.
|
||||
<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">
|
||||
{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>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
@@ -433,7 +433,7 @@ function formatChangeValue(field: string, value: unknown) {
|
||||
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) || "-";
|
||||
}
|
||||
|
||||
@@ -493,6 +493,10 @@ function changeAllowedByFields(item: ZammadSyncItem, change: ZammadSyncChange, f
|
||||
return fields.projectName && consultingProjectCanBeTouched;
|
||||
}
|
||||
|
||||
if (change.field === "Merge-Projektdaten" || change.field === "Merge-Prüfung") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (change.field === "Teamspace-Budget") {
|
||||
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>) {
|
||||
const stateId = numericValue(cachedValue(zammad, "stateId"));
|
||||
const pendingTime = cachedValue(zammad, "pendingTime");
|
||||
@@ -666,6 +674,11 @@ function changesFromExistingPreviewCache(item: ZammadSyncItem, fields: ZammadSyn
|
||||
const localWorkType = cachedValue(local, "workType");
|
||||
const zammadWorkType = cachedValue(zammad, "workType");
|
||||
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 localTeamspaceProjects = cachedStringList(project, "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"));
|
||||
}
|
||||
|
||||
if (fields.type) {
|
||||
if (fields.type && !mergeProtected) {
|
||||
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" });
|
||||
}
|
||||
|
||||
const consultingProjectCanBeTouched = zammadWorkType === "consulting" && (fields.type || localWorkType === "consulting");
|
||||
const consultingProjectCanBeTouched = !mergeProtected && zammadWorkType === "consulting" && (fields.type || localWorkType === "consulting");
|
||||
|
||||
if (consultingProjectCanBeTouched) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user