New CustomerInventory,
New Mitgliederverwaltung für Vereine New Bank Auto Complete
This commit is contained in:
73
backend/db/migrations/0012_shiny_customer_inventory.sql
Normal file
73
backend/db/migrations/0012_shiny_customer_inventory.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
CREATE TABLE "customerspaces" (
|
||||
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "customerspaces_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"tenant" bigint NOT NULL,
|
||||
"customer" bigint NOT NULL,
|
||||
"spaceNumber" text NOT NULL,
|
||||
"parentSpace" bigint,
|
||||
"infoData" jsonb DEFAULT '{"zip":"","city":"","streetNumber":""}'::jsonb NOT NULL,
|
||||
"description" text,
|
||||
"archived" boolean DEFAULT false NOT NULL,
|
||||
"updated_at" timestamp with time zone,
|
||||
"updated_by" uuid
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "customerinventoryitems" (
|
||||
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "customerinventoryitems_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"tenant" bigint NOT NULL,
|
||||
"customer" bigint NOT NULL,
|
||||
"customerspace" bigint,
|
||||
"customerInventoryId" text NOT NULL,
|
||||
"serialNumber" text,
|
||||
"quantity" bigint DEFAULT 0 NOT NULL,
|
||||
"manufacturer" text,
|
||||
"manufacturerNumber" text,
|
||||
"purchaseDate" date,
|
||||
"purchasePrice" double precision DEFAULT 0,
|
||||
"currentValue" double precision,
|
||||
"product" bigint,
|
||||
"archived" boolean DEFAULT false NOT NULL,
|
||||
"updated_at" timestamp with time zone,
|
||||
"updated_by" uuid
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_parentSpace_customerspaces_id_fk" FOREIGN KEY ("parentSpace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_customerspace_customerspaces_id_fk" FOREIGN KEY ("customerspace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_product_products_id_fk" FOREIGN KEY ("product") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "customerinventoryitems_tenant_customerInventoryId_idx" ON "customerinventoryitems" USING btree ("tenant","customerInventoryId");
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "historyitems" ADD COLUMN "customerspace" bigint;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "historyitems" ADD COLUMN "customerinventoryitem" bigint;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_customerspace_customerspaces_id_fk" FOREIGN KEY ("customerspace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_customerinventoryitem_customerinventoryitems_id_fk" FOREIGN KEY ("customerinventoryitem") REFERENCES "public"."customerinventoryitems"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "tenants" ALTER COLUMN "numberRanges" SET DEFAULT '{"vendors":{"prefix":"","suffix":"","nextNumber":10000},"customers":{"prefix":"","suffix":"","nextNumber":10000},"products":{"prefix":"AT-","suffix":"","nextNumber":1000},"quotes":{"prefix":"AN-","suffix":"","nextNumber":1000},"confirmationOrders":{"prefix":"AB-","suffix":"","nextNumber":1000},"invoices":{"prefix":"RE-","suffix":"","nextNumber":1000},"spaces":{"prefix":"LP-","suffix":"","nextNumber":1000},"customerspaces":{"prefix":"KLP-","suffix":"","nextNumber":1000},"inventoryitems":{"prefix":"IA-","suffix":"","nextNumber":1000},"customerinventoryitems":{"prefix":"KIA-","suffix":"","nextNumber":1000},"projects":{"prefix":"PRJ-","suffix":"","nextNumber":1000},"costcentres":{"prefix":"KST-","suffix":"","nextNumber":1000}}'::jsonb;
|
||||
--> statement-breakpoint
|
||||
UPDATE "tenants"
|
||||
SET "numberRanges" = COALESCE("numberRanges", '{}'::jsonb) || jsonb_build_object(
|
||||
'customerspaces', COALESCE("numberRanges"->'customerspaces', '{"prefix":"KLP-","suffix":"","nextNumber":1000}'::jsonb),
|
||||
'customerinventoryitems', COALESCE("numberRanges"->'customerinventoryitems', '{"prefix":"KIA-","suffix":"","nextNumber":1000}'::jsonb)
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "customerinventoryitems" ADD COLUMN "vendor" bigint;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;
|
||||
20
backend/db/migrations/0014_smart_memberrelations.sql
Normal file
20
backend/db/migrations/0014_smart_memberrelations.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE "memberrelations" (
|
||||
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "memberrelations_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,
|
||||
"type" text NOT NULL,
|
||||
"billingInterval" text NOT NULL,
|
||||
"billingAmount" double precision DEFAULT 0 NOT NULL,
|
||||
"archived" boolean DEFAULT false NOT NULL,
|
||||
"updated_at" timestamp with time zone,
|
||||
"updated_by" uuid
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customers" ADD COLUMN "memberrelation" bigint;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "memberrelations" ADD CONSTRAINT "memberrelations_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "memberrelations" ADD CONSTRAINT "memberrelations_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customers" ADD CONSTRAINT "customers_memberrelation_memberrelations_id_fk" FOREIGN KEY ("memberrelation") REFERENCES "public"."memberrelations"("id") ON DELETE no action ON UPDATE no action;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "historyitems" ADD COLUMN "memberrelation" bigint;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_memberrelation_memberrelations_id_fk" FOREIGN KEY ("memberrelation") REFERENCES "public"."memberrelations"("id") ON DELETE no action ON UPDATE no action;
|
||||
|
||||
@@ -85,6 +85,34 @@
|
||||
"when": 1773000300000,
|
||||
"tag": "0011_mighty_member_bankaccounts",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "7",
|
||||
"when": 1773000400000,
|
||||
"tag": "0012_shiny_customer_inventory",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "7",
|
||||
"when": 1773000500000,
|
||||
"tag": "0013_brisk_customer_inventory_vendor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1773000600000,
|
||||
"tag": "0014_smart_memberrelations",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "7",
|
||||
"when": 1773000700000,
|
||||
"tag": "0015_wise_memberrelation_history",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
66
backend/db/schema/customerinventoryitems.ts
Normal file
66
backend/db/schema/customerinventoryitems.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
doublePrecision,
|
||||
uuid,
|
||||
date,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { customerspaces } from "./customerspaces"
|
||||
import { products } from "./products"
|
||||
import { vendors } from "./vendors"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const customerinventoryitems = pgTable("customerinventoryitems", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
customer: bigint("customer", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => customers.id),
|
||||
|
||||
customerspace: bigint("customerspace", { mode: "number" }).references(
|
||||
() => customerspaces.id
|
||||
),
|
||||
|
||||
customerInventoryId: text("customerInventoryId").notNull(),
|
||||
serialNumber: text("serialNumber"),
|
||||
|
||||
quantity: bigint("quantity", { mode: "number" }).notNull().default(0),
|
||||
|
||||
manufacturer: text("manufacturer"),
|
||||
manufacturerNumber: text("manufacturerNumber"),
|
||||
|
||||
purchaseDate: date("purchaseDate"),
|
||||
purchasePrice: doublePrecision("purchasePrice").default(0),
|
||||
currentValue: doublePrecision("currentValue"),
|
||||
|
||||
product: bigint("product", { mode: "number" }).references(() => products.id),
|
||||
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type CustomerInventoryItem = typeof customerinventoryitems.$inferSelect
|
||||
export type NewCustomerInventoryItem = typeof customerinventoryitems.$inferInsert
|
||||
54
backend/db/schema/customerspaces.ts
Normal file
54
backend/db/schema/customerspaces.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const customerspaces = pgTable("customerspaces", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
customer: bigint("customer", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => customers.id),
|
||||
|
||||
space_number: text("spaceNumber").notNull(),
|
||||
|
||||
parentSpace: bigint("parentSpace", { mode: "number" }).references(
|
||||
() => customerspaces.id
|
||||
),
|
||||
|
||||
info_data: jsonb("infoData")
|
||||
.notNull()
|
||||
.default({ zip: "", city: "", streetNumber: "" }),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type CustomerSpace = typeof customerspaces.$inferSelect
|
||||
export type NewCustomerSpace = typeof customerspaces.$inferInsert
|
||||
@@ -20,6 +20,8 @@ import { tasks } from "./tasks"
|
||||
import { vehicles } from "./vehicles"
|
||||
import { bankstatements } from "./bankstatements"
|
||||
import { spaces } from "./spaces"
|
||||
import { customerspaces } from "./customerspaces"
|
||||
import { customerinventoryitems } from "./customerinventoryitems"
|
||||
import { costcentres } from "./costcentres"
|
||||
import { ownaccounts } from "./ownaccounts"
|
||||
import { createddocuments } from "./createddocuments"
|
||||
@@ -32,6 +34,7 @@ import { events } from "./events"
|
||||
import { inventoryitemgroups } from "./inventoryitemgroups"
|
||||
import { authUsers } from "./auth_users"
|
||||
import {files} from "./files";
|
||||
import { memberrelations } from "./memberrelations";
|
||||
|
||||
export const historyitems = pgTable("historyitems", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
@@ -99,6 +102,12 @@ export const historyitems = pgTable("historyitems", {
|
||||
|
||||
space: bigint("space", { mode: "number" }).references(() => spaces.id),
|
||||
|
||||
customerspace: bigint("customerspace", { mode: "number" }).references(() => customerspaces.id),
|
||||
|
||||
customerinventoryitem: bigint("customerinventoryitem", { mode: "number" }).references(() => customerinventoryitems.id),
|
||||
|
||||
memberrelation: bigint("memberrelation", { mode: "number" }).references(() => memberrelations.id),
|
||||
|
||||
config: jsonb("config"),
|
||||
|
||||
projecttype: bigint("projecttype", { mode: "number" }).references(
|
||||
|
||||
@@ -19,6 +19,8 @@ export * from "./countrys"
|
||||
export * from "./createddocuments"
|
||||
export * from "./createdletters"
|
||||
export * from "./customers"
|
||||
export * from "./customerspaces"
|
||||
export * from "./customerinventoryitems"
|
||||
export * from "./devices"
|
||||
export * from "./documentboxes"
|
||||
export * from "./enums"
|
||||
@@ -44,6 +46,7 @@ export * from "./incominginvoices"
|
||||
export * from "./inventoryitemgroups"
|
||||
export * from "./inventoryitems"
|
||||
export * from "./letterheads"
|
||||
export * from "./memberrelations"
|
||||
export * from "./movements"
|
||||
export * from "./m2m_api_keys"
|
||||
export * from "./notifications_event_types"
|
||||
|
||||
39
backend/db/schema/memberrelations.ts
Normal file
39
backend/db/schema/memberrelations.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
uuid,
|
||||
doublePrecision,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const memberrelations = pgTable("memberrelations", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
type: text("type").notNull(),
|
||||
billingInterval: text("billingInterval").notNull(),
|
||||
billingAmount: doublePrecision("billingAmount").notNull().default(0),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type MemberRelation = typeof memberrelations.$inferSelect
|
||||
export type NewMemberRelation = typeof memberrelations.$inferInsert
|
||||
|
||||
@@ -88,7 +88,9 @@ export const tenants = pgTable(
|
||||
confirmationOrders: { prefix: "AB-", suffix: "", nextNumber: 1000 },
|
||||
invoices: { prefix: "RE-", suffix: "", nextNumber: 1000 },
|
||||
spaces: { prefix: "LP-", suffix: "", nextNumber: 1000 },
|
||||
customerspaces: { prefix: "KLP-", suffix: "", nextNumber: 1000 },
|
||||
inventoryitems: { prefix: "IA-", suffix: "", nextNumber: 1000 },
|
||||
customerinventoryitems: { prefix: "KIA-", suffix: "", nextNumber: 1000 },
|
||||
projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 },
|
||||
costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 },
|
||||
}),
|
||||
|
||||
95
backend/scripts/generate-de-bank-codes.ts
Normal file
95
backend/scripts/generate-de-bank-codes.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import https from "node:https"
|
||||
|
||||
const DEFAULT_SOURCE_URL =
|
||||
"https://www.bundesbank.de/resource/blob/602632/bec25ca5df1eb62fefadd8325dafe67c/472B63F073F071307366337C94F8C870/blz-aktuell-txt-data.txt"
|
||||
|
||||
const OUTPUT_NAME_FILE = path.resolve("src/utils/deBankCodes.ts")
|
||||
const OUTPUT_BIC_FILE = path.resolve("src/utils/deBankBics.ts")
|
||||
|
||||
function fetchBuffer(url: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(url, (res) => {
|
||||
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
return resolve(fetchBuffer(res.headers.location))
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
return reject(new Error(`Download failed with status ${res.statusCode}`))
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
|
||||
res.on("end", () => resolve(Buffer.concat(chunks)))
|
||||
res.on("error", reject)
|
||||
})
|
||||
.on("error", reject)
|
||||
})
|
||||
}
|
||||
|
||||
function escapeTsString(value: string) {
|
||||
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const source = process.env.BLZ_SOURCE_URL || DEFAULT_SOURCE_URL
|
||||
const sourceFile = process.env.BLZ_SOURCE_FILE
|
||||
let raw: Buffer
|
||||
|
||||
if (sourceFile) {
|
||||
console.log(`Reading BLZ source file: ${sourceFile}`)
|
||||
raw = await fs.readFile(sourceFile)
|
||||
} else {
|
||||
console.log(`Downloading BLZ source: ${source}`)
|
||||
raw = await fetchBuffer(source)
|
||||
}
|
||||
const content = raw.toString("latin1")
|
||||
|
||||
const lines = content.split(/\r?\n/)
|
||||
const nameMap = new Map<string, string>()
|
||||
const bicMap = new Map<string, string>()
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line || line.length < 150) continue
|
||||
const blz = line.slice(0, 8).trim()
|
||||
const name = line.slice(9, 67).trim()
|
||||
const bic = line.slice(139, 150).trim()
|
||||
|
||||
if (!/^\d{8}$/.test(blz) || !name) continue
|
||||
if (!nameMap.has(blz)) nameMap.set(blz, name)
|
||||
if (bic && !bicMap.has(blz)) bicMap.set(blz, bic)
|
||||
}
|
||||
|
||||
const sortedNames = [...nameMap.entries()].sort(([a], [b]) => a.localeCompare(b))
|
||||
const sortedBics = [...bicMap.entries()].sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
const nameOutputLines = [
|
||||
"// Lokale Bankleitzahl-zu-Institut Zuordnung (DE).",
|
||||
"// Quelle: Deutsche Bundesbank, BLZ-Datei (vollstaendig).",
|
||||
"export const DE_BANK_CODE_TO_NAME: Record<string, string> = {",
|
||||
...sortedNames.map(([blz, name]) => ` "${blz}": "${escapeTsString(name)}",`),
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
|
||||
const bicOutputLines = [
|
||||
"// Lokale Bankleitzahl-zu-BIC Zuordnung (DE).",
|
||||
"// Quelle: Deutsche Bundesbank, BLZ-Datei (vollstaendig).",
|
||||
"export const DE_BANK_CODE_TO_BIC: Record<string, string> = {",
|
||||
...sortedBics.map(([blz, bic]) => ` "${blz}": "${escapeTsString(bic)}",`),
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
|
||||
await fs.writeFile(OUTPUT_NAME_FILE, nameOutputLines.join("\n"), "utf8")
|
||||
await fs.writeFile(OUTPUT_BIC_FILE, bicOutputLines.join("\n"), "utf8")
|
||||
console.log(`Wrote ${sortedNames.length} bank names to ${OUTPUT_NAME_FILE}`)
|
||||
console.log(`Wrote ${sortedBics.length} bank BICs to ${OUTPUT_BIC_FILE}`)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -6,6 +6,7 @@ import { secrets } from "../utils/secrets"
|
||||
import { insertHistoryItem } from "../utils/history"
|
||||
import { decrypt, encrypt } from "../utils/crypt"
|
||||
import { DE_BANK_CODE_TO_NAME } from "../utils/deBankCodes"
|
||||
import { DE_BANK_CODE_TO_BIC } from "../utils/deBankBics"
|
||||
|
||||
import {
|
||||
bankrequisitions,
|
||||
@@ -116,16 +117,20 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
return remainder === 1
|
||||
}
|
||||
|
||||
const resolveBankInstituteFromIbanLocal = (iban: string) => {
|
||||
const resolveGermanBankDataFromIbanLocal = (iban: string) => {
|
||||
const normalized = normalizeIban(iban)
|
||||
if (!isValidIbanLocal(normalized)) return null
|
||||
|
||||
// Für DE-IBANs kann die BLZ aus Position 5-12 lokal gelesen werden.
|
||||
if (normalized.startsWith("DE") && normalized.length === 22) {
|
||||
const bankCode = normalized.slice(4, 12)
|
||||
const bankName = DE_BANK_CODE_TO_NAME[bankCode]
|
||||
if (bankName) return bankName
|
||||
return `Unbekannt (BLZ ${bankCode})`
|
||||
const bankName = DE_BANK_CODE_TO_NAME[bankCode] || `Unbekannt (BLZ ${bankCode})`
|
||||
const bic = DE_BANK_CODE_TO_BIC[bankCode] || null
|
||||
return {
|
||||
bankName,
|
||||
bic,
|
||||
bankCode,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -139,13 +144,14 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
const normalizedIban = normalizeIban(iban)
|
||||
if (!normalizedIban) return null
|
||||
|
||||
const bankInstitute = resolveBankInstituteFromIbanLocal(normalizedIban)
|
||||
const bankData = resolveGermanBankDataFromIbanLocal(normalizedIban)
|
||||
|
||||
const allAccounts = await server.db
|
||||
.select({
|
||||
id: entitybankaccounts.id,
|
||||
ibanEncrypted: entitybankaccounts.ibanEncrypted,
|
||||
bankNameEncrypted: entitybankaccounts.bankNameEncrypted,
|
||||
bicEncrypted: entitybankaccounts.bicEncrypted,
|
||||
})
|
||||
.from(entitybankaccounts)
|
||||
.where(eq(entitybankaccounts.tenant, tenantId))
|
||||
@@ -161,19 +167,28 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
if (existing?.id) {
|
||||
if (bankInstitute) {
|
||||
if (bankData) {
|
||||
let currentBankName = ""
|
||||
let currentBic = ""
|
||||
try {
|
||||
currentBankName = String(decrypt(existing.bankNameEncrypted as any) || "").trim()
|
||||
} catch {
|
||||
currentBankName = ""
|
||||
}
|
||||
try {
|
||||
currentBic = String(decrypt((existing as any).bicEncrypted as any) || "").trim()
|
||||
} catch {
|
||||
currentBic = ""
|
||||
}
|
||||
|
||||
if (currentBankName !== bankInstitute) {
|
||||
const nextBankName = bankData?.bankName || "Unbekannt"
|
||||
const nextBic = bankData?.bic || "UNBEKANNT"
|
||||
if (currentBankName !== nextBankName || currentBic !== nextBic) {
|
||||
await server.db
|
||||
.update(entitybankaccounts)
|
||||
.set({
|
||||
bankNameEncrypted: encrypt(bankInstitute),
|
||||
bankNameEncrypted: encrypt(nextBankName),
|
||||
bicEncrypted: encrypt(nextBic),
|
||||
updatedAt: new Date(),
|
||||
updatedBy: userId,
|
||||
})
|
||||
@@ -189,8 +204,8 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
.values({
|
||||
tenant: tenantId,
|
||||
ibanEncrypted: encrypt(normalizedIban),
|
||||
bicEncrypted: encrypt("UNBEKANNT"),
|
||||
bankNameEncrypted: encrypt(bankInstitute || "Unbekannt"),
|
||||
bicEncrypted: encrypt(bankData?.bic || "UNBEKANNT"),
|
||||
bankNameEncrypted: encrypt(bankData?.bankName || "Unbekannt"),
|
||||
description: "Automatisch aus Bankbuchung übernommen",
|
||||
updatedAt: new Date(),
|
||||
updatedBy: userId,
|
||||
@@ -200,6 +215,30 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
return created?.id ? Number(created.id) : null
|
||||
}
|
||||
|
||||
server.get("/banking/iban/:iban", async (req, reply) => {
|
||||
try {
|
||||
const { iban } = req.params as { iban: string }
|
||||
const normalized = normalizeIban(iban)
|
||||
if (!normalized) {
|
||||
return reply.code(400).send({ error: "IBAN missing" })
|
||||
}
|
||||
|
||||
const valid = isValidIbanLocal(normalized)
|
||||
const bankData = resolveGermanBankDataFromIbanLocal(normalized)
|
||||
|
||||
return reply.send({
|
||||
iban: normalized,
|
||||
valid,
|
||||
bic: bankData?.bic || null,
|
||||
bankName: bankData?.bankName || null,
|
||||
bankCode: bankData?.bankCode || null,
|
||||
})
|
||||
} catch (err) {
|
||||
server.log.error(err)
|
||||
return reply.code(500).send({ error: "Failed to resolve IBAN data" })
|
||||
}
|
||||
})
|
||||
|
||||
const assignIbanFromStatementToCustomer = async (tenantId: number, userId: string, statementId: number, createdDocumentId?: number) => {
|
||||
if (!createdDocumentId) return
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import {createInvoicePDF, createTimeSheetPDF} from "../utils/pdf";
|
||||
//import {encodeBase64ToNiimbot, generateLabel, useNextNumberRangeNumber} from "../utils/functions";
|
||||
import {encodeBase64ToNiimbot, generateLabel, useNextNumberRangeNumber} from "../utils/functions";
|
||||
import dayjs from "dayjs";
|
||||
//import { ready as zplReady } from 'zpl-renderer-js'
|
||||
//import { renderZPL } from "zpl-image";
|
||||
@@ -15,7 +15,6 @@ import timezone from "dayjs/plugin/timezone.js";
|
||||
import {generateTimesEvaluation} from "../modules/time/evaluation.service";
|
||||
import {citys} from "../../db/schema";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {useNextNumberRangeNumber} from "../utils/functions";
|
||||
import {executeManualGeneration, finishManualGeneration} from "../modules/serialexecution.service";
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(isoWeek)
|
||||
@@ -177,44 +176,20 @@ export default async function functionRoutes(server: FastifyInstance) {
|
||||
await server.services.dokuboxSync.run()
|
||||
})
|
||||
|
||||
|
||||
/*server.post('/print/zpl/preview', async (req, reply) => {
|
||||
const { zpl, widthMm = 50, heightMm = 30, dpmm = 8, asBase64 = false } = req.body as {zpl:string,widthMm:number,heightMm:number,dpmm:number,asBase64:string}
|
||||
|
||||
console.log(widthMm,heightMm,dpmm)
|
||||
|
||||
if (!zpl) {
|
||||
return reply.code(400).send({ error: 'Missing ZPL string' })
|
||||
}
|
||||
|
||||
try {
|
||||
// 1️⃣ Renderer initialisieren
|
||||
const { api } = await zplReady
|
||||
|
||||
// 2️⃣ Rendern (liefert base64-encoded PNG)
|
||||
const base64Png = await api.zplToBase64Async(zpl, widthMm, heightMm, dpmm)
|
||||
|
||||
return await encodeBase64ToNiimbot(base64Png, 'top')
|
||||
} catch (err) {
|
||||
console.error('[ZPL Preview Error]', err)
|
||||
return reply.code(500).send({ error: err.message || 'Failed to render ZPL' })
|
||||
}
|
||||
})
|
||||
|
||||
server.post('/print/label', async (req, reply) => {
|
||||
const { context, width=584, heigth=354 } = req.body as {context:any,width:number,heigth:number}
|
||||
const { context, width = 584, height = 354 } = req.body as {context:any,width:number,height:number}
|
||||
|
||||
try {
|
||||
const base64 = await generateLabel(context,width,heigth)
|
||||
const base64 = await generateLabel(context,width,height)
|
||||
|
||||
return {
|
||||
encoded: await encodeBase64ToNiimbot(base64, 'top'),
|
||||
base64: base64
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ZPL Preview Error]', err)
|
||||
return reply.code(500).send({ error: err.message || 'Failed to render ZPL' })
|
||||
console.error('[Label Render Error]', err)
|
||||
return reply.code(500).send({ error: err.message || 'Failed to render label' })
|
||||
}
|
||||
})*/
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { authProfiles, historyitems } from "../../db/schema";
|
||||
|
||||
const columnMap: Record<string, any> = {
|
||||
customers: historyitems.customer,
|
||||
members: historyitems.customer,
|
||||
vendors: historyitems.vendor,
|
||||
projects: historyitems.project,
|
||||
plants: historyitems.plant,
|
||||
@@ -22,10 +23,14 @@ const columnMap: Record<string, any> = {
|
||||
documentboxes: historyitems.documentbox,
|
||||
hourrates: historyitems.hourrate,
|
||||
services: historyitems.service,
|
||||
customerspaces: historyitems.customerspace,
|
||||
customerinventoryitems: historyitems.customerinventoryitem,
|
||||
memberrelations: historyitems.memberrelation,
|
||||
};
|
||||
|
||||
const insertFieldMap: Record<string, string> = {
|
||||
customers: "customer",
|
||||
members: "customer",
|
||||
vendors: "vendor",
|
||||
projects: "project",
|
||||
plants: "plant",
|
||||
@@ -43,6 +48,9 @@ const insertFieldMap: Record<string, string> = {
|
||||
documentboxes: "documentbox",
|
||||
hourrates: "hourrate",
|
||||
services: "service",
|
||||
customerspaces: "customerspace",
|
||||
customerinventoryitems: "customerinventoryitem",
|
||||
memberrelations: "memberrelation",
|
||||
}
|
||||
|
||||
const parseId = (value: string) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
import { resourceConfig } from "../../utils/resource.config";
|
||||
import { useNextNumberRangeNumber } from "../../utils/functions";
|
||||
import { insertHistoryItem } from "../../utils/history";
|
||||
import { getHistoryEntityLabel, insertHistoryItem } from "../../utils/history";
|
||||
import { diffObjects } from "../../utils/diff";
|
||||
import { recalculateServicePricesForTenant } from "../../modules/service-price-recalculation.service";
|
||||
import { decrypt, encrypt } from "../../utils/crypt";
|
||||
@@ -67,7 +67,8 @@ function getUserVisibleChanges(oldRecord: Record<string, any>, updated: Record<s
|
||||
}
|
||||
|
||||
function buildFieldUpdateHistoryText(resource: string, label: string, oldValue: any, newValue: any) {
|
||||
return `${resource}: ${label} geändert von "${formatDiffValue(oldValue)}" zu "${formatDiffValue(newValue)}"`
|
||||
const resourceLabel = getHistoryEntityLabel(resource)
|
||||
return `${resourceLabel}: ${label} geändert von "${formatDiffValue(oldValue)}" zu "${formatDiffValue(newValue)}"`
|
||||
}
|
||||
|
||||
function applyResourceWhereFilters(resource: string, table: any, whereCond: any) {
|
||||
@@ -525,6 +526,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
if (created) {
|
||||
try {
|
||||
const resourceLabel = getHistoryEntityLabel(resource)
|
||||
await insertHistoryItem(server, {
|
||||
tenant_id: req.user.tenant_id,
|
||||
created_by: req.user?.user_id || null,
|
||||
@@ -533,7 +535,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
action: "created",
|
||||
oldVal: null,
|
||||
newVal: created,
|
||||
text: `Neuer Eintrag in ${resource} erstellt`,
|
||||
text: `Neuer Eintrag in ${resourceLabel} erstellt`,
|
||||
})
|
||||
} catch (historyError) {
|
||||
server.log.warn({ err: historyError, resource }, "Failed to write create history entry")
|
||||
@@ -608,6 +610,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
if (updated) {
|
||||
try {
|
||||
const resourceLabel = getHistoryEntityLabel(resource)
|
||||
const changes = oldRecord ? getUserVisibleChanges(oldRecord, updated) : []
|
||||
if (!changes.length) {
|
||||
await insertHistoryItem(server, {
|
||||
@@ -618,7 +621,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
action: "updated",
|
||||
oldVal: oldRecord || null,
|
||||
newVal: updated,
|
||||
text: `Eintrag in ${resource} geändert`,
|
||||
text: `Eintrag in ${resourceLabel} geändert`,
|
||||
})
|
||||
} else {
|
||||
for (const change of changes) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
plants,
|
||||
products,
|
||||
inventoryitems,
|
||||
customerinventoryitems,
|
||||
customerspaces,
|
||||
// NEU HINZUGEFÜGT (Basierend auf deinem DataStore)
|
||||
tasks,
|
||||
contacts,
|
||||
@@ -34,6 +36,8 @@ const ENTITY_CONFIG: Record<string, { table: any, labelField: any, rootLabel: st
|
||||
'plants': { table: plants, labelField: plants.name, rootLabel: 'Objekte', idField: 'id' },
|
||||
'products': { table: products, labelField: products.name, rootLabel: 'Artikel', idField: 'id' },
|
||||
'inventoryitems': { table: inventoryitems, labelField: inventoryitems.name, rootLabel: 'Inventarartikel', idField: 'id' },
|
||||
'customerinventoryitems': { table: customerinventoryitems, labelField: customerinventoryitems.name, rootLabel: 'Kundeninventar', idField: 'id' },
|
||||
'customerspaces': { table: customerspaces, labelField: customerspaces.name, rootLabel: 'Kundenlagerplätze', idField: 'id' },
|
||||
|
||||
// --- NEU BASIEREND AUF DATASTORE ---
|
||||
'tasks': { table: tasks, labelField: tasks.name, rootLabel: 'Aufgaben', idField: 'id' },
|
||||
|
||||
3512
backend/src/utils/deBankBics.ts
Normal file
3512
backend/src/utils/deBankBics.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -698,7 +698,7 @@ export const DE_BANK_CODE_TO_NAME: Record<string, string> = {
|
||||
"28022822": "Oldenburgische Landesbank AG",
|
||||
"28023224": "Oldenburgische Landesbank AG",
|
||||
"28023325": "Oldenburgische Landesbank AG",
|
||||
"28030300": "Oldenburgische Landesbank AG (vormals W. Fortmann & Söhne",
|
||||
"28030300": "Oldenburgische Landesbank AG (vormals W. Fortmann & Söhne)",
|
||||
"28040046": "Commerzbank",
|
||||
"28042865": "Commerzbank",
|
||||
"28050100": "Landessparkasse zu Oldenburg",
|
||||
@@ -1186,7 +1186,7 @@ export const DE_BANK_CODE_TO_NAME: Record<string, string> = {
|
||||
"39570061": "Deutsche Bank",
|
||||
"39580041": "Commerzbank vormals Dresdner Bank",
|
||||
"40022000": "NRW.BANK",
|
||||
"40030000": "Münsterländische Bank Thie,Zndl. d.VR-Bank Westmünst.-a",
|
||||
"40030000": "Münsterländische Bank Thie,Zndl. d.VR-Bank Westmünst.-alt-",
|
||||
"40040028": "Commerzbank",
|
||||
"40050000": "Landesbank Hessen-Thüringen Girozentrale NL. Düsseldorf",
|
||||
"40050150": "Sparkasse Münsterland Ost",
|
||||
@@ -3214,7 +3214,7 @@ export const DE_BANK_CODE_TO_NAME: Record<string, string> = {
|
||||
"76260451": "Raiffeisen-Volksbank Fürth -alt-",
|
||||
"76320072": "UniCredit Bank - HypoVereinsbank",
|
||||
"76340061": "Commerzbank Erlangen",
|
||||
"76350000": "Stadt- u. Kreissparkasse Erlangen Höchstadt Herzogenaurac",
|
||||
"76350000": "Stadt- u. Kreissparkasse Erlangen Höchstadt Herzogenaurach",
|
||||
"76351040": "Sparkasse Forchheim",
|
||||
"76351560": "Kreissparkasse Höchstadt",
|
||||
"76360033": "VR-Bank Erlangen-Höchstadt-Herzogenaurach -alt-",
|
||||
|
||||
@@ -236,8 +236,11 @@ export const diffTranslations: Record<
|
||||
sellingPriceComposed: { label: "Verkaufspreis Zusammensetzung" },
|
||||
purchaseDate: { label: "Kaufdatum" },
|
||||
serialNumber: { label: "Seriennummer" },
|
||||
customerInventoryId: { label: "Kundeninventar-ID" },
|
||||
customerinventoryitems: { label: "Kundeninventar" },
|
||||
usePlanning: { label: "In Plantafel verwenden" },
|
||||
currentSpace: { label: "Lagerplatz" },
|
||||
customerspace: { label: "Kundenlagerplatz" },
|
||||
|
||||
customer: {
|
||||
label: "Kunde",
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import {FastifyInstance} from "fastify";
|
||||
// import { PNG } from 'pngjs'
|
||||
// import { ready as zplReady } from 'zpl-renderer-js'
|
||||
// import { Utils } from '@mmote/niimbluelib'
|
||||
// import { createCanvas } from 'canvas'
|
||||
// import bwipjs from 'bwip-js'
|
||||
// import Sharp from 'sharp'
|
||||
// import fs from 'fs'
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { PNG } from "pngjs"
|
||||
import { Utils } from "@mmote/niimbluelib"
|
||||
import bwipjs from "bwip-js"
|
||||
import Sharp from "sharp"
|
||||
|
||||
import { tenants } from "../../db/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
@@ -15,7 +12,6 @@ export const useNextNumberRangeNumber = async (
|
||||
tenantId: number,
|
||||
numberRange: string
|
||||
) => {
|
||||
// 1️⃣ Tenant laden
|
||||
const [tenant] = await server.db
|
||||
.select()
|
||||
.from(tenants)
|
||||
@@ -33,23 +29,20 @@ export const useNextNumberRangeNumber = async (
|
||||
|
||||
const current = numberRanges[numberRange]
|
||||
|
||||
// 2️⃣ Used Number generieren
|
||||
const usedNumber =
|
||||
(current.prefix || "") +
|
||||
current.nextNumber +
|
||||
(current.suffix || "")
|
||||
|
||||
// 3️⃣ nextNumber erhöhen
|
||||
const updatedRanges = {
|
||||
// @ts-ignore
|
||||
...numberRanges,
|
||||
[numberRange]: {
|
||||
...current,
|
||||
nextNumber: current.nextNumber + 1
|
||||
}
|
||||
nextNumber: current.nextNumber + 1,
|
||||
},
|
||||
}
|
||||
|
||||
// 4️⃣ Tenant aktualisieren
|
||||
await server.db
|
||||
.update(tenants)
|
||||
.set({ numberRanges: updatedRanges })
|
||||
@@ -58,24 +51,17 @@ export const useNextNumberRangeNumber = async (
|
||||
return { usedNumber }
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') {
|
||||
// 1️⃣ PNG dekodieren
|
||||
const buffer = Buffer.from(base64Png, 'base64')
|
||||
const png = PNG.sync.read(buffer) // liefert {width, height, data: Uint8Array(RGBA)}
|
||||
export async function encodeBase64ToNiimbot(base64Png: string, printDirection: "top" | "left" = "top") {
|
||||
const buffer = Buffer.from(base64Png, "base64")
|
||||
const png = PNG.sync.read(buffer)
|
||||
|
||||
const { width, height, data } = png
|
||||
console.log(width, height, data)
|
||||
const cols = printDirection === 'left' ? height : width
|
||||
const rows = printDirection === 'left' ? width : height
|
||||
const rowsData = []
|
||||
const cols = printDirection === "left" ? height : width
|
||||
const rows = printDirection === "left" ? width : height
|
||||
const rowsData: any[] = []
|
||||
|
||||
console.log(cols)
|
||||
if (cols % 8 !== 0) throw new Error("Column count must be multiple of 8")
|
||||
|
||||
if (cols % 8 !== 0) throw new Error('Column count must be multiple of 8')
|
||||
|
||||
// 2️⃣ Zeilenweise durchgehen und Bits bilden
|
||||
for (let row = 0; row < rows; row++) {
|
||||
let isVoid = true
|
||||
let blackPixelsCount = 0
|
||||
@@ -84,8 +70,8 @@ export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') {
|
||||
for (let colOct = 0; colOct < cols / 8; colOct++) {
|
||||
let pixelsOctet = 0
|
||||
for (let colBit = 0; colBit < 8; colBit++) {
|
||||
const x = printDirection === 'left' ? row : colOct * 8 + colBit
|
||||
const y = printDirection === 'left' ? height - 1 - (colOct * 8 + colBit) : row
|
||||
const x = printDirection === "left" ? row : colOct * 8 + colBit
|
||||
const y = printDirection === "left" ? height - 1 - (colOct * 8 + colBit) : row
|
||||
const idx = (y * width + x) * 4
|
||||
const lum = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]
|
||||
const isBlack = lum < 128
|
||||
@@ -99,7 +85,7 @@ export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') {
|
||||
}
|
||||
|
||||
const newPart = {
|
||||
dataType: isVoid ? 'void' : 'pixels',
|
||||
dataType: isVoid ? "void" : "pixels",
|
||||
rowNumber: row,
|
||||
repeat: 1,
|
||||
rowData: isVoid ? undefined : rowData,
|
||||
@@ -111,14 +97,15 @@ export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') {
|
||||
} else {
|
||||
const last = rowsData[rowsData.length - 1]
|
||||
let same = newPart.dataType === last.dataType
|
||||
if (same && newPart.dataType === 'pixels') {
|
||||
if (same && newPart.dataType === "pixels") {
|
||||
same = Utils.u8ArraysEqual(newPart.rowData, last.rowData)
|
||||
}
|
||||
if (same) last.repeat++
|
||||
else rowsData.push(newPart)
|
||||
|
||||
if (row % 200 === 199) {
|
||||
rowsData.push({
|
||||
dataType: 'check',
|
||||
dataType: "check",
|
||||
rowNumber: row,
|
||||
repeat: 0,
|
||||
rowData: undefined,
|
||||
@@ -131,44 +118,69 @@ export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') {
|
||||
return { cols, rows, rowsData }
|
||||
}
|
||||
|
||||
export async function generateLabel(context,width,height) {
|
||||
// Canvas für Hintergrund & Text
|
||||
const canvas = createCanvas(width, height)
|
||||
const ctx = canvas.getContext('2d')
|
||||
function escapeXml(value: string) {
|
||||
return String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
// Hintergrund weiß
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
export async function generateLabel(context: any = {}, width = 584, height = 354) {
|
||||
const normalizedWidth = Math.ceil(Number(width) / 8) * 8
|
||||
const normalizedHeight = Math.max(1, Number(height) || 203)
|
||||
|
||||
// Überschrift
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.font = '32px Arial'
|
||||
ctx.fillText(context.text, 20, 40)
|
||||
const idFont = Math.max(24, Math.round(normalizedHeight * 0.125))
|
||||
const nameFont = Math.max(17, Math.round(normalizedHeight * 0.078))
|
||||
const customerFont = Math.max(14, Math.round(normalizedHeight * 0.06))
|
||||
const serialFont = Math.max(12, Math.round(normalizedHeight * 0.052))
|
||||
|
||||
const labelId = context.customerInventoryId || context.datamatrix || context.id || "N/A"
|
||||
const labelName = context.name || context.text || "Kundeninventarartikel"
|
||||
const customerName = context.customerName || ""
|
||||
const serial = context.serialNumber ? `SN: ${context.serialNumber}` : ""
|
||||
const nameLine1 = String(labelName).slice(0, 30)
|
||||
const nameLine2 = String(labelName).slice(30, 60)
|
||||
|
||||
// 3) DataMatrix
|
||||
const dataMatrixPng = await bwipjs.toBuffer({
|
||||
bcid: 'datamatrix',
|
||||
text: context.datamatrix,
|
||||
scale: 6,
|
||||
bcid: "datamatrix",
|
||||
text: String(labelId),
|
||||
scale: normalizedWidth >= 560 ? 7 : 5,
|
||||
includetext: false,
|
||||
})
|
||||
const dataMatrixMeta = await Sharp(dataMatrixPng).metadata()
|
||||
const dataMatrixWidth = dataMatrixMeta.width || 0
|
||||
const dataMatrixHeight = dataMatrixMeta.height || 0
|
||||
const dmLeft = Math.max(8, normalizedWidth - dataMatrixWidth - 28)
|
||||
const dmTop = Math.max(8, Math.floor((normalizedHeight - dataMatrixHeight) / 2))
|
||||
const textMaxWidth = Math.max(120, dmLeft - 20)
|
||||
|
||||
// Basisbild aus Canvas
|
||||
const base = await Sharp(canvas.toBuffer())
|
||||
.png()
|
||||
.toBuffer()
|
||||
const textSvg = `
|
||||
<svg width="${normalizedWidth}" height="${normalizedHeight}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="white"/>
|
||||
<text x="12" y="${Math.round(normalizedHeight * 0.15)}" font-size="${idFont}" font-family="Arial, Helvetica, sans-serif" font-weight="700" fill="black">${escapeXml(String(labelId).slice(0, 26))}</text>
|
||||
<text x="12" y="${Math.round(normalizedHeight * 0.29)}" font-size="${nameFont}" font-family="Arial, Helvetica, sans-serif" fill="black">${escapeXml(nameLine1)}</text>
|
||||
<text x="12" y="${Math.round(normalizedHeight * 0.37)}" font-size="${nameFont}" font-family="Arial, Helvetica, sans-serif" fill="black">${escapeXml(nameLine2)}</text>
|
||||
<text x="12" y="${Math.round(normalizedHeight * 0.49)}" font-size="${customerFont}" font-family="Arial, Helvetica, sans-serif" fill="black">${escapeXml(String(customerName).slice(0, 40))}</text>
|
||||
<text x="12" y="${Math.round(normalizedHeight * 0.58)}" font-size="${serialFont}" font-family="Arial, Helvetica, sans-serif" fill="black">${escapeXml(String(serial).slice(0, 42))}</text>
|
||||
<rect x="0" y="0" width="${textMaxWidth}" height="${normalizedHeight}" fill="none"/>
|
||||
</svg>`.trim()
|
||||
|
||||
// Alles zusammen compositen
|
||||
const final = await Sharp(base)
|
||||
const final = await Sharp({
|
||||
create: {
|
||||
width: normalizedWidth,
|
||||
height: normalizedHeight,
|
||||
channels: 3,
|
||||
background: { r: 255, g: 255, b: 255 },
|
||||
},
|
||||
})
|
||||
.composite([
|
||||
{ input: dataMatrixPng, top: 60, left: 20 },
|
||||
{ input: Buffer.from(textSvg), top: 0, left: 0 },
|
||||
{ input: dataMatrixPng, top: dmTop, left: dmLeft },
|
||||
])
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
fs.writeFileSync('label.png', final)
|
||||
|
||||
// Optional: Base64 zurückgeben (z.B. für API)
|
||||
const base64 = final.toString('base64')
|
||||
|
||||
return base64
|
||||
}*/
|
||||
return final.toString("base64")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,43 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { historyitems } from "../../db/schema";
|
||||
|
||||
const HISTORY_ENTITY_LABELS: Record<string, string> = {
|
||||
customers: "Kunden",
|
||||
members: "Mitglieder",
|
||||
vendors: "Lieferanten",
|
||||
projects: "Projekte",
|
||||
plants: "Objekte",
|
||||
contacts: "Kontakte",
|
||||
inventoryitems: "Inventarartikel",
|
||||
customerinventoryitems: "Kundeninventar",
|
||||
products: "Artikel",
|
||||
profiles: "Mitarbeiter",
|
||||
absencerequests: "Abwesenheiten",
|
||||
events: "Termine",
|
||||
tasks: "Aufgaben",
|
||||
vehicles: "Fahrzeuge",
|
||||
costcentres: "Kostenstellen",
|
||||
ownaccounts: "zusätzliche Buchungskonten",
|
||||
documentboxes: "Dokumentenboxen",
|
||||
hourrates: "Stundensätze",
|
||||
services: "Leistungen",
|
||||
roles: "Rollen",
|
||||
checks: "Überprüfungen",
|
||||
spaces: "Lagerplätze",
|
||||
customerspaces: "Kundenlagerplätze",
|
||||
trackingtrips: "Fahrten",
|
||||
createddocuments: "Dokumente",
|
||||
inventoryitemgroups: "Inventarartikelgruppen",
|
||||
bankstatements: "Buchungen",
|
||||
incominginvoices: "Eingangsrechnungen",
|
||||
files: "Dateien",
|
||||
memberrelations: "Mitgliedsverhältnisse",
|
||||
}
|
||||
|
||||
export function getHistoryEntityLabel(entity: string) {
|
||||
return HISTORY_ENTITY_LABELS[entity] || entity
|
||||
}
|
||||
|
||||
export async function insertHistoryItem(
|
||||
server: FastifyInstance,
|
||||
params: {
|
||||
@@ -14,16 +51,18 @@ export async function insertHistoryItem(
|
||||
text?: string
|
||||
}
|
||||
) {
|
||||
const entityLabel = getHistoryEntityLabel(params.entity)
|
||||
const textMap = {
|
||||
created: `Neuer Eintrag in ${params.entity} erstellt`,
|
||||
updated: `Eintrag in ${params.entity} geändert`,
|
||||
unchanged: `Eintrag in ${params.entity} unverändert`,
|
||||
archived: `Eintrag in ${params.entity} archiviert`,
|
||||
deleted: `Eintrag in ${params.entity} gelöscht`
|
||||
created: `Neuer Eintrag in ${entityLabel} erstellt`,
|
||||
updated: `Eintrag in ${entityLabel} geändert`,
|
||||
unchanged: `Eintrag in ${entityLabel} unverändert`,
|
||||
archived: `Eintrag in ${entityLabel} archiviert`,
|
||||
deleted: `Eintrag in ${entityLabel} gelöscht`
|
||||
}
|
||||
|
||||
const columnMap: Record<string, string> = {
|
||||
customers: "customer",
|
||||
members: "customer",
|
||||
vendors: "vendor",
|
||||
projects: "project",
|
||||
plants: "plant",
|
||||
@@ -43,12 +82,15 @@ export async function insertHistoryItem(
|
||||
roles: "role",
|
||||
checks: "check",
|
||||
spaces: "space",
|
||||
customerspaces: "customerspace",
|
||||
customerinventoryitems: "customerinventoryitem",
|
||||
trackingtrips: "trackingtrip",
|
||||
createddocuments: "createddocument",
|
||||
inventoryitemgroups: "inventoryitemgroup",
|
||||
bankstatements: "bankstatement",
|
||||
incominginvoices: "incomingInvoice",
|
||||
files: "file",
|
||||
memberrelations: "memberrelation",
|
||||
}
|
||||
|
||||
const fkColumn = columnMap[params.entity]
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
contracttypes,
|
||||
costcentres,
|
||||
createddocuments,
|
||||
customerinventoryitems,
|
||||
customerspaces,
|
||||
customers,
|
||||
files,
|
||||
filetags,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
inventoryitemgroups,
|
||||
inventoryitems,
|
||||
letterheads,
|
||||
memberrelations,
|
||||
ownaccounts,
|
||||
plants,
|
||||
productcategories,
|
||||
@@ -46,7 +49,7 @@ export const resourceConfig = {
|
||||
},
|
||||
customers: {
|
||||
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
|
||||
mtmLoad: ["contacts","projects","plants","createddocuments","contracts"],
|
||||
mtmLoad: ["contacts","projects","plants","createddocuments","contracts","customerinventoryitems","customerspaces"],
|
||||
table: customers,
|
||||
numberRangeHolder: "customerNumber",
|
||||
},
|
||||
@@ -57,6 +60,10 @@ export const resourceConfig = {
|
||||
numberRangeHolder: "customerNumber",
|
||||
relationKey: "customer",
|
||||
},
|
||||
memberrelations: {
|
||||
table: memberrelations,
|
||||
searchColumns: ["type", "billingInterval"],
|
||||
},
|
||||
contacts: {
|
||||
searchColumns: ["firstName", "lastName", "email", "phone", "notes"],
|
||||
table: contacts,
|
||||
@@ -99,6 +106,12 @@ export const resourceConfig = {
|
||||
table: inventoryitems,
|
||||
numberRangeHolder: "articleNumber",
|
||||
},
|
||||
customerinventoryitems: {
|
||||
table: customerinventoryitems,
|
||||
numberRangeHolder: "customerInventoryId",
|
||||
mtoLoad: ["customer", "customerspace", "product", "vendor"],
|
||||
searchColumns: ["name", "customerInventoryId", "serialNumber", "description", "manufacturer", "manufacturerNumber"],
|
||||
},
|
||||
inventoryitemgroups: {
|
||||
table: inventoryitemgroups
|
||||
},
|
||||
@@ -133,6 +146,13 @@ export const resourceConfig = {
|
||||
searchColumns: ["name","space_number","type","info_data"],
|
||||
numberRangeHolder: "spaceNumber",
|
||||
},
|
||||
customerspaces: {
|
||||
table: customerspaces,
|
||||
searchColumns: ["name","space_number","type","info_data","description"],
|
||||
numberRangeHolder: "space_number",
|
||||
mtoLoad: ["customer"],
|
||||
mtmLoad: ["customerinventoryitems"],
|
||||
},
|
||||
ownaccounts: {
|
||||
table: ownaccounts,
|
||||
searchColumns: ["name","description","number"],
|
||||
|
||||
@@ -16,6 +16,7 @@ const toast = useToast()
|
||||
const accounts = ref([])
|
||||
const ibanSearch = ref("")
|
||||
const showCreate = ref(false)
|
||||
const resolvingIban = ref(false)
|
||||
|
||||
const createPayload = ref({
|
||||
iban: "",
|
||||
@@ -78,6 +79,25 @@ const createAndAssign = async () => {
|
||||
showCreate.value = false
|
||||
}
|
||||
|
||||
const resolveCreatePayloadFromIban = async () => {
|
||||
const normalized = normalizeIban(createPayload.value.iban)
|
||||
if (!normalized) return
|
||||
|
||||
resolvingIban.value = true
|
||||
try {
|
||||
const data = await useFunctions().useBankingResolveIban(normalized)
|
||||
if (!data) return
|
||||
|
||||
createPayload.value.iban = data.iban || normalized
|
||||
if (data.bic) createPayload.value.bic = data.bic
|
||||
if (data.bankName) createPayload.value.bankName = data.bankName
|
||||
} catch (e) {
|
||||
// intentionally ignored: user can still enter fields manually
|
||||
} finally {
|
||||
resolvingIban.value = false
|
||||
}
|
||||
}
|
||||
|
||||
loadAccounts()
|
||||
</script>
|
||||
|
||||
@@ -125,7 +145,21 @@ loadAccounts()
|
||||
<template #header>Neue Bankverbindung erstellen</template>
|
||||
<div class="space-y-3">
|
||||
<UFormGroup label="IBAN">
|
||||
<UInput v-model="createPayload.iban" />
|
||||
<InputGroup>
|
||||
<UInput
|
||||
v-model="createPayload.iban"
|
||||
@blur="resolveCreatePayloadFromIban"
|
||||
@keydown.enter.prevent="resolveCreatePayloadFromIban"
|
||||
/>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="outline"
|
||||
:loading="resolvingIban"
|
||||
@click="resolveCreatePayloadFromIban"
|
||||
>
|
||||
Ermitteln
|
||||
</UButton>
|
||||
</InputGroup>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="BIC">
|
||||
<UInput v-model="createPayload.bic" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import EntityShowSubTimes from "~/components/EntityShowSubTimes.vue";
|
||||
import WikiEntityWidget from "~/components/wiki/WikiEntityWidget.vue";
|
||||
import LabelPrintModal from "~/components/LabelPrintModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
@@ -136,6 +137,18 @@ const changePinned = async () => {
|
||||
|
||||
}
|
||||
|
||||
const openCustomerInventoryLabelPrint = () => {
|
||||
modal.open(LabelPrintModal, {
|
||||
context: {
|
||||
id: props.item.id,
|
||||
customerInventoryId: props.item.customerInventoryId,
|
||||
name: props.item.name,
|
||||
customerName: props.item.customer?.name,
|
||||
serialNumber: props.item.serialNumber
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -193,6 +206,14 @@ const changePinned = async () => {
|
||||
color="yellow"
|
||||
@click="changePinned"
|
||||
></UButton>
|
||||
<UButton
|
||||
v-if="type === 'customerinventoryitems'"
|
||||
icon="i-heroicons-printer"
|
||||
variant="outline"
|
||||
@click="openCustomerInventoryLabelPrint"
|
||||
>
|
||||
Label
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/standardEntity/${type}/edit/${item.id}`)"
|
||||
>
|
||||
@@ -214,6 +235,14 @@ const changePinned = async () => {
|
||||
>{{item ? `${dataType.labelSingle}${props.item[dataType.templateColumns.find(i => i.title).key] ? ': ' + props.item[dataType.templateColumns.find(i => i.title).key] : ''}`: '' }}</h1>
|
||||
</template>
|
||||
<template #right>
|
||||
<UButton
|
||||
v-if="type === 'customerinventoryitems'"
|
||||
icon="i-heroicons-printer"
|
||||
variant="outline"
|
||||
@click="openCustomerInventoryLabelPrint"
|
||||
>
|
||||
Label
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/standardEntity/${type}/edit/${item.id}`)"
|
||||
>
|
||||
|
||||
@@ -21,13 +21,20 @@ const props = defineProps({
|
||||
const dataStore = useDataStore()
|
||||
|
||||
const dataType = dataStore.dataTypes[props.topLevelType]
|
||||
const historyType = computed(() => {
|
||||
const holder = dataType?.historyItemHolder
|
||||
if (!holder) return props.topLevelType
|
||||
|
||||
const normalized = String(holder).toLowerCase()
|
||||
return normalized.endsWith("s") ? normalized : `${normalized}s`
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard class="mt-5 scroll" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''">
|
||||
<HistoryDisplay
|
||||
:type="props.topLevelType"
|
||||
:type="historyType"
|
||||
v-if="props.item.id"
|
||||
:element-id="props.item.id"
|
||||
render-headline
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
queryStringData: {
|
||||
@@ -28,6 +29,33 @@ const dataType = dataStore.dataTypes[props.topLevelType]
|
||||
// const selectedColumns = ref(tempStore.columns[props.topLevelType] ? tempStore.columns[props.topLevelType] : dataType.templateColumns.filter(i => !i.disabledInTable))
|
||||
// const columns = computed(() => dataType.templateColumns.filter((column) => !column.disabledInTable && selectedColumns.value.find(i => i.key === column.key)))
|
||||
|
||||
const getDatapointValue = (datapoint) => {
|
||||
if (datapoint.key.includes(".")) {
|
||||
const [parentKey, childKey] = datapoint.key.split(".")
|
||||
return props.item?.[parentKey]?.[childKey]
|
||||
}
|
||||
return props.item?.[datapoint.key]
|
||||
}
|
||||
|
||||
const renderDatapointValue = (datapoint) => {
|
||||
const value = getDatapointValue(datapoint)
|
||||
if (value === null || value === undefined || value === "") return "-"
|
||||
|
||||
if (datapoint.inputType === "date") {
|
||||
return dayjs(value).isValid() ? dayjs(value).format("DD.MM.YYYY") : String(value)
|
||||
}
|
||||
|
||||
if (datapoint.inputType === "datetime") {
|
||||
return dayjs(value).isValid() ? dayjs(value).format("DD.MM.YYYY HH:mm") : String(value)
|
||||
}
|
||||
|
||||
if (datapoint.inputType === "bool" || typeof value === "boolean") {
|
||||
return value ? "Ja" : "Nein"
|
||||
}
|
||||
|
||||
return `${value}${datapoint.unit ? datapoint.unit : ""}`
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -53,8 +81,7 @@ const dataType = dataStore.dataTypes[props.topLevelType]
|
||||
<td>
|
||||
<component v-if="datapoint.component" :is="datapoint.component" :row="props.item" :in-show="true"></component>
|
||||
<div v-else>
|
||||
<span v-if="datapoint.key.includes('.')">{{props.item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]}}{{datapoint.unit}}</span>
|
||||
<span v-else>{{props.item[datapoint.key]}} {{datapoint.unit}}</span>
|
||||
<span>{{ renderDatapointValue(datapoint) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -24,6 +24,7 @@ const emit = defineEmits(["updateNeeded"]);
|
||||
|
||||
const router = useRouter()
|
||||
const profileStore = useProfileStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const renderedPhases = computed(() => {
|
||||
if(props.topLevelType === "projects" && props.item.phases) {
|
||||
@@ -57,6 +58,7 @@ const renderedPhases = computed(() => {
|
||||
})
|
||||
|
||||
const changeActivePhase = async (key) => {
|
||||
console.log(props.item)
|
||||
let item = await useEntities("projects").selectSingle(props.item.id,'*')
|
||||
|
||||
let phaseLabel = ""
|
||||
@@ -67,13 +69,15 @@ const changeActivePhase = async (key) => {
|
||||
if(p.key === key) {
|
||||
p.active = true
|
||||
p.activated_at = dayjs().format()
|
||||
p.activated_by = profileStore.activeProfile.id
|
||||
p.activated_by = auth.user.id
|
||||
phaseLabel = p.label
|
||||
}
|
||||
|
||||
return p
|
||||
})
|
||||
|
||||
console.log(item)
|
||||
|
||||
const res = await useEntities("projects").update(item.id, {phases:item.phases,active_phase: item.phases.find(i => i.active).label})
|
||||
|
||||
emit("updateNeeded")
|
||||
@@ -140,7 +144,7 @@ const changeActivePhase = async (key) => {
|
||||
|
||||
<div>
|
||||
<p v-if="item.activated_at" class="dark:text-white text-black">Aktiviert am: {{dayjs(item.activated_at).format("DD.MM.YY HH:mm")}} Uhr</p>
|
||||
<p v-if="item.activated_by" class="dark:text-white text-black">Aktiviert durch: {{profileStore.getProfileById(item.activated_by).fullName}}</p>
|
||||
<p v-if="item.activated_by" class="dark:text-white text-black">Aktiviert durch: {{item.activated_by}}</p>
|
||||
<p v-if="item.description" class="dark:text-white text-black">Beschreibung: {{item.description}}</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
@@ -44,7 +44,9 @@ async function loadLabel() {
|
||||
labelData.value = await $api(`/api/print/label`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
context: props.context || null
|
||||
context: props.context || null,
|
||||
width: 584,
|
||||
height: 354
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
@@ -78,11 +80,17 @@ onMounted(() => {
|
||||
|
||||
})
|
||||
|
||||
watch(() => labelPrinter.connected, (connected) => {
|
||||
if (connected && !labelData.value) {
|
||||
loadLabel()
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal>
|
||||
<UCard>
|
||||
<UModal :ui="{ width: 'sm:max-w-5xl' }">
|
||||
<UCard class="w-[92vw] max-w-5xl">
|
||||
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -91,11 +99,11 @@ onMounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="!loading && labelPrinter.connected">
|
||||
<div v-if="!loading && labelPrinter.connected" class="w-full">
|
||||
<img
|
||||
:src="`data:image/png;base64,${labelData.base64}`"
|
||||
alt="Label Preview"
|
||||
class="max-w-full max-h-64 object-contain"
|
||||
class="w-full max-h-[70vh] object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="loading && !labelPrinter.connected">
|
||||
|
||||
@@ -12,6 +12,9 @@ const tenantExtraModules = computed(() => {
|
||||
const showMembersNav = computed(() => {
|
||||
return tenantExtraModules.value.includes("verein") && (has("members") || has("customers"))
|
||||
})
|
||||
const showMemberRelationsNav = computed(() => {
|
||||
return tenantExtraModules.value.includes("verein") && has("members")
|
||||
})
|
||||
|
||||
const links = computed(() => {
|
||||
return [
|
||||
@@ -191,6 +194,26 @@ const links = computed(() => {
|
||||
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"
|
||||
}] : [],
|
||||
]
|
||||
}] : [],
|
||||
{
|
||||
@@ -218,6 +241,11 @@ const links = computed(() => {
|
||||
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",
|
||||
@@ -228,21 +256,21 @@ const links = computed(() => {
|
||||
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"
|
||||
}] : [],
|
||||
...has("inventoryitems") ? [{
|
||||
label: "Inventar",
|
||||
to: "/standardEntity/inventoryitems",
|
||||
icon: "i-heroicons-puzzle-piece"
|
||||
}] : [],
|
||||
...has("inventoryitems") ? [{
|
||||
label: "Inventargruppen",
|
||||
to: "/standardEntity/inventoryitemgroups",
|
||||
icon: "i-heroicons-puzzle-piece"
|
||||
}] : [],
|
||||
]
|
||||
},
|
||||
|
||||
@@ -286,14 +314,6 @@ const links = computed(() => {
|
||||
label: "Firmeneinstellungen",
|
||||
to: "/settings/tenant",
|
||||
icon: "i-heroicons-building-office",
|
||||
}, {
|
||||
label: "Projekttypen",
|
||||
to: "/projecttypes",
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
}, {
|
||||
label: "Vertragstypen",
|
||||
to: "/standardEntity/contracttypes",
|
||||
icon: "i-heroicons-document-duplicate",
|
||||
}, {
|
||||
label: "Export",
|
||||
to: "/export",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
@@ -6,13 +8,15 @@ const props = defineProps({
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
const addressData = computed(() => props.row?.infoData || props.row?.info_data || {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="props.row.infoData.streetNumber">{{props.row.infoData.streetNumber}},</span>
|
||||
<span v-if="props.row.infoData.street">{{props.row.infoData.street}},</span>
|
||||
<span v-if="props.row.infoData.special">{{props.row.infoData.special}},</span>
|
||||
<span v-if="props.row.infoData.zip">{{props.row.infoData.zip}},</span>
|
||||
<span v-if="props.row.infoData.city">{{props.row.infoData.city}}</span>
|
||||
<span v-if="addressData.streetNumber">{{ addressData.streetNumber }},</span>
|
||||
<span v-if="addressData.street">{{ addressData.street }},</span>
|
||||
<span v-if="addressData.special">{{ addressData.special }},</span>
|
||||
<span v-if="addressData.zip">{{ addressData.zip }},</span>
|
||||
<span v-if="addressData.city">{{ addressData.city }}</span>
|
||||
|
||||
</template>
|
||||
|
||||
48
frontend/components/columnRenderings/memberrelation.vue
Normal file
48
frontend/components/columnRenderings/memberrelation.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
const relations = ref([])
|
||||
|
||||
const normalizeId = (value) => {
|
||||
if (value === null || value === undefined || value === "") return null
|
||||
const parsed = Number(value)
|
||||
return Number.isNaN(parsed) ? String(value) : parsed
|
||||
}
|
||||
|
||||
const relationLabel = computed(() => {
|
||||
const id = normalizeId(props.row?.infoData?.memberrelation)
|
||||
if (!id) return ""
|
||||
return relations.value.find((i) => normalizeId(i.id) === id)?.type || ""
|
||||
})
|
||||
|
||||
const relationId = computed(() => {
|
||||
return normalizeId(props.row?.infoData?.memberrelation)
|
||||
})
|
||||
|
||||
const loadRelations = async () => {
|
||||
try {
|
||||
relations.value = await useEntities("memberrelations").select()
|
||||
} catch (e) {
|
||||
relations.value = []
|
||||
}
|
||||
}
|
||||
|
||||
loadRelations()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
v-if="relationId && relationLabel"
|
||||
:to="`/standardEntity/memberrelations/show/${relationId}`"
|
||||
class="text-primary"
|
||||
>
|
||||
{{ relationLabel }}
|
||||
</NuxtLink>
|
||||
<span v-else>{{ relationLabel }}</span>
|
||||
</template>
|
||||
20
frontend/components/columnRenderings/product.vue
Normal file
20
frontend/components/columnRenderings/product.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: {}
|
||||
},
|
||||
inShow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.row.product">
|
||||
<nuxt-link v-if="props.inShow" :to="`/standardEntity/products/show/${props.row.product.id}`">{{ props.row.product ? props.row.product.name : '' }}</nuxt-link>
|
||||
<span v-else>{{ props.row.product ? props.row.product.name : '' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
const productcategories = ref([])
|
||||
|
||||
const setup = async () => {
|
||||
productcategories.value = await useEntities("productcategories").select()
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
const renderedCategories = computed(() => {
|
||||
if (!Array.isArray(props.row?.productcategories)) return ""
|
||||
|
||||
return props.row.productcategories
|
||||
.map((id) => productcategories.value.find((x) => x.id === id)?.name)
|
||||
.filter(Boolean)
|
||||
.join(", ")
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="renderedCategories">{{ renderedCategories }}</span>
|
||||
</template>
|
||||
@@ -86,5 +86,11 @@ export const useFunctions = () => {
|
||||
|
||||
}
|
||||
|
||||
return {getWorkingTimesEvaluationData, useNextNumber, useBankingGenerateLink, useZipCheck, useBankingCheckInstitutions, useBankingListRequisitions, useCreatePDF}
|
||||
const useBankingResolveIban = async (iban) => {
|
||||
const normalized = String(iban || "").replace(/\s+/g, "").toUpperCase()
|
||||
if (!normalized) return null
|
||||
return await useNuxtApp().$api(`/api/banking/iban/${encodeURIComponent(normalized)}`)
|
||||
}
|
||||
|
||||
return {getWorkingTimesEvaluationData, useNextNumber, useBankingGenerateLink, useZipCheck, useBankingCheckInstitutions, useBankingListRequisitions, useBankingResolveIban, useCreatePDF}
|
||||
}
|
||||
|
||||
@@ -350,6 +350,14 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
<span class="font-mono text-xs opacity-75 mr-2">{{ option.vendorNumber }}</span> {{ option.name }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
<UButton
|
||||
v-if="mode !== 'show'"
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
:disabled="!itemInfo.vendor"
|
||||
@click="itemInfo.vendor = null"
|
||||
/>
|
||||
<EntityModalButtons
|
||||
v-if="mode !== 'show'"
|
||||
type="vendors"
|
||||
|
||||
@@ -14,6 +14,9 @@ const resources = {
|
||||
spaces: {
|
||||
label: "Lagerplätze"
|
||||
},
|
||||
customerspaces: {
|
||||
label: "Kundenlagerplätze"
|
||||
},
|
||||
invoices: {
|
||||
label: "Rechnungen"
|
||||
},
|
||||
@@ -23,6 +26,9 @@ const resources = {
|
||||
inventoryitems: {
|
||||
label: "Inventarartikel"
|
||||
},
|
||||
customerinventoryitems: {
|
||||
label: "Kundeninventarartikel"
|
||||
},
|
||||
projects: {
|
||||
label: "Projekte"
|
||||
},
|
||||
@@ -37,7 +43,17 @@ const resources = {
|
||||
}
|
||||
}
|
||||
|
||||
const numberRanges = ref(auth.activeTenantData.numberRanges)
|
||||
const numberRanges = ref(auth.activeTenantData.numberRanges || {})
|
||||
|
||||
Object.keys(resources).forEach((key) => {
|
||||
if (!numberRanges.value[key]) {
|
||||
numberRanges.value[key] = {
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
nextNumber: 1000
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const updateNumberRanges = async (range) => {
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ import dayjs from "dayjs"
|
||||
|
||||
import projecttype from "~/components/columnRenderings/projecttype.vue"
|
||||
import contracttype from "~/components/columnRenderings/contracttype.vue"
|
||||
import memberrelation from "~/components/columnRenderings/memberrelation.vue"
|
||||
import customer from "~/components/columnRenderings/customer.vue"
|
||||
import contact from "~/components/columnRenderings/contact.vue"
|
||||
import plant from "~/components/columnRenderings/plant.vue"
|
||||
import vendor from "~/components/columnRenderings/vendor.vue"
|
||||
import product from "~/components/columnRenderings/product.vue"
|
||||
import active from "~/components/columnRenderings/active.vue"
|
||||
import sellingPrice from "~/components/columnRenderings/sellingPrice.vue";
|
||||
import unit from "~/components/columnRenderings/unit.vue";
|
||||
@@ -32,6 +34,7 @@ import endDate from "~/components/columnRenderings/endDate.vue"
|
||||
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 phase from "~/components/columnRenderings/phase.vue"
|
||||
import vehiclesWithLoad from "~/components/columnRenderings/vehiclesWithLoad.vue"
|
||||
import inventoryitemsWithLoad from "~/components/columnRenderings/inventoryitemsWithLoad.vue"
|
||||
@@ -167,7 +170,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
numberRangeHolder: "customerNumber",
|
||||
historyItemHolder: "customer",
|
||||
sortColumn: "customerNumber",
|
||||
selectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*)",
|
||||
selectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*), customerinventoryitems(*), customerspaces(*)",
|
||||
filters: [{
|
||||
name: "Archivierte ausblenden",
|
||||
default: true,
|
||||
@@ -443,7 +446,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
inputColumn: "Allgemeines"
|
||||
},*/
|
||||
],
|
||||
showTabs: [{label: 'Informationen'},{label: 'Ansprechpartner'},{label: 'Dateien'},{label: 'Ausgangsbelege'},{label: 'Projekte'},{label: 'Objekte'},{label: 'Termine'},{label: 'Verträge'},{label: 'Wiki'}]
|
||||
showTabs: [{label: 'Informationen'},{label: 'Ansprechpartner'},{label: 'Dateien'},{label: 'Ausgangsbelege'},{label: 'Projekte'},{label: 'Objekte'},{label: 'Termine'},{label: 'Verträge'},{label: 'Kundeninventar', key: 'customerinventoryitems'},{label: 'Kundenlagerplätze', key: 'customerspaces'},{label: 'Wiki'}]
|
||||
},
|
||||
members: {
|
||||
isArchivable: true,
|
||||
@@ -524,6 +527,24 @@ export const useDataStore = defineStore('data', () => {
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "infoData.birthdate",
|
||||
label: "Geburtsdatum",
|
||||
inputType: "date",
|
||||
inputColumn: "Allgemeines",
|
||||
disabledInTable: true
|
||||
},
|
||||
{
|
||||
key: "infoData.memberrelation",
|
||||
label: "Mitgliedsverhältnis",
|
||||
component: memberrelation,
|
||||
inputType: "select",
|
||||
selectDataType: "memberrelations",
|
||||
selectOptionAttribute: "type",
|
||||
selectSearchAttributes: ['type', 'billingInterval'],
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "active",
|
||||
label: "Aktiv",
|
||||
@@ -1035,6 +1056,57 @@ export const useDataStore = defineStore('data', () => {
|
||||
],
|
||||
showTabs: [{ label: "Informationen" }]
|
||||
},
|
||||
memberrelations: {
|
||||
isArchivable: true,
|
||||
label: "Mitgliedsverhältnisse",
|
||||
labelSingle: "Mitgliedsverhältnis",
|
||||
isStandardEntity: true,
|
||||
historyItemHolder: "memberrelation",
|
||||
redirect: true,
|
||||
sortColumn: "type",
|
||||
selectWithInformation: "*",
|
||||
filters: [{
|
||||
name: "Archivierte ausblenden",
|
||||
default: true,
|
||||
"filterFunction": function (row) {
|
||||
return !row.archived
|
||||
}
|
||||
}],
|
||||
templateColumns: [
|
||||
{
|
||||
key: "type",
|
||||
label: "Typ",
|
||||
required: true,
|
||||
title: true,
|
||||
inputType: "text",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "billingInterval",
|
||||
label: "Abrechnungsintervall",
|
||||
required: true,
|
||||
inputType: "select",
|
||||
selectValueAttribute: "label",
|
||||
selectOptionAttribute: "label",
|
||||
selectManualOptions: [
|
||||
{ label: "Monatlich" },
|
||||
{ label: "Quartalsweise" },
|
||||
{ label: "Halbjährlich" },
|
||||
{ label: "Jährlich" }
|
||||
],
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "billingAmount",
|
||||
label: "Abrechnungshöhe",
|
||||
required: true,
|
||||
inputType: "number",
|
||||
inputTrailing: "€",
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
showTabs: [{ label: "Informationen" }]
|
||||
},
|
||||
absencerequests: {
|
||||
isArchivable: true,
|
||||
label: "Abwesenheiten",
|
||||
@@ -1293,7 +1365,8 @@ export const useDataStore = defineStore('data', () => {
|
||||
selectDataType: "productcategories",
|
||||
selectOptionAttribute: "name",
|
||||
selectSearchAttributes: ['name'],
|
||||
selectMultiple: true
|
||||
selectMultiple: true,
|
||||
component: productcategoriesWithLoad
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
@@ -1773,6 +1846,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
selectValueAttribute: "label",
|
||||
selectManualOptions: [
|
||||
{label:"Standort"},
|
||||
{label:"Raum"},
|
||||
{label:"Regalplatz"},
|
||||
{label:"Kiste"},
|
||||
{label:"Palettenplatz"},
|
||||
@@ -1856,6 +1930,149 @@ export const useDataStore = defineStore('data', () => {
|
||||
},{label: 'Inventarartikel'},{label: 'Wiki'}
|
||||
]
|
||||
},
|
||||
customerspaces: {
|
||||
isArchivable: true,
|
||||
label: "Kundenlagerplätze",
|
||||
labelSingle: "Kundenlagerplatz",
|
||||
isStandardEntity: true,
|
||||
selectWithInformation: "*, customer(id,name), files(*)",
|
||||
sortColumn: "space_number",
|
||||
redirect: true,
|
||||
numberRangeHolder: "space_number",
|
||||
historyItemHolder: "customerspace",
|
||||
filters:[{
|
||||
name: "Archivierte ausblenden",
|
||||
default: true,
|
||||
"filterFunction": function (row) {
|
||||
return !row.archived
|
||||
}
|
||||
}],
|
||||
inputColumns: [
|
||||
"Allgemeines",
|
||||
"Ort"
|
||||
],
|
||||
templateColumns: [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
inputType: "text",
|
||||
required: true,
|
||||
title: true,
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "customer",
|
||||
label: "Kunde",
|
||||
inputType: "select",
|
||||
required: true,
|
||||
selectDataType: "customers",
|
||||
selectOptionAttribute: "name",
|
||||
selectSearchAttributes: ["name"],
|
||||
component: customer,
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "space_number",
|
||||
label: "Kundenlagerplatznr.",
|
||||
inputType: "text",
|
||||
inputIsNumberRange: true,
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
label: "Typ",
|
||||
inputType: "select",
|
||||
required: true,
|
||||
selectValueAttribute: "label",
|
||||
selectManualOptions: [
|
||||
{label:"Standort"},
|
||||
{label:"Raum"},
|
||||
{label:"Regalplatz"},
|
||||
{label:"Kiste"},
|
||||
{label:"Palettenplatz"},
|
||||
{label:"Sonstiges"}
|
||||
],
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "parentSpace",
|
||||
label: "Übergeordneter Kundenlagerplatz",
|
||||
inputType: "select",
|
||||
selectDataType: "customerspaces",
|
||||
selectOptionAttribute: "space_number",
|
||||
selectValueAttribute: "id",
|
||||
inputColumn: "Allgemeines"
|
||||
},
|
||||
{
|
||||
key: "info_data.streetNumber",
|
||||
label: "Straße + Hausnummer",
|
||||
inputType: "text",
|
||||
disabledInTable: true,
|
||||
inputColumn: "Ort"
|
||||
},
|
||||
{
|
||||
key: "info_data.special",
|
||||
label: "Adresszusatz",
|
||||
inputType: "text",
|
||||
disabledInTable: true,
|
||||
inputColumn: "Ort"
|
||||
},
|
||||
{
|
||||
key: "info_data.zip",
|
||||
label: "Postleitzahl",
|
||||
inputType: "text",
|
||||
disabledInTable: true,
|
||||
inputColumn: "Ort",
|
||||
inputChangeFunction: async function (row) {
|
||||
const zip = String(row.info_data.zip || "").replace(/\D/g, "")
|
||||
row.info_data.zip = zip
|
||||
if ([4, 5].includes(zip.length)) {
|
||||
const zipData = await useFunctions().useZipCheck(zip)
|
||||
row.info_data.zip = zipData?.zip || row.info_data.zip
|
||||
row.info_data.city = zipData?.short || row.info_data.city
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "info_data.city",
|
||||
label: "Stadt",
|
||||
inputType: "text",
|
||||
disabledInTable: true,
|
||||
inputColumn: "Ort"
|
||||
},
|
||||
{
|
||||
key: "info_data.country",
|
||||
label: "Land",
|
||||
inputType: "select",
|
||||
selectDataType: "countrys",
|
||||
selectOptionAttribute: "name",
|
||||
selectValueAttribute: "name",
|
||||
disabledInTable: true,
|
||||
inputColumn: "Ort"
|
||||
},
|
||||
{
|
||||
key: "address",
|
||||
label: "Adresse",
|
||||
component: address
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "Beschreibung",
|
||||
inputType: "textarea",
|
||||
}
|
||||
],
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}, {
|
||||
label: 'Dateien',
|
||||
},{label: 'Kundeninventar', key: 'customerinventoryitems'},{label: 'Wiki'}
|
||||
]
|
||||
},
|
||||
users: {
|
||||
label: "Benutzer",
|
||||
labelSingle: "Benutzer"
|
||||
@@ -1968,6 +2185,179 @@ export const useDataStore = defineStore('data', () => {
|
||||
}
|
||||
]
|
||||
},
|
||||
customerinventoryitems: {
|
||||
isArchivable: true,
|
||||
label: "Kundeninventar",
|
||||
labelSingle: "Kundeninventarartikel",
|
||||
isStandardEntity: true,
|
||||
selectWithInformation: "*, files(*), customer(id,name), customerspace(id,name,space_number), product(id,name,article_number,description,manufacturer,manufacturer_number,purchase_price,vendorAllocation), vendor(id,name)",
|
||||
redirect: true,
|
||||
numberRangeHolder: "customerInventoryId",
|
||||
historyItemHolder: "customerinventoryitem",
|
||||
inputColumns: [
|
||||
"Allgemeines",
|
||||
"Anschaffung"
|
||||
],
|
||||
filters:[{
|
||||
name: "Archivierte ausblenden",
|
||||
default: true,
|
||||
"filterFunction": function (row) {
|
||||
return !row.archived
|
||||
}
|
||||
}],
|
||||
templateColumns: [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
title: true,
|
||||
required: true,
|
||||
inputType: "text",
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "customer",
|
||||
label: "Kunde",
|
||||
inputType: "select",
|
||||
required: true,
|
||||
selectDataType: "customers",
|
||||
selectOptionAttribute: "name",
|
||||
selectSearchAttributes: ["name"],
|
||||
component: customer,
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "customerInventoryId",
|
||||
label: "Kundeninventar-ID",
|
||||
inputType: "text",
|
||||
inputIsNumberRange: true,
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "product",
|
||||
label: "Ableitung von Artikel",
|
||||
inputType: "select",
|
||||
selectDataType: "products",
|
||||
selectOptionAttribute: "name",
|
||||
selectSearchAttributes: ["name", "article_number"],
|
||||
inputColumn: "Allgemeines",
|
||||
component: product,
|
||||
inputChangeFunction: function (row, loadedOptions) {
|
||||
const products = loadedOptions?.products || []
|
||||
const selected = products.find((p) => p.id === row.product)
|
||||
if (!selected) return
|
||||
|
||||
row.name = selected.name || null
|
||||
row.description = selected.description || null
|
||||
row.manufacturer = selected.manufacturer || null
|
||||
row.manufacturerNumber = selected.manufacturer_number || null
|
||||
row.purchasePrice = typeof selected.purchase_price === "number" ? selected.purchase_price : row.purchasePrice
|
||||
row.currentValue = row.currentValue ?? row.purchasePrice
|
||||
|
||||
const allocations = Array.isArray(selected.vendor_allocation)
|
||||
? selected.vendor_allocation
|
||||
: (Array.isArray(selected.vendorAllocation) ? selected.vendorAllocation : [])
|
||||
const firstAllocation = allocations[0]
|
||||
if (typeof firstAllocation === "number") {
|
||||
row.vendor = firstAllocation
|
||||
} else if (firstAllocation && typeof firstAllocation === "object") {
|
||||
row.vendor = firstAllocation.vendor || firstAllocation.vendor_id || firstAllocation.id || row.vendor
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "Beschreibung",
|
||||
inputType: "textarea",
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "customerspace",
|
||||
label: "Aktueller Kundenlagerplatz",
|
||||
inputType: "select",
|
||||
selectDataType: "customerspaces",
|
||||
selectOptionAttribute: "name",
|
||||
selectSearchAttributes: ["name", "space_number"],
|
||||
inputColumn: "Allgemeines",
|
||||
component: space
|
||||
},
|
||||
{
|
||||
key: "serialNumber",
|
||||
label: "Seriennummer",
|
||||
inputType: "text",
|
||||
inputColumn: "Allgemeines"
|
||||
},
|
||||
{
|
||||
key: "quantity",
|
||||
label: "Menge",
|
||||
inputType: "number",
|
||||
inputColumn: "Allgemeines",
|
||||
disabledFunction: function (item) {
|
||||
return item.serialNumber
|
||||
},
|
||||
helpComponent: quantity,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "purchaseDate",
|
||||
label: "Kaufdatum",
|
||||
inputType: "date",
|
||||
inputColumn: "Anschaffung",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "vendor",
|
||||
label: "Lieferant",
|
||||
inputType: "select",
|
||||
selectDataType: "vendors",
|
||||
selectOptionAttribute: "name",
|
||||
selectSearchAttributes: ["name"],
|
||||
inputColumn: "Anschaffung",
|
||||
component: vendor
|
||||
},
|
||||
{
|
||||
key: "purchasePrice",
|
||||
label: "Kaufpreis",
|
||||
inputType: "number",
|
||||
inputStepSize: "0.01",
|
||||
inputColumn: "Anschaffung",
|
||||
component: purchasePrice,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "manufacturer",
|
||||
label: "Hersteller",
|
||||
inputType: "text",
|
||||
inputColumn: "Anschaffung"
|
||||
},
|
||||
{
|
||||
key: "manufacturerNumber",
|
||||
label: "Herstellernummer",
|
||||
inputType: "text",
|
||||
inputColumn: "Anschaffung"
|
||||
},
|
||||
{
|
||||
key: "currentValue",
|
||||
label: "Aktueller Wert",
|
||||
inputType: "number",
|
||||
inputStepSize: "0.01",
|
||||
inputColumn: "Anschaffung",
|
||||
sortable: true
|
||||
},
|
||||
],
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}, {
|
||||
label: 'Dateien',
|
||||
}, {
|
||||
label: 'Wiki',
|
||||
}
|
||||
]
|
||||
},
|
||||
inventoryitems: {
|
||||
isArchivable: true,
|
||||
label: "Inventarartikel",
|
||||
|
||||
14
skills/openclaw-m2m-openapi/references/m2m-endpoints.md
Normal file
14
skills/openclaw-m2m-openapi/references/m2m-endpoints.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Expected Endpoints
|
||||
|
||||
- OpenAPI UI: `/docs`
|
||||
- OpenAPI JSON: `/openapi.json`
|
||||
- API key list: `GET /api/tenant/api-keys`
|
||||
- API key create: `POST /api/tenant/api-keys`
|
||||
- API key update: `PATCH /api/tenant/api-keys/:id`
|
||||
- API key delete: `DELETE /api/tenant/api-keys/:id`
|
||||
- M2M exchange: `POST /internal/auth/m2m/token`
|
||||
|
||||
# Header Conventions
|
||||
|
||||
- API key auth: `x-api-key`
|
||||
- JWT auth: `Authorization: Bearer <token>`
|
||||
Reference in New Issue
Block a user