diff --git a/backend/db/migrations/0034_events_color.sql b/backend/db/migrations/0034_events_color.sql new file mode 100644 index 0000000..b736953 --- /dev/null +++ b/backend/db/migrations/0034_events_color.sql @@ -0,0 +1,8 @@ +ALTER TABLE "events" ADD COLUMN "color" text; + +UPDATE "events" AS e +SET "color" = COALESCE(t."calendarConfig"->'quickEntry'->>'color', '#2563eb') +FROM "tenants" AS t +WHERE e."tenant" = t."id" + AND e."quick" = true + AND e."color" IS NULL; diff --git a/backend/db/migrations/0038_events_state.sql b/backend/db/migrations/0038_events_state.sql new file mode 100644 index 0000000..3b3d65b --- /dev/null +++ b/backend/db/migrations/0038_events_state.sql @@ -0,0 +1,5 @@ +ALTER TABLE "events" ADD COLUMN "state" text DEFAULT 'Final' NOT NULL; + +UPDATE "events" +SET "state" = 'Final' +WHERE "state" IS NULL; diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json index 30fd6a4..cb288c3 100644 --- a/backend/db/migrations/meta/_journal.json +++ b/backend/db/migrations/meta/_journal.json @@ -243,29 +243,36 @@ { "idx": 34, "version": "7", + "when": 1777420800000, + "tag": "0034_events_color", + "breakpoints": true + }, + { + "idx": 35, + "version": "7", "when": 1778191200000, "tag": "0035_contract_history", "breakpoints": true }, { - "idx": 35, + "idx": 36, "version": "7", "when": 1778194800000, "tag": "0036_allowed_contracttypes", "breakpoints": true }, { - "idx": 36, + "idx": 37, "version": "7", "when": 1778840100000, "tag": "0037_outgoing_sepa_mandates", "breakpoints": true }, { - "idx": 37, + "idx": 38, "version": "7", - "when": 1778840200000, - "tag": "0034_profile_availability_note", + "when": 1779158400000, + "tag": "0038_events_state", "breakpoints": true } ] diff --git a/backend/db/schema/events.ts b/backend/db/schema/events.ts index 0f435ba..16e8e0a 100644 --- a/backend/db/schema/events.ts +++ b/backend/db/schema/events.ts @@ -32,6 +32,8 @@ export const events = pgTable( eventtype: text("eventtype").default("Umsetzung"), quick: boolean("quick").notNull().default(false), + state: text("state").notNull().default("Final"), + color: text("color"), project: bigint("project", { mode: "number" }), // FK follows when projects.ts exists diff --git a/frontend/pages/organisation/plantafel.vue b/frontend/pages/organisation/plantafel.vue index 00386cb..0545db3 100644 --- a/frontend/pages/organisation/plantafel.vue +++ b/frontend/pages/organisation/plantafel.vue @@ -30,6 +30,9 @@ const resources = ref([]) const events = ref([]) const profiles = ref([]) const inventoryitems = ref([]) +const isDraftModeActive = ref(false) +const isFinalizeDraftsModalOpen = ref(false) +const finalizingDrafts = ref(false) const savingQuickConfig = ref(false) const isQuickConfigModalOpen = ref(false) const quickConfigWindowEl = ref(null) @@ -273,6 +276,14 @@ const visibleEvents = computed(() => { ) }) +const visibleDraftEventIds = computed(() => + [...new Set( + visibleEvents.value + .filter((event) => event.state === "Entwurf" && event.entrytype === "event" && event.eventId) + .map((event) => event.eventId) + )] +) + const calendarOptions = computed(() => ({ schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives", locale: deLocale, @@ -388,10 +399,15 @@ function resolveEventTitle(event, projectsById) { } function resolveRenderedEventColor(event) { - if (event?.quick) return activeQuickEntryConfig.value.color + if (event?.quick) return event?.color || activeQuickEntryConfig.value.color return resolveEventColor(event.eventtype) } +function resolveDisplayedEventTitle(event, projectsById) { + const baseTitle = resolveEventTitle(event, projectsById) + return event?.state === "Entwurf" ? `[Entwurf] ${baseTitle}` : baseTitle +} + function getProfileLabel(profile) { return profile?.full_name || profile?.fullName || [profile?.first_name, profile?.last_name].filter(Boolean).join(" ") || profile?.email || `Profil ${profile?.id || ""}`.trim() } @@ -527,13 +543,16 @@ function buildEvents({ rawEvents, projectsById }) { ] return { - title: resolveEventTitle(event, projectsById), + title: resolveDisplayedEventTitle(event, projectsById), start: event.startDate, end: event.endDate, resourceIds, + color: event.color || null, + state: event.state || "Final", backgroundColor: resolveRenderedEventColor(event), borderColor: resolveRenderedEventColor(event), textColor: "#ffffff", + classNames: event.state === "Entwurf" ? ["planning-board-draft-event"] : [], entrytype: "event", eventId: event.id } @@ -755,6 +774,8 @@ async function createQuickEvent(info) { const payload = { name: activeQuickEntryConfig.value.name, quick: true, + state: isDraftModeActive.value ? "Entwurf" : "Final", + color: activeQuickEntryConfig.value.color, startDate: info.startStr, endDate: info.endStr, profiles: resourceIds @@ -786,6 +807,54 @@ async function createQuickEvent(info) { } } +function toggleDraftMode() { + if (isDraftModeActive.value) { + if (!visibleDraftEventIds.value.length) { + isDraftModeActive.value = false + toast.add({ title: "Entwurfsmodus deaktiviert", color: "green" }) + return + } + + isFinalizeDraftsModalOpen.value = true + return + } + + isDraftModeActive.value = true + toast.add({ title: "Entwurfsmodus aktiviert", description: "Neue Schichten werden als Entwurf angelegt.", color: "green" }) +} + +async function finalizeVisibleDrafts() { + if (finalizingDrafts.value || !visibleDraftEventIds.value.length) return + + finalizingDrafts.value = true + const draftCount = visibleDraftEventIds.value.length + + try { + await Promise.all( + visibleDraftEventIds.value.map((eventId) => + $api(`/api/resource/events/${eventId}`, { + method: "PUT", + body: { state: "Final" } + }) + ) + ) + + isDraftModeActive.value = false + isFinalizeDraftsModalOpen.value = false + toast.add({ title: "Entwürfe finalisiert", description: `${draftCount} Termine wurden finalisiert.`, color: "green" }) + await loadPlanningBoard() + } catch (error) { + console.error("finalizeVisibleDrafts failed", error) + toast.add({ + title: "Entwürfe konnten nicht finalisiert werden", + description: error?.message || "Bitte erneut versuchen.", + color: "red" + }) + } finally { + finalizingDrafts.value = false + } +} + function openAbsenceModal(type = "vacation", preset = {}) { absenceForm.mode = preset.entry ? "edit" : "create" absenceForm.entry = preset.entry || null @@ -983,6 +1052,14 @@ onMounted(() => { > Quick-Einträge + + {{ isDraftModeActive ? "Entwurfsmodus aktiv" : "Entwurfsmodus" }} + { + + + + + + + + Entwürfe finalisieren + + + Beim Beenden des Entwurfsmodus können die aktuell sichtbaren Entwürfe finalisiert werden. + + + + + + + + + + + + + + Abbrechen + + + Entwürfe finalisieren + + + + + + + @@ -1506,3 +1630,13 @@ onMounted(() => { + + diff --git a/frontend/stores/data.js b/frontend/stores/data.js index bf14ac9..c355530 100644 --- a/frontend/stores/data.js +++ b/frontend/stores/data.js @@ -3089,6 +3089,19 @@ export const useDataStore = defineStore('data', () => { inputType: "bool", sortable: true }, + { + key: "state", + label: "Status", + inputType: "select", + selectManualOptions: ["Entwurf", "Final"], + sortable: true + }, + { + key: "color", + label: "Farbe", + inputType: "text", + sortable: true + }, { key: "startDate", label: "Start",
+ Beim Beenden des Entwurfsmodus können die aktuell sichtbaren Entwürfe finalisiert werden. +