Ergänze Entwurfsstatus für Termine und Plantafel

This commit is contained in:
2026-05-19 12:18:30 +02:00
parent 0ac22d346f
commit ea392af094
6 changed files with 176 additions and 7 deletions

View File

@@ -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;

View File

@@ -0,0 +1,5 @@
ALTER TABLE "events" ADD COLUMN "state" text DEFAULT 'Final' NOT NULL;
UPDATE "events"
SET "state" = 'Final'
WHERE "state" IS NULL;

View File

@@ -243,29 +243,36 @@
{ {
"idx": 34, "idx": 34,
"version": "7", "version": "7",
"when": 1777420800000,
"tag": "0034_events_color",
"breakpoints": true
},
{
"idx": 35,
"version": "7",
"when": 1778191200000, "when": 1778191200000,
"tag": "0035_contract_history", "tag": "0035_contract_history",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 35, "idx": 36,
"version": "7", "version": "7",
"when": 1778194800000, "when": 1778194800000,
"tag": "0036_allowed_contracttypes", "tag": "0036_allowed_contracttypes",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 36, "idx": 37,
"version": "7", "version": "7",
"when": 1778840100000, "when": 1778840100000,
"tag": "0037_outgoing_sepa_mandates", "tag": "0037_outgoing_sepa_mandates",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 37, "idx": 38,
"version": "7", "version": "7",
"when": 1778840200000, "when": 1779158400000,
"tag": "0034_profile_availability_note", "tag": "0038_events_state",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -32,6 +32,8 @@ export const events = pgTable(
eventtype: text("eventtype").default("Umsetzung"), eventtype: text("eventtype").default("Umsetzung"),
quick: boolean("quick").notNull().default(false), 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 project: bigint("project", { mode: "number" }), // FK follows when projects.ts exists

View File

@@ -30,6 +30,9 @@ const resources = ref([])
const events = ref([]) const events = ref([])
const profiles = ref([]) const profiles = ref([])
const inventoryitems = ref([]) const inventoryitems = ref([])
const isDraftModeActive = ref(false)
const isFinalizeDraftsModalOpen = ref(false)
const finalizingDrafts = ref(false)
const savingQuickConfig = ref(false) const savingQuickConfig = ref(false)
const isQuickConfigModalOpen = ref(false) const isQuickConfigModalOpen = ref(false)
const quickConfigWindowEl = ref(null) 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(() => ({ const calendarOptions = computed(() => ({
schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives", schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
locale: deLocale, locale: deLocale,
@@ -388,10 +399,15 @@ function resolveEventTitle(event, projectsById) {
} }
function resolveRenderedEventColor(event) { function resolveRenderedEventColor(event) {
if (event?.quick) return activeQuickEntryConfig.value.color if (event?.quick) return event?.color || activeQuickEntryConfig.value.color
return resolveEventColor(event.eventtype) return resolveEventColor(event.eventtype)
} }
function resolveDisplayedEventTitle(event, projectsById) {
const baseTitle = resolveEventTitle(event, projectsById)
return event?.state === "Entwurf" ? `[Entwurf] ${baseTitle}` : baseTitle
}
function getProfileLabel(profile) { function getProfileLabel(profile) {
return profile?.full_name || profile?.fullName || [profile?.first_name, profile?.last_name].filter(Boolean).join(" ") || profile?.email || `Profil ${profile?.id || ""}`.trim() 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 { return {
title: resolveEventTitle(event, projectsById), title: resolveDisplayedEventTitle(event, projectsById),
start: event.startDate, start: event.startDate,
end: event.endDate, end: event.endDate,
resourceIds, resourceIds,
color: event.color || null,
state: event.state || "Final",
backgroundColor: resolveRenderedEventColor(event), backgroundColor: resolveRenderedEventColor(event),
borderColor: resolveRenderedEventColor(event), borderColor: resolveRenderedEventColor(event),
textColor: "#ffffff", textColor: "#ffffff",
classNames: event.state === "Entwurf" ? ["planning-board-draft-event"] : [],
entrytype: "event", entrytype: "event",
eventId: event.id eventId: event.id
} }
@@ -755,6 +774,8 @@ async function createQuickEvent(info) {
const payload = { const payload = {
name: activeQuickEntryConfig.value.name, name: activeQuickEntryConfig.value.name,
quick: true, quick: true,
state: isDraftModeActive.value ? "Entwurf" : "Final",
color: activeQuickEntryConfig.value.color,
startDate: info.startStr, startDate: info.startStr,
endDate: info.endStr, endDate: info.endStr,
profiles: resourceIds 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 = {}) { function openAbsenceModal(type = "vacation", preset = {}) {
absenceForm.mode = preset.entry ? "edit" : "create" absenceForm.mode = preset.entry ? "edit" : "create"
absenceForm.entry = preset.entry || null absenceForm.entry = preset.entry || null
@@ -983,6 +1052,14 @@ onMounted(() => {
> >
Quick-Einträge Quick-Einträge
</UButton> </UButton>
<UButton
:color="isDraftModeActive ? 'amber' : 'neutral'"
:variant="isDraftModeActive ? 'solid' : 'outline'"
icon="i-heroicons-document-duplicate"
@click="toggleDraftMode"
>
{{ isDraftModeActive ? "Entwurfsmodus aktiv" : "Entwurfsmodus" }}
</UButton>
<UButton <UButton
color="amber" color="amber"
variant="soft" variant="soft"
@@ -1211,6 +1288,53 @@ onMounted(() => {
</div> </div>
</div> </div>
<UModal v-model:open="isFinalizeDraftsModalOpen">
<template #content>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between gap-3">
<div>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Entwürfe finalisieren
</h3>
<p class="text-sm text-muted">
Beim Beenden des Entwurfsmodus können die aktuell sichtbaren Entwürfe finalisiert werden.
</p>
</div>
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
@click="isFinalizeDraftsModalOpen = false"
/>
</div>
</template>
<div class="space-y-4">
<UAlert
color="amber"
variant="soft"
icon="i-heroicons-exclamation-triangle"
:title="`${visibleDraftEventIds.length} sichtbare Entwürfe gefunden`"
description="Diese Termine werden beim Finalisieren auf den Status Final gesetzt."
/>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<UButton color="gray" variant="soft" @click="isFinalizeDraftsModalOpen = false">
Abbrechen
</UButton>
<UButton color="primary" :loading="finalizingDrafts" @click="finalizeVisibleDrafts">
Entwürfe finalisieren
</UButton>
</div>
</template>
</UCard>
</template>
</UModal>
<UModal v-model:open="isAbsenceModalOpen"> <UModal v-model:open="isAbsenceModalOpen">
<template #content> <template #content>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
@@ -1506,3 +1630,13 @@ onMounted(() => {
</div> </div>
</div> </div>
</template> </template>
<style scoped>
:deep(.planning-board-draft-event) {
opacity: 0.7;
}
:deep(.planning-board-draft-event .fc-timeline-event) {
border-style: dashed;
}
</style>

View File

@@ -3089,6 +3089,19 @@ export const useDataStore = defineStore('data', () => {
inputType: "bool", inputType: "bool",
sortable: true sortable: true
}, },
{
key: "state",
label: "Status",
inputType: "select",
selectManualOptions: ["Entwurf", "Final"],
sortable: true
},
{
key: "color",
label: "Farbe",
inputType: "text",
sortable: true
},
{ {
key: "startDate", key: "startDate",
label: "Start", label: "Start",