diff --git a/backend/db/migrations/0027_product_supplier_link.sql b/backend/db/migrations/0027_product_supplier_link.sql
new file mode 100644
index 0000000..064628b
--- /dev/null
+++ b/backend/db/migrations/0027_product_supplier_link.sql
@@ -0,0 +1,2 @@
+ALTER TABLE "products"
+ADD COLUMN "supplierLink" text;
diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json
index 6e7e0a9..1b1c5d9 100644
--- a/backend/db/migrations/meta/_journal.json
+++ b/backend/db/migrations/meta/_journal.json
@@ -190,6 +190,13 @@
"when": 1774393202000,
"tag": "0026_statementallocation_depreciation_method",
"breakpoints": true
+ },
+ {
+ "idx": 27,
+ "version": "7",
+ "when": 1774602000000,
+ "tag": "0027_product_supplier_link",
+ "breakpoints": true
}
]
}
diff --git a/backend/db/schema/products.ts b/backend/db/schema/products.ts
index d070254..ffce4a5 100644
--- a/backend/db/schema/products.ts
+++ b/backend/db/schema/products.ts
@@ -50,6 +50,7 @@ export const products = pgTable("products", {
vendor_allocation: jsonb("vendorAllocation").default([]),
article_number: text("articleNumber"),
+ supplier_link: text("supplierLink"),
barcodes: text("barcodes").array().notNull().default([]),
diff --git a/frontend/components/columnRenderings/externalLink.vue b/frontend/components/columnRenderings/externalLink.vue
new file mode 100644
index 0000000..8d6be39
--- /dev/null
+++ b/frontend/components/columnRenderings/externalLink.vue
@@ -0,0 +1,48 @@
+
+
+
+
+ {{ resolvedKey ? row[resolvedKey] : "" }}
+
+ -
+
diff --git a/frontend/pages/createDocument/edit/[[id]].vue b/frontend/pages/createDocument/edit/[[id]].vue
index 3f941b1..e12d4dc 100644
--- a/frontend/pages/createDocument/edit/[[id]].vue
+++ b/frontend/pages/createDocument/edit/[[id]].vue
@@ -51,6 +51,7 @@ const itemInfo = ref({
dateOfPerformance: null,
paymentDays: auth.activeTenantData.standardPaymentDays,
payment_type: "transfer",
+ availableInPortal: false,
customSurchargePercentage: 0,
created_by: auth.user.id,
title: null,
@@ -111,6 +112,18 @@ const serialIntervalItems = ['wöchentlich', '2 - wöchentlich', 'monatlich', 'v
const serialDateDirectionItems = ['Rückwirkend', 'Im Voraus']
const taxPercentItems = [19, 7, 0]
const selectedCustomer = computed(() => customers.value.find(i => i.id === itemInfo.value.customer) || null)
+const normalizeExternalUrl = (value) => {
+ if (!value || typeof value !== "string") return null
+
+ const trimmedValue = value.trim()
+ if (!trimmedValue) return null
+
+ if (/^https?:\/\//i.test(trimmedValue)) return trimmedValue
+
+ return `https://${trimmedValue}`
+}
+
+const getSelectedProduct = (row) => products.value.find((item) => item.id === row.product) || null
const formatNumberLikeValue = (value) => {
if (value === null || typeof value === 'undefined' || value === '') return '-'
if (typeof value === 'string' || typeof value === 'number') return String(value)
@@ -1438,7 +1451,8 @@ const saveSerialInvoice = async () => {
contactPerson: itemInfo.value.contactPerson,
serialConfig: itemInfo.value.serialConfig,
letterhead: itemInfo.value.letterhead,
- taxType:itemInfo.value.taxType
+ taxType:itemInfo.value.taxType,
+ availableInPortal: itemInfo.value.availableInPortal
}
let data = null
@@ -1527,6 +1541,7 @@ const saveDocument = async (state, resetup = false) => {
agriculture: itemInfo.value.agriculture,
letterhead: itemInfo.value.letterhead,
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices,
+ availableInPortal: itemInfo.value.availableInPortal,
customSurchargePercentage: itemInfo.value.customSurchargePercentage,
report: documentReport.value
}
@@ -2234,6 +2249,12 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
class="w-full"
/>
+
+
+
@@ -2589,24 +2610,39 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
v-else-if="row.mode === 'normal'"
>
- setRowData(row)"
- >
-
- {{
- products.find(i => i.id === row.product) ? products.find(i => i.id === row.product).name : "Kein Produkt ausgewählt"
- }}
-
-
+
+
setRowData(row)"
+ >
+
+ {{
+ getSelectedProduct(row) ? getSelectedProduct(row).name : "Kein Produkt ausgewählt"
+ }}
+
+
+
+
{
absenceForm[field] = $dayjs().format("YYYY-MM-DD")
}
+const startDateValue = computed({
+ get: () => {
+ if (!absenceForm.startDate) return null
+
+ try {
+ return parseDate(absenceForm.startDate)
+ } catch {
+ return null
+ }
+ },
+ set: (value) => {
+ absenceForm.startDate = value ? value.toString() : ""
+ }
+})
+
+const endDateValue = computed({
+ get: () => {
+ if (!absenceForm.endDate) return null
+
+ try {
+ return parseDate(absenceForm.endDate)
+ } catch {
+ return null
+ }
+ },
+ set: (value) => {
+ absenceForm.endDate = value ? value.toString() : ""
+ }
+})
+
const resourceTypeOptions = [
{ label: "Alle Ressourcen", value: "all" },
{ label: "Profile", value: "Profile" },
{ label: "Inventarartikel", value: "Inventarartikel" }
]
+const calendarViewOptions = [
+ { label: "Tag", value: "resourceTimelineDay" },
+ { label: "Woche", value: "resourceTimelineWeek" },
+ { label: "Monat", value: "resourceTimelineMonth" }
+]
+
const absenceTypeOptions = [
{ label: "Urlaub", value: "vacation" },
{ label: "Krank", value: "sick" }
]
+const calendarPickerValue = computed({
+ get: () => {
+ if (!calendarCurrentDate.value) return null
+
+ try {
+ return parseDate(calendarCurrentDate.value)
+ } catch {
+ return null
+ }
+ },
+ set: (value) => {
+ calendarCurrentDate.value = value ? value.toString() : ""
+ if (value) {
+ const api = calendarRef.value?.getApi?.()
+ api?.gotoDate(value.toString())
+ }
+ }
+})
+
const profileOptions = computed(() =>
profiles.value
.filter((profile) => !profile.archived && profile.user_id)
@@ -83,12 +143,9 @@ const calendarOptions = computed(() => ({
schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
locale: deLocale,
plugins: [resourceTimelinePlugin, interactionPlugin],
- initialView: "resourceTimelineWeek",
- headerToolbar: {
- left: "prev,next today",
- center: "title",
- right: "resourceTimelineDay,resourceTimelineWeek,resourceTimelineMonth"
- },
+ initialView: calendarView.value,
+ initialDate: calendarCurrentDate.value,
+ headerToolbar: false,
resourceAreaWidth: "280px",
resourceGroupField: "type",
resourceOrder: "type,title",
@@ -149,6 +206,10 @@ const calendarOptions = computed(() => ({
const nextTo = $dayjs(info.end).subtract(1, "day").format("YYYY-MM-DD")
const nextKey = `${nextFrom}:${nextTo}`
+ calendarView.value = info.view.type
+ calendarCurrentDate.value = $dayjs(info.view.currentStart).format("YYYY-MM-DD")
+ calendarTitle.value = info.view.title
+
if (nextKey === lastRangeKey.value) return
lastRangeKey.value = nextKey
@@ -264,6 +325,31 @@ function resetAbsenceForm() {
absenceForm.description = ""
}
+function getCalendarApi() {
+ return calendarRef.value?.getApi?.()
+}
+
+function changeCalendarView(view) {
+ const api = getCalendarApi()
+ if (!api || !view) return
+ calendarView.value = view
+ api.changeView(view)
+}
+
+function moveCalendar(direction) {
+ const api = getCalendarApi()
+ if (!api) return
+
+ if (direction === "prev") api.prev()
+ if (direction === "next") api.next()
+}
+
+function moveCalendarToday() {
+ const api = getCalendarApi()
+ if (!api) return
+ api.today()
+}
+
function openAbsenceModal(type = "vacation", preset = {}) {
absenceForm.mode = preset.entry ? "edit" : "create"
absenceForm.entry = preset.entry || null
@@ -404,12 +490,49 @@ onMounted(() => {
+
+
+
+ {{ calendarCurrentDate ? $dayjs(calendarCurrentDate).format("DD.MM.YYYY") : "Datum wählen" }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ calendarTitle }}
+
@@ -449,6 +572,7 @@ onMounted(() => {
@@ -475,33 +599,82 @@ onMounted(() => {
-
-
+
+
+
+ {{ absenceForm.startDate ? $dayjs(absenceForm.startDate).format("DD.MM.YYYY") : "Kein Datum" }}
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+ {{ absenceForm.endDate ? $dayjs(absenceForm.endDate).format("DD.MM.YYYY") : "Kein Datum" }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/stores/data.js b/frontend/stores/data.js
index 7ea75bf..c634ce7 100644
--- a/frontend/stores/data.js
+++ b/frontend/stores/data.js
@@ -36,6 +36,7 @@ import startDateTime from "~/components/columnRenderings/startDateTime.vue"
import endDateTime from "~/components/columnRenderings/endDateTime.vue"
import serviceCategories from "~/components/columnRenderings/serviceCategories.vue"
import productcategoriesWithLoad from "~/components/columnRenderings/productcategoriesWithLoad.vue"
+import externalLink from "~/components/columnRenderings/externalLink.vue"
import phase from "~/components/columnRenderings/phase.vue"
import vehiclesWithLoad from "~/components/columnRenderings/vehiclesWithLoad.vue"
import inventoryitemsWithLoad from "~/components/columnRenderings/inventoryitemsWithLoad.vue"
@@ -1380,6 +1381,12 @@ export const useDataStore = defineStore('data', () => {
label: "Beschreibung",
inputType:"textarea"
},
+ {
+ key: "supplier_link",
+ label: "Link zum Lieferanten",
+ inputType: "text",
+ component: externalLink
+ },
],
showTabs: [
{