Tasks und Vertragstyp fix #17
This commit is contained in:
1
backend/db/migrations/0007_bright_default_tax_type.sql
Normal file
1
backend/db/migrations/0007_bright_default_tax_type.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "customers" ADD COLUMN "customTaxType" text;
|
||||||
16
backend/db/migrations/0008_quick_contracttypes.sql
Normal file
16
backend/db/migrations/0008_quick_contracttypes.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE "contracttypes" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "contracttypes_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"paymentType" text,
|
||||||
|
"recurring" boolean DEFAULT false NOT NULL,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracttypes" ADD CONSTRAINT "contracttypes_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracttypes" ADD CONSTRAINT "contracttypes_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "contracts" ADD COLUMN "contracttype" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_contracttype_contracttypes_id_fk" FOREIGN KEY ("contracttype") REFERENCES "public"."contracttypes"("id") ON DELETE no action ON UPDATE no action;
|
||||||
3
backend/db/migrations/0010_sudden_billing_interval.sql
Normal file
3
backend/db/migrations/0010_sudden_billing_interval.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "contracttypes" ADD COLUMN "billingInterval" text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracts" ADD COLUMN "billingInterval" text;
|
||||||
@@ -57,6 +57,27 @@
|
|||||||
"when": 1772000100000,
|
"when": 1772000100000,
|
||||||
"tag": "0007_bright_default_tax_type",
|
"tag": "0007_bright_default_tax_type",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000000000,
|
||||||
|
"tag": "0008_quick_contracttypes",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000100000,
|
||||||
|
"tag": "0009_heavy_contract_contracttype",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000200000,
|
||||||
|
"tag": "0010_sudden_billing_interval",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { tenants } from "./tenants"
|
import { tenants } from "./tenants"
|
||||||
import { customers } from "./customers"
|
import { customers } from "./customers"
|
||||||
import { contacts } from "./contacts"
|
import { contacts } from "./contacts"
|
||||||
|
import { contracttypes } from "./contracttypes"
|
||||||
import { authUsers } from "./auth_users"
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
export const contracts = pgTable(
|
export const contracts = pgTable(
|
||||||
@@ -48,6 +49,9 @@ export const contracts = pgTable(
|
|||||||
contact: bigint("contact", { mode: "number" }).references(
|
contact: bigint("contact", { mode: "number" }).references(
|
||||||
() => contacts.id
|
() => contacts.id
|
||||||
),
|
),
|
||||||
|
contracttype: bigint("contracttype", { mode: "number" }).references(
|
||||||
|
() => contracttypes.id
|
||||||
|
),
|
||||||
|
|
||||||
bankingIban: text("bankingIban"),
|
bankingIban: text("bankingIban"),
|
||||||
bankingBIC: text("bankingBIC"),
|
bankingBIC: text("bankingBIC"),
|
||||||
@@ -57,6 +61,7 @@ export const contracts = pgTable(
|
|||||||
sepaDate: timestamp("sepaDate", { withTimezone: true }),
|
sepaDate: timestamp("sepaDate", { withTimezone: true }),
|
||||||
|
|
||||||
paymentType: text("paymentType"),
|
paymentType: text("paymentType"),
|
||||||
|
billingInterval: text("billingInterval"),
|
||||||
invoiceDispatch: text("invoiceDispatch"),
|
invoiceDispatch: text("invoiceDispatch"),
|
||||||
|
|
||||||
ownFields: jsonb("ownFields").notNull().default({}),
|
ownFields: jsonb("ownFields").notNull().default({}),
|
||||||
|
|||||||
40
backend/db/schema/contracttypes.ts
Normal file
40
backend/db/schema/contracttypes.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const contracttypes = pgTable("contracttypes", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
paymentType: text("paymentType"),
|
||||||
|
recurring: boolean("recurring").notNull().default(false),
|
||||||
|
billingInterval: text("billingInterval"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ContractType = typeof contracttypes.$inferSelect
|
||||||
|
export type NewContractType = typeof contracttypes.$inferInsert
|
||||||
@@ -13,6 +13,7 @@ export * from "./checks"
|
|||||||
export * from "./citys"
|
export * from "./citys"
|
||||||
export * from "./contacts"
|
export * from "./contacts"
|
||||||
export * from "./contracts"
|
export * from "./contracts"
|
||||||
|
export * from "./contracttypes"
|
||||||
export * from "./costcentres"
|
export * from "./costcentres"
|
||||||
export * from "./countrys"
|
export * from "./countrys"
|
||||||
export * from "./createddocuments"
|
export * from "./createddocuments"
|
||||||
|
|||||||
@@ -296,6 +296,8 @@ export const diffTranslations: Record<
|
|||||||
},
|
},
|
||||||
|
|
||||||
projecttype: { label: "Projekttyp" },
|
projecttype: { label: "Projekttyp" },
|
||||||
|
contracttype: { label: "Vertragstyp" },
|
||||||
|
billingInterval: { label: "Abrechnungsintervall" },
|
||||||
|
|
||||||
fixed: {
|
fixed: {
|
||||||
label: "Festgeschrieben",
|
label: "Festgeschrieben",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
bankstatements,
|
bankstatements,
|
||||||
contacts,
|
contacts,
|
||||||
contracts,
|
contracts,
|
||||||
|
contracttypes,
|
||||||
costcentres,
|
costcentres,
|
||||||
createddocuments,
|
createddocuments,
|
||||||
customers,
|
customers,
|
||||||
@@ -55,9 +56,13 @@ export const resourceConfig = {
|
|||||||
},
|
},
|
||||||
contracts: {
|
contracts: {
|
||||||
table: contracts,
|
table: contracts,
|
||||||
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
|
searchColumns: ["name", "notes", "contractNumber", "paymentType", "billingInterval", "sepaRef", "bankingName"],
|
||||||
numberRangeHolder: "contractNumber",
|
numberRangeHolder: "contractNumber",
|
||||||
mtoLoad: ["customer"],
|
mtoLoad: ["customer", "contracttype"],
|
||||||
|
},
|
||||||
|
contracttypes: {
|
||||||
|
table: contracttypes,
|
||||||
|
searchColumns: ["name", "description", "paymentType", "billingInterval"],
|
||||||
},
|
},
|
||||||
plants: {
|
plants: {
|
||||||
table: plants,
|
table: plants,
|
||||||
@@ -173,4 +178,4 @@ export const resourceConfig = {
|
|||||||
serialexecutions: {
|
serialexecutions: {
|
||||||
table: serialExecutions
|
table: serialExecutions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const dataStore = useDataStore()
|
|||||||
const tempStore = useTempStore()
|
const tempStore = useTempStore()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const createRoute = computed(() => type.value === "tasks" ? `/tasks/create?${props.queryStringData}` : `/standardEntity/${type.value}/create?${props.queryStringData}`)
|
||||||
|
|
||||||
let dataType = null
|
let dataType = null
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ setup()
|
|||||||
</template>
|
</template>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<UButton
|
<UButton
|
||||||
@click="router.push(`/standardEntity/${type}/create?${props.queryStringData}`)"
|
@click="router.push(createRoute)"
|
||||||
>
|
>
|
||||||
+ {{dataType.labelSingle}}
|
+ {{dataType.labelSingle}}
|
||||||
</UButton>
|
</UButton>
|
||||||
@@ -125,4 +126,4 @@ setup()
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
const getShowRoute = (entityType, id) => entityType === "tasks" ? `/tasks/show/${id}` : `/standardEntity/${entityType}/show/${id}`
|
||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
/*'/': () => {
|
/*'/': () => {
|
||||||
//console.log(searchinput)
|
//console.log(searchinput)
|
||||||
@@ -8,7 +10,7 @@
|
|||||||
'Enter': {
|
'Enter': {
|
||||||
usingInput: true,
|
usingInput: true,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
router.push(`/standardEntity/${props.type}/show/${props.rows.value[selectedItem.value].id}`)
|
router.push(getShowRoute(props.type, props.rows[selectedItem.value].id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'arrowdown': () => {
|
'arrowdown': () => {
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
:columns="props.columns"
|
:columns="props.columns"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||||
@select="(i) => router.push(`/standardEntity/${type}/show/${i.id}`) "
|
@select="(i) => router.push(getShowRoute(type, i.id))"
|
||||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
|
||||||
>
|
>
|
||||||
<!-- <template
|
<!-- <template
|
||||||
@@ -134,4 +136,4 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const getShowRoute = (entityType, id) => entityType === "tasks" ? `/tasks/show/${id}` : `/standardEntity/${entityType}/show/${id}`
|
||||||
|
|
||||||
const dataType = dataStore.dataTypes[props.type]
|
const dataType = dataStore.dataTypes[props.type]
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@
|
|||||||
<a
|
<a
|
||||||
v-for="item in props.rows"
|
v-for="item in props.rows"
|
||||||
class="my-1"
|
class="my-1"
|
||||||
@click="router.push(`/standardEntity/${type}/show/${item.id}`)"
|
@click="router.push(getShowRoute(type, item.id))"
|
||||||
>
|
>
|
||||||
<p class="truncate text-left text-primary text-xl">{{dataType.templateColumns.find(i => i.title).key ? item[dataType.templateColumns.find(i => i.title).key] : null}}</p>
|
<p class="truncate text-left text-primary text-xl">{{dataType.templateColumns.find(i => i.title).key ? item[dataType.templateColumns.find(i => i.title).key] : null}}</p>
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
@@ -126,4 +127,4 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const links = computed(() => {
|
|||||||
} else if (pin.type === "standardEntity") {
|
} else if (pin.type === "standardEntity") {
|
||||||
return {
|
return {
|
||||||
label: pin.label,
|
label: pin.label,
|
||||||
to: `/standardEntity/${pin.datatype}/show/${pin.id}`,
|
to: pin.datatype === "tasks" ? `/tasks/show/${pin.id}` : `/standardEntity/${pin.datatype}/show/${pin.id}`,
|
||||||
icon: pin.icon,
|
icon: pin.icon,
|
||||||
pinned: true
|
pinned: true
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ const links = computed(() => {
|
|||||||
children: [
|
children: [
|
||||||
...has("tasks") ? [{
|
...has("tasks") ? [{
|
||||||
label: "Aufgaben",
|
label: "Aufgaben",
|
||||||
to: "/standardEntity/tasks",
|
to: "/tasks",
|
||||||
icon: "i-heroicons-rectangle-stack"
|
icon: "i-heroicons-rectangle-stack"
|
||||||
}] : [],
|
}] : [],
|
||||||
...true ? [{
|
...true ? [{
|
||||||
@@ -278,6 +278,10 @@ const links = computed(() => {
|
|||||||
label: "Projekttypen",
|
label: "Projekttypen",
|
||||||
to: "/projecttypes",
|
to: "/projecttypes",
|
||||||
icon: "i-heroicons-clipboard-document-list",
|
icon: "i-heroicons-clipboard-document-list",
|
||||||
|
}, {
|
||||||
|
label: "Vertragstypen",
|
||||||
|
to: "/standardEntity/contracttypes",
|
||||||
|
icon: "i-heroicons-document-duplicate",
|
||||||
}, {
|
}, {
|
||||||
label: "Export",
|
label: "Export",
|
||||||
to: "/export",
|
to: "/export",
|
||||||
@@ -365,4 +369,4 @@ const buttonItems = computed(() =>
|
|||||||
</UAccordion>
|
</UAccordion>
|
||||||
|
|
||||||
<Calculator v-if="showCalculator" v-model="showCalculator"/>
|
<Calculator v-if="showCalculator" v-model="showCalculator"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
13
frontend/components/columnRenderings/contracttype.vue
Normal file
13
frontend/components/columnRenderings/contracttype.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>{{ props.row.contracttype ? props.row.contracttype.name : '' }}</span>
|
||||||
|
</template>
|
||||||
@@ -5,8 +5,11 @@ const router = useRouter()
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
const setupPage = async () => {
|
const setupPage = async () => {
|
||||||
//TODO: BACKEND CHANGE Migrate to auth_users for profile
|
openTasks.value = (await useEntities("tasks").select()).filter((task) => {
|
||||||
openTasks.value = (await useEntities("tasks").select()).filter(i => !i.archived && i.user_id === auth.user.id)
|
const assignee = task.userId || task.user_id || task.profile
|
||||||
|
const currentUser = auth.user?.user_id || auth.user?.id
|
||||||
|
return !task.archived && assignee === currentUser
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPage()
|
setupPage()
|
||||||
@@ -18,7 +21,7 @@ setupPage()
|
|||||||
v-if="openTasks.length > 0"
|
v-if="openTasks.length > 0"
|
||||||
:rows="openTasks"
|
:rows="openTasks"
|
||||||
:columns="[{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}]"
|
:columns="[{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}]"
|
||||||
@select="(i) => router.push(`/standardEntity/tasks/show/${i.id}`)"
|
@select="(i) => router.push(`/tasks/show/${i.id}`)"
|
||||||
/>
|
/>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p class="text-center font-bold">Keine offenen Aufgaben</p>
|
<p class="text-center font-bold">Keine offenen Aufgaben</p>
|
||||||
@@ -27,4 +30,4 @@ setupPage()
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const _useDashboard = () => {
|
|||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
'g-h': () => router.push('/'),
|
'g-h': () => router.push('/'),
|
||||||
'g-a': () => router.push('/standardEntity/tasks'),
|
'g-a': () => router.push('/tasks'),
|
||||||
'g-d': () => router.push('/files'),
|
'g-d': () => router.push('/files'),
|
||||||
'g-k': () => router.push('/standardEntity/customers'),
|
'g-k': () => router.push('/standardEntity/customers'),
|
||||||
'g-l': () => router.push('/standardEntity/vendors'),
|
'g-l': () => router.push('/standardEntity/vendors'),
|
||||||
@@ -31,4 +31,4 @@ const _useDashboard = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDashboard = createSharedComposable(_useDashboard)
|
export const useDashboard = createSharedComposable(_useDashboard)
|
||||||
|
|||||||
@@ -31,6 +31,18 @@ export const useRole = () => {
|
|||||||
label: "Verträge erstellen",
|
label: "Verträge erstellen",
|
||||||
parent: "contracts"
|
parent: "contracts"
|
||||||
},
|
},
|
||||||
|
contracttypes: {
|
||||||
|
label: "Vertragstypen",
|
||||||
|
showToAllUsers: false
|
||||||
|
},
|
||||||
|
"contracttypes-viewAll": {
|
||||||
|
label: "Alle Vertragstypen einsehen",
|
||||||
|
parent: "contracttypes"
|
||||||
|
},
|
||||||
|
"contracttypes-create": {
|
||||||
|
label: "Vertragstypen erstellen",
|
||||||
|
parent: "contracttypes"
|
||||||
|
},
|
||||||
plants: {
|
plants: {
|
||||||
label: "Objekte",
|
label: "Objekte",
|
||||||
showToAllUsers: false
|
showToAllUsers: false
|
||||||
@@ -306,4 +318,4 @@ export const useRole = () => {
|
|||||||
checkRight
|
checkRight
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ const setupPage = async (sort_column = null, sort_direction = null) => {
|
|||||||
loaded.value = false
|
loaded.value = false
|
||||||
setPageLayout(platform)
|
setPageLayout(platform)
|
||||||
|
|
||||||
|
if (type === "tasks") {
|
||||||
|
const query = { ...route.query, mode: route.params.mode }
|
||||||
|
if (route.params.id) query.id = route.params.id
|
||||||
|
await navigateTo({ path: "/tasks", query })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (route.params.mode) mode.value = route.params.mode
|
if (route.params.mode) mode.value = route.params.mode
|
||||||
@@ -88,4 +95,4 @@ setupPage()
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -143,6 +143,11 @@ const setupPage = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
setPageLayout(platformIsNative ? "mobile" : "default")
|
setPageLayout(platformIsNative ? "mobile" : "default")
|
||||||
|
|
||||||
|
if (type === "tasks") {
|
||||||
|
await navigateTo("/tasks")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
archived:false
|
archived:false
|
||||||
@@ -624,4 +629,4 @@ const handleFilterChange = async (action,column) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
22
frontend/pages/tasks/[mode]/[[id]].vue
Normal file
22
frontend/pages/tasks/[mode]/[[id]].vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const mode = typeof route.params.mode === "string" ? route.params.mode : ""
|
||||||
|
const id = typeof route.params.id === "string" ? route.params.id : ""
|
||||||
|
|
||||||
|
const query = { ...route.query }
|
||||||
|
|
||||||
|
if (["create", "show", "edit"].includes(mode)) {
|
||||||
|
query.mode = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
query.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigateTo({ path: "/tasks", query }, { replace: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UProgress animation="carousel" class="p-5 mt-10" />
|
||||||
|
</template>
|
||||||
746
frontend/pages/tasks/index.vue
Normal file
746
frontend/pages/tasks/index.vue
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
<script setup>
|
||||||
|
import { setPageLayout } from "#app"
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const toast = useToast()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const profileStore = useProfileStore()
|
||||||
|
const { has } = usePermission()
|
||||||
|
|
||||||
|
const STATUS_COLUMNS = ["Offen", "In Bearbeitung", "Abgeschlossen"]
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const deleting = ref(false)
|
||||||
|
const quickCompleteLoadingId = ref(null)
|
||||||
|
const tasks = ref([])
|
||||||
|
const search = ref("")
|
||||||
|
const viewMode = ref("kanban")
|
||||||
|
const draggingTaskId = ref(null)
|
||||||
|
const droppingOn = ref("")
|
||||||
|
const projects = ref([])
|
||||||
|
const customers = ref([])
|
||||||
|
const plants = ref([])
|
||||||
|
|
||||||
|
const canViewAll = computed(() => has("tasks-viewAll"))
|
||||||
|
const canCreate = computed(() => has("tasks-create"))
|
||||||
|
const currentUserId = computed(() => auth.user?.user_id || auth.user?.id || null)
|
||||||
|
const showOnlyMine = ref(!canViewAll.value)
|
||||||
|
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
const modalMode = ref("show")
|
||||||
|
const taskForm = ref(getEmptyTask())
|
||||||
|
|
||||||
|
const assigneeOptions = computed(() =>
|
||||||
|
(profileStore.profiles || [])
|
||||||
|
.map((profile) => {
|
||||||
|
const value = profile.user_id || profile.id
|
||||||
|
const label = profile.full_name || profile.fullName || profile.email
|
||||||
|
return value && label ? { value, label } : null
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
|
||||||
|
const projectOptions = computed(() =>
|
||||||
|
(projects.value || []).map((project) => ({ value: project.id, label: project.name }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const customerOptions = computed(() =>
|
||||||
|
(customers.value || []).map((customer) => ({ value: customer.id, label: customer.name }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const plantOptions = computed(() =>
|
||||||
|
(plants.value || []).map((plant) => ({ value: plant.id, label: plant.name }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredTasks = computed(() => {
|
||||||
|
const needle = search.value.trim().toLowerCase()
|
||||||
|
|
||||||
|
return tasks.value.filter((task) => {
|
||||||
|
const assigneeId = getTaskAssigneeId(task)
|
||||||
|
const mineMatch = !showOnlyMine.value || (currentUserId.value && assigneeId === currentUserId.value)
|
||||||
|
const searchMatch = !needle || [task.name, task.description, task.categorie].some((value) => String(value || "").toLowerCase().includes(needle))
|
||||||
|
return !task.archived && mineMatch && searchMatch
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupedTasks = computed(() => {
|
||||||
|
return STATUS_COLUMNS.reduce((acc, status) => {
|
||||||
|
acc[status] = filteredTasks.value.filter((task) => normalizeStatus(task.categorie) === status)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
if (modalMode.value === "create") return "Neue Aufgabe"
|
||||||
|
if (modalMode.value === "edit") return "Aufgabe bearbeiten"
|
||||||
|
return "Aufgabe"
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFormReadonly = computed(() => modalMode.value === "show")
|
||||||
|
const listColumns = [
|
||||||
|
{ key: "actions", label: "" },
|
||||||
|
{ key: "name", label: "Titel" },
|
||||||
|
{ key: "categorie", label: "Status" },
|
||||||
|
{ key: "assignee", label: "Zuweisung" },
|
||||||
|
{ key: "project", label: "Projekt" },
|
||||||
|
{ key: "customer", label: "Kunde" },
|
||||||
|
{ key: "plant", label: "Objekt" }
|
||||||
|
]
|
||||||
|
|
||||||
|
function getEmptyTask() {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
categorie: "Offen",
|
||||||
|
userId: currentUserId.value || null,
|
||||||
|
project: null,
|
||||||
|
customer: null,
|
||||||
|
plant: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStatus(status) {
|
||||||
|
if (!status) return "Offen"
|
||||||
|
if (status === "Erledigt") return "Abgeschlossen"
|
||||||
|
return STATUS_COLUMNS.includes(status) ? status : "Offen"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTaskAssigneeId(task) {
|
||||||
|
return task.userId || task.user_id || task.profile || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNullableNumber(value) {
|
||||||
|
if (value === null || value === undefined || value === "") return null
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTaskToForm(task) {
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
name: task.name || "",
|
||||||
|
description: task.description || "",
|
||||||
|
categorie: normalizeStatus(task.categorie),
|
||||||
|
userId: getTaskAssigneeId(task),
|
||||||
|
project: toNullableNumber(task.project?.id ?? task.project),
|
||||||
|
customer: toNullableNumber(task.customer?.id ?? task.customer),
|
||||||
|
plant: toNullableNumber(task.plant?.id ?? task.plant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntityLabel(options, id) {
|
||||||
|
if (!id) return null
|
||||||
|
const hit = options.find((item) => Number(item.value) === Number(id))
|
||||||
|
return hit?.label || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssigneeLabel(task) {
|
||||||
|
const assigneeId = getTaskAssigneeId(task)
|
||||||
|
return assigneeOptions.value.find((option) => option.value === assigneeId)?.label || "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusBadgeColor(status) {
|
||||||
|
const normalized = normalizeStatus(status)
|
||||||
|
if (normalized === "Offen") return "gray"
|
||||||
|
if (normalized === "In Bearbeitung") return "amber"
|
||||||
|
return "green"
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTasks() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const rows = await useEntities("tasks").select()
|
||||||
|
tasks.value = rows.map((task) => ({ ...task, categorie: normalizeStatus(task.categorie) }))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTaskOptions() {
|
||||||
|
const [projectRows, customerRows, plantRows] = await Promise.all([
|
||||||
|
useEntities("projects").select(),
|
||||||
|
useEntities("customers").select(),
|
||||||
|
useEntities("plants").select()
|
||||||
|
])
|
||||||
|
|
||||||
|
projects.value = projectRows || []
|
||||||
|
customers.value = customerRows || []
|
||||||
|
plants.value = plantRows || []
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateTask(initialData = {}) {
|
||||||
|
if (!canCreate.value) return
|
||||||
|
modalMode.value = "create"
|
||||||
|
taskForm.value = {
|
||||||
|
...getEmptyTask(),
|
||||||
|
...initialData,
|
||||||
|
categorie: normalizeStatus(initialData.categorie || "Offen"),
|
||||||
|
userId: initialData.userId || initialData.user_id || currentUserId.value || null
|
||||||
|
}
|
||||||
|
isModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTask(task, mode = "show") {
|
||||||
|
modalMode.value = mode
|
||||||
|
taskForm.value = mapTaskToForm(task)
|
||||||
|
isModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
isModalOpen.value = false
|
||||||
|
modalMode.value = "show"
|
||||||
|
taskForm.value = getEmptyTask()
|
||||||
|
const query = { ...route.query }
|
||||||
|
delete query.mode
|
||||||
|
delete query.id
|
||||||
|
router.replace({ path: "/tasks", query })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTask() {
|
||||||
|
if (!canCreate.value) return
|
||||||
|
const name = taskForm.value.name?.trim()
|
||||||
|
if (!name) {
|
||||||
|
toast.add({ title: "Name fehlt", description: "Bitte einen Aufgabennamen angeben.", color: "orange" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name,
|
||||||
|
description: taskForm.value.description || null,
|
||||||
|
categorie: normalizeStatus(taskForm.value.categorie),
|
||||||
|
userId: taskForm.value.userId || null,
|
||||||
|
project: toNullableNumber(taskForm.value.project),
|
||||||
|
customer: toNullableNumber(taskForm.value.customer),
|
||||||
|
plant: toNullableNumber(taskForm.value.plant)
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
let targetId = taskForm.value.id
|
||||||
|
if (taskForm.value.id) {
|
||||||
|
await useEntities("tasks").update(taskForm.value.id, payload, true)
|
||||||
|
} else {
|
||||||
|
const created = await useEntities("tasks").create(payload, true)
|
||||||
|
targetId = created?.id || null
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadTasks()
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
const query = { ...route.query, mode: "show", id: String(targetId) }
|
||||||
|
router.replace({ path: "/tasks", query })
|
||||||
|
const target = tasks.value.find((task) => String(task.id) === String(targetId))
|
||||||
|
if (target) openTask(target, "show")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function archiveTask() {
|
||||||
|
if (!canCreate.value || !taskForm.value.id) return
|
||||||
|
deleting.value = true
|
||||||
|
try {
|
||||||
|
await useEntities("tasks").update(taskForm.value.id, { archived: true }, true)
|
||||||
|
await loadTasks()
|
||||||
|
closeModal()
|
||||||
|
} finally {
|
||||||
|
deleting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeTaskQuick(task) {
|
||||||
|
if (!canCreate.value) return
|
||||||
|
if (!task?.id || normalizeStatus(task.categorie) === "Abgeschlossen") return
|
||||||
|
|
||||||
|
const previousStatus = task.categorie
|
||||||
|
quickCompleteLoadingId.value = task.id
|
||||||
|
task.categorie = "Abgeschlossen"
|
||||||
|
|
||||||
|
try {
|
||||||
|
await useEntities("tasks").update(task.id, { categorie: "Abgeschlossen" }, true)
|
||||||
|
toast.add({ title: "Aufgabe abgeschlossen", color: "green" })
|
||||||
|
} catch (error) {
|
||||||
|
task.categorie = previousStatus
|
||||||
|
toast.add({ title: "Aufgabe konnte nicht abgeschlossen werden", color: "red" })
|
||||||
|
} finally {
|
||||||
|
quickCompleteLoadingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart(task) {
|
||||||
|
draggingTaskId.value = task.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDrop(status) {
|
||||||
|
if (!canCreate.value || !draggingTaskId.value) return
|
||||||
|
|
||||||
|
const droppedTask = tasks.value.find((task) => String(task.id) === String(draggingTaskId.value))
|
||||||
|
draggingTaskId.value = null
|
||||||
|
droppingOn.value = ""
|
||||||
|
|
||||||
|
if (!droppedTask || normalizeStatus(droppedTask.categorie) === status) return
|
||||||
|
|
||||||
|
const oldStatus = droppedTask.categorie
|
||||||
|
droppedTask.categorie = status
|
||||||
|
|
||||||
|
try {
|
||||||
|
await useEntities("tasks").update(droppedTask.id, { categorie: status }, true)
|
||||||
|
} catch (error) {
|
||||||
|
droppedTask.categorie = oldStatus
|
||||||
|
toast.add({ title: "Status konnte nicht geändert werden", color: "red" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRouteIntent() {
|
||||||
|
const mode = typeof route.query.mode === "string" ? route.query.mode : null
|
||||||
|
const id = typeof route.query.id === "string" ? route.query.id : null
|
||||||
|
|
||||||
|
if (!mode) {
|
||||||
|
if (isModalOpen.value) closeModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "create") {
|
||||||
|
openCreateTask({
|
||||||
|
project: toNullableNumber(route.query.project),
|
||||||
|
customer: toNullableNumber(route.query.customer),
|
||||||
|
plant: toNullableNumber(route.query.plant),
|
||||||
|
userId: route.query.userId || route.query.user_id || currentUserId.value || null
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
let task = tasks.value.find((item) => String(item.id) === id)
|
||||||
|
if (!task) {
|
||||||
|
const loadedTask = await useEntities("tasks").selectSingle(id, "*", true)
|
||||||
|
if (loadedTask) {
|
||||||
|
task = { ...loadedTask, categorie: normalizeStatus(loadedTask.categorie) }
|
||||||
|
const idx = tasks.value.findIndex((item) => String(item.id) === id)
|
||||||
|
if (idx === -1) tasks.value.push(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!task) return
|
||||||
|
openTask(task, mode === "edit" ? "edit" : "show")
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateViaRoute() {
|
||||||
|
router.push({ path: "/tasks", query: { ...route.query, mode: "create" } })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTaskViaRoute(task) {
|
||||||
|
router.push({ path: "/tasks", query: { ...route.query, mode: "show", id: String(task.id) } })
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query,
|
||||||
|
async () => {
|
||||||
|
await handleRouteIntent()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
setPageLayout("default")
|
||||||
|
await Promise.all([loadTasks(), loadTaskOptions()])
|
||||||
|
await handleRouteIntent()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardNavbar title="Aufgaben" :badge="filteredTasks.length">
|
||||||
|
<template #right>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UInput
|
||||||
|
v-model="search"
|
||||||
|
icon="i-heroicons-magnifying-glass"
|
||||||
|
placeholder="Aufgaben durchsuchen..."
|
||||||
|
class="w-72"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-path"
|
||||||
|
color="gray"
|
||||||
|
variant="soft"
|
||||||
|
:loading="loading"
|
||||||
|
@click="loadTasks"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="canCreate"
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
@click="openCreateViaRoute"
|
||||||
|
>
|
||||||
|
Aufgabe
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UCheckbox
|
||||||
|
v-if="canViewAll"
|
||||||
|
v-model="showOnlyMine"
|
||||||
|
label="Nur meine Aufgaben"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-sm text-gray-500">Ansicht: Nur eigene Aufgaben</span>
|
||||||
|
</template>
|
||||||
|
<template #right>
|
||||||
|
<div class="flex items-center gap-1 rounded-lg border border-gray-200 p-1 dark:border-gray-700">
|
||||||
|
<UButton
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-view-columns"
|
||||||
|
:color="viewMode === 'kanban' ? 'primary' : 'gray'"
|
||||||
|
@click="viewMode = 'kanban'"
|
||||||
|
>
|
||||||
|
Kanban
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-list-bullet"
|
||||||
|
:color="viewMode === 'list' ? 'primary' : 'gray'"
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
>
|
||||||
|
Liste
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
|
||||||
|
<UDashboardPanelContent>
|
||||||
|
<div v-if="viewMode === 'kanban'" class="kanban-grid">
|
||||||
|
<section
|
||||||
|
v-for="status in STATUS_COLUMNS"
|
||||||
|
:key="status"
|
||||||
|
class="kanban-column"
|
||||||
|
@dragover.prevent="droppingOn = status"
|
||||||
|
@dragleave="droppingOn = ''"
|
||||||
|
@drop.prevent="onDrop(status)"
|
||||||
|
>
|
||||||
|
<header class="kanban-column-header">
|
||||||
|
<h3>{{ status }}</h3>
|
||||||
|
<UBadge color="gray" variant="subtle">{{ groupedTasks[status]?.length || 0 }}</UBadge>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div :class="['kanban-dropzone', droppingOn === status ? 'kanban-dropzone-active' : '']">
|
||||||
|
<article
|
||||||
|
v-for="task in groupedTasks[status]"
|
||||||
|
:key="task.id"
|
||||||
|
class="kanban-card"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="onDragStart(task)"
|
||||||
|
@click="openTaskViaRoute(task)"
|
||||||
|
>
|
||||||
|
<div class="kanban-card-title">{{ task.name }}</div>
|
||||||
|
<p v-if="task.description" class="kanban-card-description">{{ task.description }}</p>
|
||||||
|
|
||||||
|
<div class="kanban-card-meta">
|
||||||
|
<UBadge v-if="getEntityLabel(projectOptions, task.project?.id || task.project)" color="primary" variant="soft">
|
||||||
|
{{ getEntityLabel(projectOptions, task.project?.id || task.project) }}
|
||||||
|
</UBadge>
|
||||||
|
<UBadge v-if="getEntityLabel(customerOptions, task.customer?.id || task.customer)" color="gray" variant="soft">
|
||||||
|
{{ getEntityLabel(customerOptions, task.customer?.id || task.customer) }}
|
||||||
|
</UBadge>
|
||||||
|
<UBadge v-if="getEntityLabel(plantOptions, task.plant?.id || task.plant)" color="gray" variant="soft">
|
||||||
|
{{ getEntityLabel(plantOptions, task.plant?.id || task.plant) }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div v-if="!groupedTasks[status]?.length" class="kanban-empty">
|
||||||
|
Keine Aufgaben
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<UTable
|
||||||
|
v-else
|
||||||
|
:rows="filteredTasks"
|
||||||
|
:columns="listColumns"
|
||||||
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Aufgaben anzuzeigen' }"
|
||||||
|
@select="(task) => openTaskViaRoute(task)"
|
||||||
|
>
|
||||||
|
<template #actions-data="{ row }">
|
||||||
|
<UButton
|
||||||
|
v-if="normalizeStatus(row.categorie) !== 'Abgeschlossen' && canCreate"
|
||||||
|
size="xs"
|
||||||
|
color="green"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-check"
|
||||||
|
:loading="quickCompleteLoadingId === row.id"
|
||||||
|
@click.stop="completeTaskQuick(row)"
|
||||||
|
>
|
||||||
|
Erledigt
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
<template #categorie-data="{ row }">
|
||||||
|
<UBadge :color="getStatusBadgeColor(row.categorie)" variant="soft">
|
||||||
|
{{ normalizeStatus(row.categorie) }}
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
<template #assignee-data="{ row }">
|
||||||
|
{{ getAssigneeLabel(row) }}
|
||||||
|
</template>
|
||||||
|
<template #project-data="{ row }">
|
||||||
|
{{ getEntityLabel(projectOptions, row.project?.id || row.project) || "-" }}
|
||||||
|
</template>
|
||||||
|
<template #customer-data="{ row }">
|
||||||
|
{{ getEntityLabel(customerOptions, row.customer?.id || row.customer) || "-" }}
|
||||||
|
</template>
|
||||||
|
<template #plant-data="{ row }">
|
||||||
|
{{ getEntityLabel(plantOptions, row.plant?.id || row.plant) || "-" }}
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
|
||||||
|
<UModal v-model="isModalOpen" :prevent-close="saving || deleting">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="font-semibold">{{ modalTitle }}</h3>
|
||||||
|
<UBadge color="gray" variant="subtle">{{ taskForm.id ? `#${taskForm.id}` : "Neu" }}</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Titel</label>
|
||||||
|
<UInput v-model="taskForm.name" :disabled="isFormReadonly || !canCreate" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Beschreibung</label>
|
||||||
|
<UTextarea v-model="taskForm.description" :disabled="isFormReadonly || !canCreate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<USelectMenu
|
||||||
|
v-model="taskForm.categorie"
|
||||||
|
:options="STATUS_COLUMNS.map((status) => ({ label: status, value: status }))"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
:disabled="isFormReadonly || !canCreate"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{ taskForm.categorie || "Status auswählen" }}
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
|
||||||
|
<USelectMenu
|
||||||
|
v-model="taskForm.userId"
|
||||||
|
:options="assigneeOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
:disabled="isFormReadonly || !canCreate"
|
||||||
|
searchable
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{ assigneeOptions.find((option) => option.value === taskForm.userId)?.label || "Zuweisung" }}
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
|
||||||
|
<USelectMenu
|
||||||
|
v-model="taskForm.project"
|
||||||
|
:options="projectOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
:disabled="isFormReadonly || !canCreate"
|
||||||
|
searchable
|
||||||
|
clear-search-on-close
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{ getEntityLabel(projectOptions, taskForm.project) || "Projekt" }}
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
|
||||||
|
<USelectMenu
|
||||||
|
v-model="taskForm.customer"
|
||||||
|
:options="customerOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
:disabled="isFormReadonly || !canCreate"
|
||||||
|
searchable
|
||||||
|
clear-search-on-close
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{ getEntityLabel(customerOptions, taskForm.customer) || "Kunde" }}
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
|
||||||
|
<USelectMenu
|
||||||
|
v-model="taskForm.plant"
|
||||||
|
:options="plantOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
:disabled="isFormReadonly || !canCreate"
|
||||||
|
searchable
|
||||||
|
clear-search-on-close
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{ getEntityLabel(plantOptions, taskForm.plant) || "Objekt" }}
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton
|
||||||
|
v-if="taskForm.id && canCreate"
|
||||||
|
color="red"
|
||||||
|
variant="soft"
|
||||||
|
:loading="deleting"
|
||||||
|
@click="archiveTask"
|
||||||
|
>
|
||||||
|
Archivieren
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton color="gray" variant="ghost" @click="closeModal">Schließen</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="modalMode === 'show' && canCreate"
|
||||||
|
color="gray"
|
||||||
|
variant="soft"
|
||||||
|
@click="modalMode = 'edit'"
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="modalMode !== 'show' && canCreate"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveTask"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.kanban-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.kanban-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column {
|
||||||
|
border: 1px solid rgb(229 231 235);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: rgb(249 250 251);
|
||||||
|
min-height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
border-bottom: 1px solid rgb(229 231 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-dropzone {
|
||||||
|
padding: 0.75rem;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-dropzone-active {
|
||||||
|
background: rgb(239 246 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card {
|
||||||
|
border: 1px solid rgb(229 231 235);
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
padding: 0.7rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card-title {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card-description {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
color: rgb(107 114 128);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-empty {
|
||||||
|
border: 1px dashed rgb(209 213 219);
|
||||||
|
color: rgb(156 163 175);
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
padding: 0.7rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .kanban-column {
|
||||||
|
border-color: rgb(55 65 81);
|
||||||
|
background: rgb(17 24 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .kanban-column-header {
|
||||||
|
border-bottom-color: rgb(55 65 81);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .kanban-dropzone-active {
|
||||||
|
background: rgb(30 41 59);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .kanban-card {
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
background: rgb(31 41 55);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .kanban-card-description {
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .kanban-empty {
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
color: rgb(156 163 175);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .form-label {
|
||||||
|
color: rgb(229 231 235);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,7 @@ import dayjs from "dayjs"
|
|||||||
|
|
||||||
|
|
||||||
import projecttype from "~/components/columnRenderings/projecttype.vue"
|
import projecttype from "~/components/columnRenderings/projecttype.vue"
|
||||||
|
import contracttype from "~/components/columnRenderings/contracttype.vue"
|
||||||
import customer from "~/components/columnRenderings/customer.vue"
|
import customer from "~/components/columnRenderings/customer.vue"
|
||||||
import contact from "~/components/columnRenderings/contact.vue"
|
import contact from "~/components/columnRenderings/contact.vue"
|
||||||
import plant from "~/components/columnRenderings/plant.vue"
|
import plant from "~/components/columnRenderings/plant.vue"
|
||||||
@@ -53,7 +54,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
isArchivable: true,
|
isArchivable: true,
|
||||||
label: "Aufgaben",
|
label: "Aufgaben",
|
||||||
labelSingle: "Aufgabe",
|
labelSingle: "Aufgabe",
|
||||||
isStandardEntity: true,
|
isStandardEntity: false,
|
||||||
redirect: true,
|
redirect: true,
|
||||||
historyItemHolder: "task",
|
historyItemHolder: "task",
|
||||||
selectWithInformation: "*, plant(*), project(*), customer(*)",
|
selectWithInformation: "*, plant(*), project(*), customer(*)",
|
||||||
@@ -582,7 +583,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
"Allgemeines",
|
"Allgemeines",
|
||||||
"Abrechnung"
|
"Abrechnung"
|
||||||
],
|
],
|
||||||
selectWithInformation: "*, customer(*), files(*)",
|
selectWithInformation: "*, customer(*), contracttype(*), files(*)",
|
||||||
templateColumns: [
|
templateColumns: [
|
||||||
{
|
{
|
||||||
key: 'contractNumber',
|
key: 'contractNumber',
|
||||||
@@ -600,6 +601,23 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
inputType: "text",
|
inputType: "text",
|
||||||
inputColumn: "Allgemeines",
|
inputColumn: "Allgemeines",
|
||||||
sortable: true
|
sortable: true
|
||||||
|
},{
|
||||||
|
key: "contracttype",
|
||||||
|
label: "Vertragstyp",
|
||||||
|
component: contracttype,
|
||||||
|
inputType: "select",
|
||||||
|
selectDataType: "contracttypes",
|
||||||
|
selectOptionAttribute: "name",
|
||||||
|
selectSearchAttributes: ["name"],
|
||||||
|
inputChangeFunction: function (item, loadedOptions = {}) {
|
||||||
|
const selectedContractType = (loadedOptions.contracttypes || []).find(i => i.id === item.contracttype)
|
||||||
|
if (!selectedContractType) return
|
||||||
|
|
||||||
|
item.paymentType = selectedContractType.paymentType || null
|
||||||
|
item.recurring = Boolean(selectedContractType.recurring)
|
||||||
|
item.billingInterval = selectedContractType.billingInterval || null
|
||||||
|
},
|
||||||
|
inputColumn: "Allgemeines"
|
||||||
},{
|
},{
|
||||||
key: "active",
|
key: "active",
|
||||||
label: "Aktiv",
|
label: "Aktiv",
|
||||||
@@ -670,6 +688,19 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
{label:'Überweisung'}
|
{label:'Überweisung'}
|
||||||
],
|
],
|
||||||
inputColumn: "Abrechnung"
|
inputColumn: "Abrechnung"
|
||||||
|
},{
|
||||||
|
key: "billingInterval",
|
||||||
|
label: "Abrechnungsintervall",
|
||||||
|
inputType: "select",
|
||||||
|
selectValueAttribute: "label",
|
||||||
|
selectManualOptions: [
|
||||||
|
{ label: "Monatlich" },
|
||||||
|
{ label: "Quartalsweise" },
|
||||||
|
{ label: "Halbjährlich" },
|
||||||
|
{ label: "Jährlich" }
|
||||||
|
],
|
||||||
|
inputColumn: "Abrechnung",
|
||||||
|
sortable: true
|
||||||
},{
|
},{
|
||||||
key: 'startDate',
|
key: 'startDate',
|
||||||
label: "Vertragsstart",
|
label: "Vertragsstart",
|
||||||
@@ -730,6 +761,75 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
],
|
],
|
||||||
showTabs: [{label: 'Informationen'},{label: 'Dateien'},{label: 'Wiki'}]
|
showTabs: [{label: 'Informationen'},{label: 'Dateien'},{label: 'Wiki'}]
|
||||||
},
|
},
|
||||||
|
contracttypes: {
|
||||||
|
isArchivable: true,
|
||||||
|
label: "Vertragstypen",
|
||||||
|
labelSingle: "Vertragstyp",
|
||||||
|
isStandardEntity: true,
|
||||||
|
redirect: true,
|
||||||
|
sortColumn: "name",
|
||||||
|
selectWithInformation: "*",
|
||||||
|
historyItemHolder: "contracttype",
|
||||||
|
filters: [{
|
||||||
|
name: "Archivierte ausblenden",
|
||||||
|
default: true,
|
||||||
|
"filterFunction": function (row) {
|
||||||
|
if(!row.archived) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
templateColumns: [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "Name",
|
||||||
|
required: true,
|
||||||
|
title: true,
|
||||||
|
inputType: "text",
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "description",
|
||||||
|
label: "Beschreibung",
|
||||||
|
inputType: "textarea",
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "paymentType",
|
||||||
|
label: "Zahlart",
|
||||||
|
inputType: "select",
|
||||||
|
selectValueAttribute: "label",
|
||||||
|
selectManualOptions: [
|
||||||
|
{ label: "Einzug" },
|
||||||
|
{ label: "Überweisung" }
|
||||||
|
],
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "billingInterval",
|
||||||
|
label: "Abrechnungsintervall",
|
||||||
|
inputType: "select",
|
||||||
|
selectValueAttribute: "label",
|
||||||
|
selectManualOptions: [
|
||||||
|
{ label: "Monatlich" },
|
||||||
|
{ label: "Quartalsweise" },
|
||||||
|
{ label: "Halbjährlich" },
|
||||||
|
{ label: "Jährlich" }
|
||||||
|
],
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "recurring",
|
||||||
|
label: "Wiederkehrend",
|
||||||
|
inputType: "bool",
|
||||||
|
component: recurring,
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
showTabs: [{ label: "Informationen" }]
|
||||||
|
},
|
||||||
absencerequests: {
|
absencerequests: {
|
||||||
isArchivable: true,
|
isArchivable: true,
|
||||||
label: "Abwesenheiten",
|
label: "Abwesenheiten",
|
||||||
|
|||||||
Reference in New Issue
Block a user