diff --git a/backend/db/migrations/0018_account_chart.sql b/backend/db/migrations/0018_account_chart.sql new file mode 100644 index 0000000..a885f0b --- /dev/null +++ b/backend/db/migrations/0018_account_chart.sql @@ -0,0 +1,3 @@ +ALTER TABLE "accounts" ADD COLUMN "accountChart" text DEFAULT 'skr03' NOT NULL; +--> statement-breakpoint +ALTER TABLE "tenants" ADD COLUMN "accountChart" text DEFAULT 'skr03' NOT NULL; diff --git a/backend/db/migrations/0019_custom_surcharge_percentage_decimal.sql b/backend/db/migrations/0019_custom_surcharge_percentage_decimal.sql new file mode 100644 index 0000000..2267fdb --- /dev/null +++ b/backend/db/migrations/0019_custom_surcharge_percentage_decimal.sql @@ -0,0 +1,3 @@ +ALTER TABLE "createddocuments" +ALTER COLUMN "customSurchargePercentage" TYPE double precision +USING "customSurchargePercentage"::double precision; diff --git a/backend/db/schema/createddocuments.ts b/backend/db/schema/createddocuments.ts index 9fd881a..c98d927 100644 --- a/backend/db/schema/createddocuments.ts +++ b/backend/db/schema/createddocuments.ts @@ -6,6 +6,7 @@ import { jsonb, boolean, smallint, + doublePrecision, uuid, } from "drizzle-orm/pg-core" @@ -96,7 +97,7 @@ export const createddocuments = pgTable("createddocuments", { taxType: text("taxType"), - customSurchargePercentage: smallint("customSurchargePercentage") + customSurchargePercentage: doublePrecision("customSurchargePercentage") .notNull() .default(0), diff --git a/backend/db/schema/tenants.ts b/backend/db/schema/tenants.ts index 82f4ecb..826159f 100644 --- a/backend/db/schema/tenants.ts +++ b/backend/db/schema/tenants.ts @@ -74,6 +74,48 @@ export const tenants = pgTable( timeTracking: true, planningBoard: true, workingTimeTracking: true, + dashboard: true, + historyitems: true, + tasks: true, + wiki: true, + files: true, + createdletters: true, + documentboxes: true, + helpdesk: true, + email: true, + members: true, + customers: true, + vendors: true, + contactsList: true, + staffTime: true, + createDocument: true, + serialInvoice: true, + incomingInvoices: true, + costcentres: true, + accounts: true, + ownaccounts: true, + banking: true, + spaces: true, + customerspaces: true, + customerinventoryitems: true, + inventoryitems: true, + inventoryitemgroups: true, + products: true, + productcategories: true, + services: true, + servicecategories: true, + memberrelations: true, + staffProfiles: true, + hourrates: true, + projecttypes: true, + contracttypes: true, + plants: true, + settingsNumberRanges: true, + settingsEmailAccounts: true, + settingsBanking: true, + settingsTexttemplates: true, + settingsTenant: true, + export: true, }), ownFields: jsonb("ownFields"), diff --git a/backend/package.json b/backend/package.json index 4682ecd..7aee79f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,8 @@ "start": "node dist/src/index.js", "schema:index": "ts-node scripts/generate-schema-index.ts", "bankcodes:update": "tsx scripts/generate-de-bank-codes.ts", - "members:import:csv": "tsx scripts/import-members-csv.ts" + "members:import:csv": "tsx scripts/import-members-csv.ts", + "accounts:import:skr42": "ts-node scripts/import-skr42-accounts.ts" }, "repository": { "type": "git", diff --git a/backend/src/modules/service-price-recalculation.service.ts b/backend/src/modules/service-price-recalculation.service.ts index bdca89f..0804f66 100644 --- a/backend/src/modules/service-price-recalculation.service.ts +++ b/backend/src/modules/service-price-recalculation.service.ts @@ -38,6 +38,11 @@ function normalizeUuid(value: unknown): string | null { return trimmed.length ? trimmed : null; } +function sanitizeCompositionRows(value: unknown): CompositionRow[] { + if (!Array.isArray(value)) return []; + return value.filter((entry): entry is CompositionRow => !!entry && typeof entry === "object"); +} + export async function recalculateServicePricesForTenant(server: FastifyInstance, tenantId: number, updatedBy?: string | null) { const [services, products, hourrates] = await Promise.all([ server.db.select().from(schema.services).where(eq(schema.services.tenant, tenantId)), @@ -88,94 +93,111 @@ export async function recalculateServicePricesForTenant(server: FastifyInstance, materialPurchaseTotal: getJsonNumber(service.purchasePriceComposed, "material"), workerTotal: getJsonNumber(service.sellingPriceComposed, "worker"), workerPurchaseTotal: getJsonNumber(service.purchasePriceComposed, "worker"), - materialComposition: Array.isArray(service.materialComposition) ? service.materialComposition as CompositionRow[] : [], - personalComposition: Array.isArray(service.personalComposition) ? service.personalComposition as CompositionRow[] : [], + materialComposition: sanitizeCompositionRows(service.materialComposition), + personalComposition: sanitizeCompositionRows(service.personalComposition), }; memo.set(serviceId, lockedResult); return lockedResult; } stack.add(serviceId); + try { + const materialComposition = sanitizeCompositionRows(service.materialComposition); + const personalComposition = sanitizeCompositionRows(service.personalComposition); + const hasMaterialComposition = materialComposition.length > 0; + const hasPersonalComposition = personalComposition.length > 0; - const materialComposition: CompositionRow[] = Array.isArray(service.materialComposition) - ? (service.materialComposition as CompositionRow[]) - : []; - const personalComposition: CompositionRow[] = Array.isArray(service.personalComposition) - ? (service.personalComposition as CompositionRow[]) - : []; - - let materialTotal = 0; - let materialPurchaseTotal = 0; - - const normalizedMaterialComposition = materialComposition.map((entry) => { - const quantity = toNumber(entry.quantity); - const productId = normalizeId(entry.product); - const childServiceId = normalizeId(entry.service); - - let sellingPrice = toNumber(entry.price); - let purchasePrice = toNumber(entry.purchasePrice); - - if (productId) { - const product = productMap.get(productId); - sellingPrice = toNumber(product?.selling_price); - purchasePrice = toNumber(product?.purchase_price); - } else if (childServiceId) { - const child = calculateService(childServiceId); - sellingPrice = toNumber(child.sellingTotal); - purchasePrice = toNumber(child.purchaseTotal); + // Ohne Zusammensetzung keine automatische Überschreibung: + // manuell gepflegte Preise sollen erhalten bleiben. + if (!hasMaterialComposition && !hasPersonalComposition) { + const manualResult = { + sellingTotal: getJsonNumber(service.sellingPriceComposed, "total") || toNumber(service.sellingPrice), + purchaseTotal: getJsonNumber(service.purchasePriceComposed, "total"), + materialTotal: getJsonNumber(service.sellingPriceComposed, "material"), + materialPurchaseTotal: getJsonNumber(service.purchasePriceComposed, "material"), + workerTotal: getJsonNumber(service.sellingPriceComposed, "worker"), + workerPurchaseTotal: getJsonNumber(service.purchasePriceComposed, "worker"), + materialComposition, + personalComposition, + }; + memo.set(serviceId, manualResult); + return manualResult; } - materialTotal += quantity * sellingPrice; - materialPurchaseTotal += quantity * purchasePrice; + let materialTotal = 0; + let materialPurchaseTotal = 0; - return { - ...entry, - price: round2(sellingPrice), - purchasePrice: round2(purchasePrice), - }; - }); + const normalizedMaterialComposition = materialComposition.map((entry) => { + const quantity = toNumber(entry.quantity); + const productId = normalizeId(entry.product); + const childServiceId = normalizeId(entry.service); - let workerTotal = 0; - let workerPurchaseTotal = 0; - const normalizedPersonalComposition = personalComposition.map((entry) => { - const quantity = toNumber(entry.quantity); - const hourrateId = normalizeUuid(entry.hourrate); + let sellingPrice = toNumber(entry.price); + let purchasePrice = toNumber(entry.purchasePrice); - let sellingPrice = toNumber(entry.price); - let purchasePrice = toNumber(entry.purchasePrice); - - if (hourrateId) { - const hourrate = hourrateMap.get(hourrateId); - if (hourrate) { - sellingPrice = toNumber(hourrate.sellingPrice); - purchasePrice = toNumber(hourrate.purchase_price); + if (productId) { + const product = productMap.get(productId); + sellingPrice = toNumber(product?.selling_price); + purchasePrice = toNumber(product?.purchase_price); + } else if (childServiceId) { + const child = calculateService(childServiceId); + sellingPrice = toNumber(child.sellingTotal); + purchasePrice = toNumber(child.purchaseTotal); } - } - workerTotal += quantity * sellingPrice; - workerPurchaseTotal += quantity * purchasePrice; + materialTotal += quantity * sellingPrice; + materialPurchaseTotal += quantity * purchasePrice; - return { - ...entry, - price: round2(sellingPrice), - purchasePrice: round2(purchasePrice), + return { + ...entry, + price: round2(sellingPrice), + purchasePrice: round2(purchasePrice), + }; + }); + + let workerTotal = 0; + let workerPurchaseTotal = 0; + const normalizedPersonalComposition = personalComposition.map((entry) => { + const quantity = toNumber(entry.quantity); + const hourrateId = normalizeUuid(entry.hourrate); + + let sellingPrice = toNumber(entry.price); + let purchasePrice = toNumber(entry.purchasePrice); + + if (hourrateId) { + const hourrate = hourrateMap.get(hourrateId); + if (hourrate) { + sellingPrice = toNumber(hourrate.sellingPrice); + purchasePrice = toNumber(hourrate.purchase_price); + } + } + + workerTotal += quantity * sellingPrice; + workerPurchaseTotal += quantity * purchasePrice; + + return { + ...entry, + price: round2(sellingPrice), + purchasePrice: round2(purchasePrice), + }; + }); + + const result = { + sellingTotal: round2(materialTotal + workerTotal), + purchaseTotal: round2(materialPurchaseTotal + workerPurchaseTotal), + materialTotal: round2(materialTotal), + materialPurchaseTotal: round2(materialPurchaseTotal), + workerTotal: round2(workerTotal), + workerPurchaseTotal: round2(workerPurchaseTotal), + materialComposition: normalizedMaterialComposition, + personalComposition: normalizedPersonalComposition, }; - }); - const result = { - sellingTotal: round2(materialTotal + workerTotal), - purchaseTotal: round2(materialPurchaseTotal + workerPurchaseTotal), - materialTotal: round2(materialTotal), - materialPurchaseTotal: round2(materialPurchaseTotal), - workerTotal: round2(workerTotal), - workerPurchaseTotal: round2(workerPurchaseTotal), - materialComposition: normalizedMaterialComposition, - personalComposition: normalizedPersonalComposition, - }; - - memo.set(serviceId, result); - stack.delete(serviceId); - return result; + memo.set(serviceId, result); + return result; + } finally { + stack.delete(serviceId); + } }; for (const service of services) { diff --git a/backend/src/plugins/queryconfig.ts b/backend/src/plugins/queryconfig.ts index 3ecb8c0..8c3c82b 100644 --- a/backend/src/plugins/queryconfig.ts +++ b/backend/src/plugins/queryconfig.ts @@ -58,8 +58,6 @@ const queryConfigPlugin: FastifyPluginAsync = async ( const query = req.query as Record - console.log(query) - // Pagination deaktivieren? const disablePagination = query.noPagination === 'true' || diff --git a/backend/src/routes/auth/me.ts b/backend/src/routes/auth/me.ts index 2db6457..d1700fa 100644 --- a/backend/src/routes/auth/me.ts +++ b/backend/src/routes/auth/me.ts @@ -51,6 +51,7 @@ export default async function meRoutes(server: FastifyInstance) { name: tenants.name, short: tenants.short, locked: tenants.locked, + features: tenants.features, extraModules: tenants.extraModules, businessInfo: tenants.businessInfo, numberRanges: tenants.numberRanges, diff --git a/frontend/components/MainNav.vue b/frontend/components/MainNav.vue index c93d910..f3095b0 100644 --- a/frontend/components/MainNav.vue +++ b/frontend/components/MainNav.vue @@ -15,8 +15,241 @@ const showMembersNav = computed(() => { const showMemberRelationsNav = computed(() => { return tenantExtraModules.value.includes("verein") && has("members") }) +const tenantFeatures = computed(() => auth.activeTenantData?.features || {}) +const featureEnabled = (key) => tenantFeatures.value?.[key] !== false const links = computed(() => { + const organisationChildren = [ + has("tasks") && featureEnabled("tasks") ? { + label: "Aufgaben", + to: "/tasks", + icon: "i-heroicons-rectangle-stack" + } : null, + featureEnabled("wiki") ? { + label: "Wiki", + to: "/wiki", + icon: "i-heroicons-book-open" + } : null, + ].filter(Boolean) + + const documentChildren = [ + featureEnabled("files") ? { + label: "Dateien", + to: "/files", + icon: "i-heroicons-document" + } : null, + featureEnabled("createdletters") ? { + label: "Anschreiben", + to: "/createdletters", + icon: "i-heroicons-document", + disabled: true + } : null, + featureEnabled("documentboxes") ? { + label: "Boxen", + to: "/standardEntity/documentboxes", + icon: "i-heroicons-archive-box", + disabled: true + } : null, + ].filter(Boolean) + + const communicationChildren = [ + featureEnabled("helpdesk") ? { + label: "Helpdesk", + to: "/helpdesk", + icon: "i-heroicons-chat-bubble-left-right", + disabled: true + } : null, + featureEnabled("email") ? { + label: "E-Mail", + to: "/email/new", + icon: "i-heroicons-envelope", + disabled: true + } : null, + ].filter(Boolean) + + const contactsChildren = [ + showMembersNav.value && featureEnabled("members") ? { + label: "Mitglieder", + to: "/standardEntity/members", + icon: "i-heroicons-user-group" + } : null, + has("customers") && featureEnabled("customers") ? { + label: "Kunden", + to: "/standardEntity/customers", + icon: "i-heroicons-user-group" + } : null, + has("vendors") && featureEnabled("vendors") ? { + label: "Lieferanten", + to: "/standardEntity/vendors", + icon: "i-heroicons-truck" + } : null, + has("contacts") && featureEnabled("contactsList") ? { + label: "Ansprechpartner", + to: "/standardEntity/contacts", + icon: "i-heroicons-user-group" + } : null, + ].filter(Boolean) + + const staffChildren = [ + featureEnabled("staffTime") ? { + label: "Zeiten", + to: "/staff/time", + icon: "i-heroicons-clock", + } : null, + ].filter(Boolean) + + const accountingChildren = [ + featureEnabled("createDocument") ? { + label: "Ausgangsbelege", + to: "/createDocument", + icon: "i-heroicons-document-text" + } : null, + featureEnabled("serialInvoice") ? { + label: "Serienvorlagen", + to: "/createDocument/serialInvoice", + icon: "i-heroicons-document-text" + } : null, + featureEnabled("incomingInvoices") ? { + label: "Eingangsbelege", + to: "/incomingInvoices", + icon: "i-heroicons-document-text", + } : null, + featureEnabled("costcentres") ? { + label: "Kostenstellen", + to: "/standardEntity/costcentres", + icon: "i-heroicons-document-currency-euro" + } : null, + featureEnabled("accounts") ? { + label: "Buchungskonten", + to: "/accounts", + icon: "i-heroicons-document-text", + } : null, + featureEnabled("ownaccounts") ? { + label: "zusätzliche Buchungskonten", + to: "/standardEntity/ownaccounts", + icon: "i-heroicons-document-text" + } : null, + featureEnabled("banking") ? { + label: "Bank", + to: "/banking", + icon: "i-heroicons-document-text", + } : null, + ].filter(Boolean) + + const inventoryChildren = [ + has("spaces") && featureEnabled("spaces") ? { + label: "Lagerplätze", + to: "/standardEntity/spaces", + icon: "i-heroicons-square-3-stack-3d" + } : null, + has("inventoryitems") && featureEnabled("customerspaces") ? { + label: "Kundenlagerplätze", + to: "/standardEntity/customerspaces", + icon: "i-heroicons-squares-plus" + } : null, + has("inventoryitems") && featureEnabled("customerinventoryitems") ? { + label: "Kundeninventar", + to: "/standardEntity/customerinventoryitems", + icon: "i-heroicons-qr-code" + } : null, + has("inventoryitems") && featureEnabled("inventoryitems") ? { + label: "Inventar", + to: "/standardEntity/inventoryitems", + icon: "i-heroicons-puzzle-piece" + } : null, + has("inventoryitems") && featureEnabled("inventoryitemgroups") ? { + label: "Inventargruppen", + to: "/standardEntity/inventoryitemgroups", + icon: "i-heroicons-puzzle-piece" + } : null, + ].filter(Boolean) + + const masterDataChildren = [ + has("products") && featureEnabled("products") ? { + label: "Artikel", + to: "/standardEntity/products", + icon: "i-heroicons-puzzle-piece" + } : null, + has("productcategories") && featureEnabled("productcategories") ? { + label: "Artikelkategorien", + to: "/standardEntity/productcategories", + icon: "i-heroicons-puzzle-piece" + } : null, + has("services") && featureEnabled("services") ? { + label: "Leistungen", + to: "/standardEntity/services", + icon: "i-heroicons-wrench-screwdriver" + } : null, + has("servicecategories") && featureEnabled("servicecategories") ? { + label: "Leistungskategorien", + to: "/standardEntity/servicecategories", + icon: "i-heroicons-wrench-screwdriver" + } : null, + showMemberRelationsNav.value && featureEnabled("memberrelations") ? { + label: "Mitgliedsverhältnisse", + to: "/standardEntity/memberrelations", + icon: "i-heroicons-identification" + } : null, + featureEnabled("staffProfiles") ? { + label: "Mitarbeiter", + to: "/staff/profiles", + icon: "i-heroicons-user-group" + } : null, + featureEnabled("hourrates") ? { + label: "Stundensätze", + to: "/standardEntity/hourrates", + icon: "i-heroicons-user-group" + } : null, + featureEnabled("projecttypes") ? { + label: "Projekttypen", + to: "/projecttypes", + icon: "i-heroicons-clipboard-document-list", + } : null, + featureEnabled("contracttypes") ? { + label: "Vertragstypen", + to: "/standardEntity/contracttypes", + icon: "i-heroicons-document-duplicate", + } : null, + has("vehicles") && featureEnabled("vehicles") ? { + label: "Fahrzeuge", + to: "/standardEntity/vehicles", + icon: "i-heroicons-truck" + } : null, + ].filter(Boolean) + + const settingsChildren = [ + featureEnabled("settingsNumberRanges") ? { + label: "Nummernkreise", + to: "/settings/numberRanges", + icon: "i-heroicons-clipboard-document-list", + } : null, + featureEnabled("settingsEmailAccounts") ? { + label: "E-Mail Konten", + to: "/settings/emailaccounts", + icon: "i-heroicons-envelope", + } : null, + featureEnabled("settingsBanking") ? { + label: "Bankkonten", + to: "/settings/banking", + icon: "i-heroicons-currency-euro", + } : null, + featureEnabled("settingsTexttemplates") ? { + label: "Textvorlagen", + to: "/settings/texttemplates", + icon: "i-heroicons-clipboard-document-list", + } : null, + featureEnabled("settingsTenant") ? { + label: "Firmeneinstellungen", + to: "/settings/tenant", + icon: "i-heroicons-building-office", + } : null, + featureEnabled("export") ? { + label: "Export", + to: "/export", + icon: "i-heroicons-clipboard-document-list" + } : null, + ].filter(Boolean) + return [ ...(auth.profile?.pinned_on_navigation || []).map(pin => { if (pin.type === "external") { @@ -37,290 +270,89 @@ const links = computed(() => { } }), - { + featureEnabled("dashboard") ? { id: 'dashboard', label: "Dashboard", to: "/", icon: "i-heroicons-home" - }, - { + } : null, + featureEnabled("historyitems") ? { id: 'historyitems', label: "Logbuch", to: "/historyitems", icon: "i-heroicons-book-open" - }, - { + } : null, + ...(organisationChildren.length > 0 ? [{ label: "Organisation", icon: "i-heroicons-rectangle-stack", defaultOpen: false, - children: [ - ...has("tasks") ? [{ - label: "Aufgaben", - to: "/tasks", - icon: "i-heroicons-rectangle-stack" - }] : [], - ...true ? [{ - label: "Wiki", - to: "/wiki", - icon: "i-heroicons-book-open" - }] : [], - ] - }, - { + children: organisationChildren + }] : []), + ...(documentChildren.length > 0 ? [{ label: "Dokumente", icon: "i-heroicons-rectangle-stack", defaultOpen: false, - children: [ - { - label: "Dateien", - to: "/files", - icon: "i-heroicons-document" - }, { - label: "Anschreiben", - to: "/createdletters", - icon: "i-heroicons-document", - disabled: true - }, { - label: "Boxen", - to: "/standardEntity/documentboxes", - icon: "i-heroicons-archive-box", - disabled: true - }, - ] - }, - { + children: documentChildren + }] : []), + ...(communicationChildren.length > 0 ? [{ label: "Kommunikation", icon: "i-heroicons-megaphone", defaultOpen: false, - children: [ - { - label: "Helpdesk", - to: "/helpdesk", - icon: "i-heroicons-chat-bubble-left-right", - disabled: true - }, - { - label: "E-Mail", - to: "/email/new", - icon: "i-heroicons-envelope", - disabled: true - } - ] - }, - ...(has("customers") || has("vendors") || has("contacts") || showMembersNav.value) ? [{ + children: communicationChildren + }] : []), + ...(contactsChildren.length > 0 ? [{ label: "Kontakte", defaultOpen: false, icon: "i-heroicons-user-group", - children: [ - ...showMembersNav.value ? [{ - label: "Mitglieder", - to: "/standardEntity/members", - icon: "i-heroicons-user-group" - }] : [], - ...has("customers") ? [{ - label: "Kunden", - to: "/standardEntity/customers", - icon: "i-heroicons-user-group" - }] : [], - ...has("vendors") ? [{ - label: "Lieferanten", - to: "/standardEntity/vendors", - icon: "i-heroicons-truck" - }] : [], - ...has("contacts") ? [{ - label: "Ansprechpartner", - to: "/standardEntity/contacts", - icon: "i-heroicons-user-group" - }] : [], - ] - }] : [], - { + children: contactsChildren + }] : []), + ...(staffChildren.length > 0 ? [{ label: "Mitarbeiter", defaultOpen: false, icon: "i-heroicons-user-group", - children: [ - ...true ? [{ - label: "Zeiten", - to: "/staff/time", - icon: "i-heroicons-clock", - }] : [], - ] - }, - ...[{ + children: staffChildren + }] : []), + ...(accountingChildren.length > 0 ? [{ label: "Buchhaltung", defaultOpen: false, icon: "i-heroicons-chart-bar-square", - children: [ - { - label: "Ausgangsbelege", - to: "/createDocument", - icon: "i-heroicons-document-text" - }, { - label: "Serienvorlagen", - to: "/createDocument/serialInvoice", - icon: "i-heroicons-document-text" - }, { - label: "Eingangsbelege", - to: "/incomingInvoices", - icon: "i-heroicons-document-text", - }, { - label: "Kostenstellen", - to: "/standardEntity/costcentres", - icon: "i-heroicons-document-currency-euro" - }, { - label: "Buchungskonten", - to: "/accounts", - icon: "i-heroicons-document-text", - }, { - label: "zusätzliche Buchungskonten", - to: "/standardEntity/ownaccounts", - icon: "i-heroicons-document-text" - }, - { - label: "Bank", - to: "/banking", - icon: "i-heroicons-document-text", - }, - ] - }], - ...has("inventory") ? [{ + children: accountingChildren + }] : []), + ...(inventoryChildren.length > 0 ? [{ label: "Lager", icon: "i-heroicons-puzzle-piece", defaultOpen: false, - children: [ - ...has("spaces") ? [{ - label: "Lagerplätze", - to: "/standardEntity/spaces", - icon: "i-heroicons-square-3-stack-3d" - }] : [], - ...has("inventoryitems") ? [{ - label: "Kundenlagerplätze", - to: "/standardEntity/customerspaces", - icon: "i-heroicons-squares-plus" - }] : [], - ...has("inventoryitems") ? [{ - label: "Kundeninventar", - to: "/standardEntity/customerinventoryitems", - icon: "i-heroicons-qr-code" - }] : [], - ...has("inventoryitems") ? [{ - label: "Inventar", - to: "/standardEntity/inventoryitems", - icon: "i-heroicons-puzzle-piece" - }] : [], - ...has("inventoryitems") ? [{ - label: "Inventargruppen", - to: "/standardEntity/inventoryitemgroups", - icon: "i-heroicons-puzzle-piece" - }] : [], - ] - }] : [], - { + children: inventoryChildren + }] : []), + ...(masterDataChildren.length > 0 ? [{ label: "Stammdaten", defaultOpen: false, icon: "i-heroicons-clipboard-document", - children: [ - ...has("products") ? [{ - label: "Artikel", - to: "/standardEntity/products", - icon: "i-heroicons-puzzle-piece" - }] : [], - ...has("productcategories") ? [{ - label: "Artikelkategorien", - to: "/standardEntity/productcategories", - icon: "i-heroicons-puzzle-piece" - }] : [], - ...has("services") ? [{ - label: "Leistungen", - to: "/standardEntity/services", - icon: "i-heroicons-wrench-screwdriver" - }] : [], - ...has("servicecategories") ? [{ - label: "Leistungskategorien", - to: "/standardEntity/servicecategories", - icon: "i-heroicons-wrench-screwdriver" - }] : [], - ...showMemberRelationsNav.value ? [{ - label: "Mitgliedsverhältnisse", - to: "/standardEntity/memberrelations", - icon: "i-heroicons-identification" - }] : [], - { - label: "Mitarbeiter", - to: "/staff/profiles", - icon: "i-heroicons-user-group" - }, - { - label: "Stundensätze", - to: "/standardEntity/hourrates", - icon: "i-heroicons-user-group" - }, - { - label: "Projekttypen", - to: "/projecttypes", - icon: "i-heroicons-clipboard-document-list", - }, - { - label: "Vertragstypen", - to: "/standardEntity/contracttypes", - icon: "i-heroicons-document-duplicate", - }, - ...has("vehicles") ? [{ - label: "Fahrzeuge", - to: "/standardEntity/vehicles", - icon: "i-heroicons-truck" - }] : [], - ] - }, + children: masterDataChildren + }] : []), - ...has("projects") ? [{ + ...(has("projects") && featureEnabled("projects")) ? [{ label: "Projekte", to: "/standardEntity/projects", icon: "i-heroicons-clipboard-document-check" }] : [], - ...has("contracts") ? [{ + ...(has("contracts") && featureEnabled("contracts")) ? [{ label: "Verträge", to: "/standardEntity/contracts", icon: "i-heroicons-clipboard-document" }] : [], - ...has("plants") ? [{ + ...(has("plants") && featureEnabled("plants")) ? [{ label: "Objekte", to: "/standardEntity/plants", icon: "i-heroicons-clipboard-document" }] : [], - { + ...(settingsChildren.length > 0 ? [{ label: "Einstellungen", defaultOpen: false, icon: "i-heroicons-cog-8-tooth", - children: [ - { - label: "Nummernkreise", - to: "/settings/numberRanges", - icon: "i-heroicons-clipboard-document-list", - }, { - label: "E-Mail Konten", - to: "/settings/emailaccounts", - icon: "i-heroicons-envelope", - }, { - label: "Bankkonten", - to: "/settings/banking", - icon: "i-heroicons-currency-euro", - }, { - label: "Textvorlagen", - to: "/settings/texttemplates", - icon: "i-heroicons-clipboard-document-list", - }, { - label: "Firmeneinstellungen", - to: "/settings/tenant", - icon: "i-heroicons-building-office", - }, { - label: "Export", - to: "/export", - icon: "i-heroicons-clipboard-document-list" - } - ] - }, - ] + children: settingsChildren + }] : []), + ].filter(Boolean) }) const accordionItems = computed(() => diff --git a/frontend/components/columnRenderings/description.vue b/frontend/components/columnRenderings/description.vue index 0647bd5..3247aa7 100644 --- a/frontend/components/columnRenderings/description.vue +++ b/frontend/components/columnRenderings/description.vue @@ -6,8 +6,20 @@ const props = defineProps({ default: {} } }) + +const descriptionText = computed(() => { + const description = props.row?.description + if (!description) return "" + if (typeof description === "string") return description + if (typeof description === "object") { + if (typeof description.text === "string" && description.text.trim().length) { + return description.text + } + } + return String(description) +}) diff --git a/frontend/pages/createDocument/edit/[[id]].vue b/frontend/pages/createDocument/edit/[[id]].vue index a0e3cba..2eb8d7c 100644 --- a/frontend/pages/createDocument/edit/[[id]].vue +++ b/frontend/pages/createDocument/edit/[[id]].vue @@ -2073,6 +2073,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = { > diff --git a/frontend/pages/settings/tenant.vue b/frontend/pages/settings/tenant.vue index 26a4b5d..6a31847 100644 --- a/frontend/pages/settings/tenant.vue +++ b/frontend/pages/settings/tenant.vue @@ -1,6 +1,108 @@ @@ -50,6 +156,8 @@ setupPage() label: 'Dokubox' },{ label: 'Rechnung & Kontakt' + },{ + label: 'Funktionen' } ]" > @@ -131,59 +239,11 @@ setupPage() class="mb-5" /> - - - - - - - - - - diff --git a/frontend/stores/data.js b/frontend/stores/data.js index adf4f52..171011b 100644 --- a/frontend/stores/data.js +++ b/frontend/stores/data.js @@ -1237,9 +1237,9 @@ export const useDataStore = defineStore('data', () => { selectOptionAttribute: "name", selectSearchAttributes: ['name'], },{ - key: "description", + key: "description.text", label: "Beschreibung", - inputType:"editor", + inputType:"textarea", component: description }, ], diff --git a/mobile/src/lib/api.ts b/mobile/src/lib/api.ts index 17629da..e61aa73 100644 --- a/mobile/src/lib/api.ts +++ b/mobile/src/lib/api.ts @@ -502,9 +502,22 @@ export async function createCustomerInventoryItem( } export async function fetchPlants(token: string, includeArchived = false): Promise { - const plants = await apiRequest('/api/resource/plants', { token }); - if (includeArchived) return plants || []; - return (plants || []).filter((plant) => !plant.archived); + const plants = await apiRequest>('/api/resource/plants', { token }); + const normalized = (plants || []).map((plant) => { + const legacyDescription = typeof plant.description === 'string' + ? plant.description + : (plant.description && typeof plant.description === 'object' && 'text' in (plant.description as Record) + ? String((plant.description as Record).text || '') + : null); + + return { + ...plant, + description: legacyDescription || null, + } as Plant; + }); + + if (includeArchived) return normalized; + return normalized.filter((plant) => !plant.archived); } export async function createPlant( @@ -518,7 +531,15 @@ export async function createPlant( return apiRequest('/api/resource/plants', { method: 'POST', token, - body: payload, + body: { + name: payload.name, + customer: payload.customer ?? null, + description: { + text: payload.description?.trim() || '', + html: '', + json: [], + }, + }, }); }