Time Changes
Some checks failed
Build and Push Docker Images / verify-docs-sync (push) Failing after 9s
Build and Push Docker Images / build-backend (push) Has been skipped
Build and Push Docker Images / build-frontend (push) Has been skipped
Build and Push Docker Images / build-docs (push) Has been skipped
Some checks failed
Build and Push Docker Images / verify-docs-sync (push) Failing after 9s
Build and Push Docker Images / build-backend (push) Has been skipped
Build and Push Docker Images / build-frontend (push) Has been skipped
Build and Push Docker Images / build-docs (push) Has been skipped
This commit is contained in:
@@ -9,12 +9,15 @@ export type DerivedSpan = {
|
|||||||
sourceEventIds: string[];
|
sourceEventIds: string[];
|
||||||
status: SpanStatus;
|
status: SpanStatus;
|
||||||
statusActorId?: string;
|
statusActorId?: string;
|
||||||
|
payload?: Record<string, any> | null;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TimeEvent = {
|
type TimeEvent = {
|
||||||
id: string;
|
id: string;
|
||||||
eventtype: string;
|
eventtype: string;
|
||||||
eventtime: Date;
|
eventtime: Date;
|
||||||
|
payload?: Record<string, any> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Liste aller faktischen Event-Typen, die die Zustandsmaschine beeinflussen
|
// Liste aller faktischen Event-Typen, die die Zustandsmaschine beeinflussen
|
||||||
@@ -45,9 +48,17 @@ export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] {
|
|||||||
let currentStart: Date | null = null;
|
let currentStart: Date | null = null;
|
||||||
let currentType: DerivedSpan["type"] | null = null;
|
let currentType: DerivedSpan["type"] | null = null;
|
||||||
let sourceEventIds: string[] = [];
|
let sourceEventIds: string[] = [];
|
||||||
|
let currentPayload: Record<string, any> | null = null;
|
||||||
|
|
||||||
const closeSpan = (end: Date) => {
|
const closeSpan = (end: Date) => {
|
||||||
if (!currentStart || !currentType) return;
|
if (!currentStart || !currentType) return;
|
||||||
|
if (end.getTime() <= currentStart.getTime()) {
|
||||||
|
currentStart = null;
|
||||||
|
currentType = null;
|
||||||
|
sourceEventIds = [];
|
||||||
|
currentPayload = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
spans.push({
|
spans.push({
|
||||||
type: currentType,
|
type: currentType,
|
||||||
@@ -55,12 +66,15 @@ export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] {
|
|||||||
endedAt: end,
|
endedAt: end,
|
||||||
sourceEventIds: [...sourceEventIds],
|
sourceEventIds: [...sourceEventIds],
|
||||||
// Standardstatus ist "factual", wird später angereichert
|
// Standardstatus ist "factual", wird später angereichert
|
||||||
status: "factual"
|
status: "factual",
|
||||||
|
payload: currentPayload,
|
||||||
|
description: currentPayload?.description || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
currentStart = null;
|
currentStart = null;
|
||||||
currentType = null;
|
currentType = null;
|
||||||
sourceEventIds = [];
|
sourceEventIds = [];
|
||||||
|
currentPayload = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeOpenSpanAsRunning = () => {
|
const closeOpenSpanAsRunning = () => {
|
||||||
@@ -72,12 +86,15 @@ export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] {
|
|||||||
endedAt: null,
|
endedAt: null,
|
||||||
sourceEventIds: [...sourceEventIds],
|
sourceEventIds: [...sourceEventIds],
|
||||||
// Standardstatus ist "factual", wird später angereichert
|
// Standardstatus ist "factual", wird später angereichert
|
||||||
status: "factual"
|
status: "factual",
|
||||||
|
payload: currentPayload,
|
||||||
|
description: currentPayload?.description || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
currentStart = null;
|
currentStart = null;
|
||||||
currentType = null;
|
currentType = null;
|
||||||
sourceEventIds = [];
|
sourceEventIds = [];
|
||||||
|
currentPayload = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
@@ -96,6 +113,7 @@ export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] {
|
|||||||
state = "WORKING";
|
state = "WORKING";
|
||||||
currentStart = event.eventtime;
|
currentStart = event.eventtime;
|
||||||
currentType = "work";
|
currentType = "work";
|
||||||
|
currentPayload = event.payload || null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "pause_start":
|
case "pause_start":
|
||||||
@@ -104,6 +122,7 @@ export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] {
|
|||||||
state = "PAUSED";
|
state = "PAUSED";
|
||||||
currentStart = event.eventtime;
|
currentStart = event.eventtime;
|
||||||
currentType = "pause";
|
currentType = "pause";
|
||||||
|
currentPayload = event.payload || null;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -113,6 +132,7 @@ export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] {
|
|||||||
state = "WORKING";
|
state = "WORKING";
|
||||||
currentStart = event.eventtime;
|
currentStart = event.eventtime;
|
||||||
currentType = "work";
|
currentType = "work";
|
||||||
|
currentPayload = event.payload || null;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -140,6 +160,7 @@ export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] {
|
|||||||
state = "ABSENT";
|
state = "ABSENT";
|
||||||
currentStart = event.eventtime;
|
currentStart = event.eventtime;
|
||||||
currentType = newType;
|
currentType = newType;
|
||||||
|
currentPayload = event.payload || null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "vacation_end":
|
case "vacation_end":
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/services/loadValidEvents.ts
|
// src/services/loadValidEvents.ts
|
||||||
|
|
||||||
import { stafftimeevents } from "../../../db/schema";
|
import { stafftimeevents } from "../../../db/schema";
|
||||||
import {sql, and, eq, gte, lte, inArray} from "drizzle-orm";
|
import {sql, and, eq, gte, lte, inArray, asc} from "drizzle-orm";
|
||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
|
|
||||||
export type TimeType = "work_start" | "work_end" | "pause_start" | "pause_end" | "sick_start" | "sick_end" | "vacation_start" | "vacation_end" | "ovetime_compensation_start" | "overtime_compensation_end";
|
export type TimeType = "work_start" | "work_end" | "pause_start" | "pause_end" | "sick_start" | "sick_end" | "vacation_start" | "vacation_end" | "ovetime_compensation_start" | "overtime_compensation_end";
|
||||||
@@ -12,11 +12,43 @@ export type TimeEvent = {
|
|||||||
id: string;
|
id: string;
|
||||||
eventtype: string;
|
eventtype: string;
|
||||||
eventtime: Date;
|
eventtime: Date;
|
||||||
actoruser_id: string;
|
actoruser_id?: string;
|
||||||
related_event_id: string | null;
|
related_event_id: string | null;
|
||||||
// Fügen Sie hier alle weiteren Felder hinzu, die Sie benötigen
|
payload?: Record<string, any> | null;
|
||||||
|
created_at?: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EVENT_TYPE_ORDER: Record<string, number> = {
|
||||||
|
auto_stop: 10,
|
||||||
|
work_end: 10,
|
||||||
|
pause_end: 10,
|
||||||
|
vacation_end: 10,
|
||||||
|
sick_end: 10,
|
||||||
|
overtime_compensation_end: 10,
|
||||||
|
work_start: 20,
|
||||||
|
pause_start: 20,
|
||||||
|
vacation_start: 20,
|
||||||
|
sick_start: 20,
|
||||||
|
overtime_compensation_start: 20,
|
||||||
|
submitted: 30,
|
||||||
|
approved: 30,
|
||||||
|
rejected: 30,
|
||||||
|
invalidated: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function compareTimeEvents(a: TimeEvent, b: TimeEvent) {
|
||||||
|
const eventTimeDiff = a.eventtime.getTime() - b.eventtime.getTime();
|
||||||
|
if (eventTimeDiff !== 0) return eventTimeDiff;
|
||||||
|
|
||||||
|
const typeOrderDiff = (EVENT_TYPE_ORDER[a.eventtype] ?? 999) - (EVENT_TYPE_ORDER[b.eventtype] ?? 999);
|
||||||
|
if (typeOrderDiff !== 0) return typeOrderDiff;
|
||||||
|
|
||||||
|
const createdAtDiff = (a.created_at?.getTime() ?? 0) - (b.created_at?.getTime() ?? 0);
|
||||||
|
if (createdAtDiff !== 0) return createdAtDiff;
|
||||||
|
|
||||||
|
return a.id.localeCompare(b.id);
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadValidEvents(
|
export async function loadValidEvents(
|
||||||
server: FastifyInstance,
|
server: FastifyInstance,
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
@@ -62,10 +94,9 @@ export async function loadValidEvents(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderBy(
|
.orderBy(
|
||||||
// Wichtig für die deriveTimeSpans Logik: Event-Zeit muss primär sein
|
asc(baseEvents.eventtime),
|
||||||
baseEvents.eventtime,
|
asc(baseEvents.created_at),
|
||||||
baseEvents.created_at, // Wenn Eventtime identisch ist, das später erstellte zuerst
|
asc(baseEvents.id)
|
||||||
baseEvents.id
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mapping auf den sauberen TimeEvent Typ
|
// Mapping auf den sauberen TimeEvent Typ
|
||||||
@@ -73,8 +104,10 @@ export async function loadValidEvents(
|
|||||||
id: e.id,
|
id: e.id,
|
||||||
eventtype: e.eventtype,
|
eventtype: e.eventtype,
|
||||||
eventtime: e.eventtime,
|
eventtime: e.eventtime,
|
||||||
// Fügen Sie hier alle weiteren benötigten Felder hinzu (z.B. metadata, actoruser_id)
|
actoruser_id: e.actoruser_id,
|
||||||
// ...
|
related_event_id: e.related_event_id,
|
||||||
|
payload: e.payload,
|
||||||
|
created_at: e.created_at,
|
||||||
})) as TimeEvent[];
|
})) as TimeEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +132,11 @@ export async function loadRelatedAdminEvents(server, eventIds) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
// Muss nach Zeit sortiert sein, um den Status korrekt zu bestimmen!
|
// Muss nach Zeit sortiert sein, um den Status korrekt zu bestimmen!
|
||||||
.orderBy(stafftimeevents.eventtime);
|
.orderBy(
|
||||||
|
asc(stafftimeevents.eventtime),
|
||||||
|
asc(stafftimeevents.created_at),
|
||||||
|
asc(stafftimeevents.id)
|
||||||
|
);
|
||||||
|
|
||||||
return adminEvents;
|
return adminEvents as TimeEvent[];
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
desc
|
desc
|
||||||
} from "drizzle-orm"
|
} from "drizzle-orm"
|
||||||
import {stafftimeevents} from "../../../db/schema/staff_time_events";
|
import {stafftimeevents} from "../../../db/schema/staff_time_events";
|
||||||
import {loadRelatedAdminEvents, loadValidEvents} from "../../modules/time/loadvalidevents.service";
|
import {compareTimeEvents, loadRelatedAdminEvents, loadValidEvents} from "../../modules/time/loadvalidevents.service";
|
||||||
import {deriveTimeSpans} from "../../modules/time/derivetimespans.service";
|
import {deriveTimeSpans} from "../../modules/time/derivetimespans.service";
|
||||||
import {buildTimeEvaluationFromSpans} from "../../modules/time/buildtimeevaluation.service";
|
import {buildTimeEvaluationFromSpans} from "../../modules/time/buildtimeevaluation.service";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
@@ -102,7 +102,7 @@ export default async function staffTimeRoutesInternal(server: FastifyInstance) {
|
|||||||
const combinedEvents = [
|
const combinedEvents = [
|
||||||
...factualEvents,
|
...factualEvents,
|
||||||
...relatedAdminEvents,
|
...relatedAdminEvents,
|
||||||
].sort((a, b) => a.eventtime.getTime() - b.eventtime.getTime());
|
].sort(compareTimeEvents);
|
||||||
|
|
||||||
// SCHRITT 5: Spans ableiten
|
// SCHRITT 5: Spans ableiten
|
||||||
const derivedSpans = deriveTimeSpans(combinedEvents);
|
const derivedSpans = deriveTimeSpans(combinedEvents);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
desc
|
desc
|
||||||
} from "drizzle-orm"
|
} from "drizzle-orm"
|
||||||
import {stafftimeevents} from "../../../db/schema/staff_time_events";
|
import {stafftimeevents} from "../../../db/schema/staff_time_events";
|
||||||
import {loadRelatedAdminEvents, loadValidEvents} from "../../modules/time/loadvalidevents.service";
|
import {compareTimeEvents, loadRelatedAdminEvents, loadValidEvents} from "../../modules/time/loadvalidevents.service";
|
||||||
import {deriveTimeSpans} from "../../modules/time/derivetimespans.service";
|
import {deriveTimeSpans} from "../../modules/time/derivetimespans.service";
|
||||||
import {buildTimeEvaluationFromSpans} from "../../modules/time/buildtimeevaluation.service";
|
import {buildTimeEvaluationFromSpans} from "../../modules/time/buildtimeevaluation.service";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
@@ -354,7 +354,7 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
|
|||||||
const combinedEvents = [
|
const combinedEvents = [
|
||||||
...factualEvents,
|
...factualEvents,
|
||||||
...relatedAdminEvents,
|
...relatedAdminEvents,
|
||||||
].sort((a, b) => a.eventtime.getTime() - b.eventtime.getTime());
|
].sort(compareTimeEvents);
|
||||||
|
|
||||||
// SCHRITT 5: Spans ableiten
|
// SCHRITT 5: Spans ableiten
|
||||||
const derivedSpans = deriveTimeSpans(combinedEvents);
|
const derivedSpans = deriveTimeSpans(combinedEvents);
|
||||||
@@ -424,7 +424,7 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
|
|||||||
const combinedEvents = [
|
const combinedEvents = [
|
||||||
...factualEvents,
|
...factualEvents,
|
||||||
...relatedAdminEvents,
|
...relatedAdminEvents,
|
||||||
].sort((a, b) => a.eventtime.getTime() - b.eventtime.getTime());
|
].sort(compareTimeEvents);
|
||||||
|
|
||||||
// SCHRITT 4: Ableiten und Anreichern
|
// SCHRITT 4: Ableiten und Anreichern
|
||||||
const derivedSpans = deriveTimeSpans(combinedEvents);
|
const derivedSpans = deriveTimeSpans(combinedEvents);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||||
|
|
||||||
// 💡 createEntry importieren
|
// 💡 createEntry importieren
|
||||||
const { update, createEntry } = useStaffTime()
|
const { list, update, createEntry } = useStaffTime()
|
||||||
const { $dayjs } = useNuxtApp()
|
const { $dayjs } = useNuxtApp()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
|||||||
if (state.end_date && state.end_time) {
|
if (state.end_date && state.end_time) {
|
||||||
endIso = $dayjs(`${state.end_date} ${state.end_time}`).toISOString()
|
endIso = $dayjs(`${state.end_date} ${state.end_time}`).toISOString()
|
||||||
|
|
||||||
if ($dayjs(endIso).isBefore($dayjs(startIso))) {
|
if (!$dayjs(endIso).isAfter($dayjs(startIso))) {
|
||||||
throw new Error("Endzeitpunkt muss nach dem Startzeitpunkt liegen.")
|
throw new Error("Endzeitpunkt muss nach dem Startzeitpunkt liegen.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +100,33 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
|||||||
})
|
})
|
||||||
toast.add({ title: 'Eintrag aktualisiert', color: 'green' })
|
toast.add({ title: 'Eintrag aktualisiert', color: 'green' })
|
||||||
} else {
|
} else {
|
||||||
|
if (endIso) {
|
||||||
|
const existingEntries = await list({
|
||||||
|
user_id: props.defaultUserId
|
||||||
|
})
|
||||||
|
|
||||||
|
const newStart = $dayjs(startIso).valueOf()
|
||||||
|
const newEnd = $dayjs(endIso).valueOf()
|
||||||
|
const blockingStates = new Set(['draft', 'factual', 'submitted', 'approved'])
|
||||||
|
|
||||||
|
const conflictingEntry = existingEntries.find(existingEntry => {
|
||||||
|
if (!blockingStates.has(existingEntry.state) || !existingEntry.stopped_at) return false
|
||||||
|
|
||||||
|
const existingStart = $dayjs(existingEntry.started_at).valueOf()
|
||||||
|
const existingEnd = $dayjs(existingEntry.stopped_at).valueOf()
|
||||||
|
|
||||||
|
if (!Number.isFinite(existingStart) || !Number.isFinite(existingEnd)) return false
|
||||||
|
|
||||||
|
return newStart < existingEnd && existingStart < newEnd
|
||||||
|
})
|
||||||
|
|
||||||
|
if (conflictingEntry) {
|
||||||
|
const conflictStart = $dayjs(conflictingEntry.started_at).format('DD.MM.YYYY HH:mm')
|
||||||
|
const conflictEnd = $dayjs(conflictingEntry.stopped_at).format('DD.MM.YYYY HH:mm')
|
||||||
|
throw new Error(`Überschneidung mit ${conflictStart} bis ${conflictEnd} (${conflictingEntry.state}).`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🟢 CREATE (Neu Erstellen)
|
// 🟢 CREATE (Neu Erstellen)
|
||||||
// 💡 HIER WAR DER FEHLER: Wir nutzen jetzt createEntry mit den Daten aus dem Formular
|
// 💡 HIER WAR DER FEHLER: Wir nutzen jetzt createEntry mit den Daten aus dem Formular
|
||||||
await createEntry({
|
await createEntry({
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const useStaffTime = () => {
|
|||||||
duration_minutes: end.diff(start, 'minute'),
|
duration_minutes: end.diff(start, 'minute'),
|
||||||
user_id: targetUserId,
|
user_id: targetUserId,
|
||||||
type: span.type,
|
type: span.type,
|
||||||
description: span.payload?.description || ''
|
description: span.description || span.payload?.description || ''
|
||||||
}
|
}
|
||||||
}).sort((a: any, b: any) => $dayjs(b.started_at).valueOf() - $dayjs(a.started_at).valueOf())
|
}).sort((a: any, b: any) => $dayjs(b.started_at).valueOf() - $dayjs(a.started_at).valueOf())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ onMounted(async () => {
|
|||||||
<UTooltip text="Genehmigen" v-if="row.original.state === 'submitted' && canViewAll">
|
<UTooltip text="Genehmigen" v-if="row.original.state === 'submitted' && canViewAll">
|
||||||
<UButton size="xs" color="green" variant="ghost" icon="i-heroicons-check" @click="handleApprove(row.original)" :loading="loading" />
|
<UButton size="xs" color="green" variant="ghost" icon="i-heroicons-check" @click="handleApprove(row.original)" :loading="loading" />
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UTooltip text="Ablehnen" v-if="(row.original.state === 'submitted' || row.original.state === 'approved') && canViewAll">
|
<UTooltip text="Ablehnen" v-if="['draft', 'factual', 'submitted', 'approved'].includes(row.original.state) && canViewAll">
|
||||||
<UButton size="xs" color="error" variant="ghost" icon="i-heroicons-x-mark" @click="openRejectModal(row.original)" :loading="loading" />
|
<UButton size="xs" color="error" variant="ghost" icon="i-heroicons-x-mark" @click="openRejectModal(row.original)" :loading="loading" />
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UTooltip text="Bearbeiten" v-if="['draft', 'factual', 'submitted'].includes(row.original.state)">
|
<UTooltip text="Bearbeiten" v-if="['draft', 'factual', 'submitted'].includes(row.original.state)">
|
||||||
@@ -366,7 +366,7 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
v-if="(entry.state === 'submitted' || entry.state === 'approved') && canViewAll"
|
v-if="['draft', 'factual', 'submitted', 'approved'].includes(entry.state) && canViewAll"
|
||||||
size="xs" color="error" variant="ghost" icon="i-heroicons-x-mark" label="Ablehnen"
|
size="xs" color="error" variant="ghost" icon="i-heroicons-x-mark" label="Ablehnen"
|
||||||
@click="openRejectModal(entry)" :loading="loading"
|
@click="openRejectModal(entry)" :loading="loading"
|
||||||
/>
|
/>
|
||||||
@@ -434,6 +434,7 @@ onMounted(async () => {
|
|||||||
<div class="flex gap-2 mt-3 justify-end">
|
<div class="flex gap-2 mt-3 justify-end">
|
||||||
<UButton v-if="(row.state === 'draft' || row.state === 'factual') && row.stopped_at" color="cyan" size="sm" icon="i-heroicons-paper-airplane" label="Einreichen" variant="soft" @click.stop="handleSubmit(row)" :loading="loading" />
|
<UButton v-if="(row.state === 'draft' || row.state === 'factual') && row.stopped_at" color="cyan" size="sm" icon="i-heroicons-paper-airplane" label="Einreichen" variant="soft" @click.stop="handleSubmit(row)" :loading="loading" />
|
||||||
<UButton v-if="row.state === 'submitted' && canViewAll" color="green" size="sm" icon="i-heroicons-check" label="Genehmigen" variant="soft" @click.stop="handleApprove(row)" :loading="loading" />
|
<UButton v-if="row.state === 'submitted' && canViewAll" color="green" size="sm" icon="i-heroicons-check" label="Genehmigen" variant="soft" @click.stop="handleApprove(row)" :loading="loading" />
|
||||||
|
<UButton v-if="['draft', 'factual', 'submitted', 'approved'].includes(row.state) && canViewAll" color="error" size="sm" icon="i-heroicons-x-mark" label="Ablehnen" variant="soft" @click.stop="openRejectModal(row)" :loading="loading" />
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user