Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
67
backend/scripts/dedupe-auth-tenant-users.ts
Normal file
67
backend/scripts/dedupe-auth-tenant-users.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import "dotenv/config"
|
||||||
|
import { drizzle } from "drizzle-orm/node-postgres"
|
||||||
|
import { sql } from "drizzle-orm"
|
||||||
|
import { Pool } from "pg"
|
||||||
|
|
||||||
|
import { loadSecrets, secrets } from "../src/utils/secrets"
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
let connectionString = process.env.DATABASE_URL
|
||||||
|
|
||||||
|
if (!connectionString) {
|
||||||
|
await loadSecrets()
|
||||||
|
connectionString = secrets.DATABASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectionString) {
|
||||||
|
throw new Error("DATABASE_URL not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString,
|
||||||
|
max: 1,
|
||||||
|
connectionTimeoutMillis: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = drizzle(pool)
|
||||||
|
|
||||||
|
const duplicates = await db.execute(sql`
|
||||||
|
SELECT tenant_id, user_id, count(*)::int AS count
|
||||||
|
FROM auth_tenant_users
|
||||||
|
GROUP BY tenant_id, user_id
|
||||||
|
HAVING count(*) > 1
|
||||||
|
ORDER BY count(*) DESC;
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log(`Gefundene doppelte Tenant-Zuordnungen: ${duplicates.rows.length}`)
|
||||||
|
for (const row of duplicates.rows as Record<string, any>[]) {
|
||||||
|
console.log(JSON.stringify(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await db.execute(sql`
|
||||||
|
DELETE FROM auth_tenant_users duplicate
|
||||||
|
USING auth_tenant_users keep
|
||||||
|
WHERE duplicate.tenant_id = keep.tenant_id
|
||||||
|
AND duplicate.user_id = keep.user_id
|
||||||
|
AND duplicate.ctid > keep.ctid;
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log(`Entfernte doppelte Zeilen: ${deleted.rowCount || 0}`)
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS auth_tenant_users_tenant_user_unique
|
||||||
|
ON auth_tenant_users (tenant_id, user_id);
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log("Unique Index auth_tenant_users_tenant_user_unique ist vorhanden")
|
||||||
|
} finally {
|
||||||
|
await pool.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error("Dedupe auth_tenant_users failed")
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -447,7 +447,10 @@ async function getCloseData(server:FastifyInstance,item: any, tenant: any, units
|
|||||||
// Formatiert Zahlen zu deutscher Währung
|
// Formatiert Zahlen zu deutscher Währung
|
||||||
function renderCurrency(value: any, currency = "€") {
|
function renderCurrency(value: any, currency = "€") {
|
||||||
if (value === undefined || value === null) return "0,00 " + currency;
|
if (value === undefined || value === null) return "0,00 " + currency;
|
||||||
return Number(value).toFixed(2).replace(".", ",") + " " + currency;
|
return Number(value).toLocaleString("de-DE", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
}) + " " + currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Berechnet den Zeilenpreis (Menge * Preis * Rabatt)
|
// Berechnet den Zeilenpreis (Menge * Preis * Rabatt)
|
||||||
|
|||||||
@@ -660,10 +660,16 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
})
|
})
|
||||||
.from(authUserRoles),
|
.from(authUserRoles),
|
||||||
]);
|
]);
|
||||||
|
const uniqueMembershipRows = Array.from(
|
||||||
|
new Map(membershipRows.map((membership) => [
|
||||||
|
`${membership.tenant_id}:${membership.user_id}`,
|
||||||
|
membership,
|
||||||
|
])).values()
|
||||||
|
);
|
||||||
|
|
||||||
const users = userRows.map((user) => {
|
const users = userRows.map((user) => {
|
||||||
const profiles = profileRows.filter((profile) => profile.user_id === user.id);
|
const profiles = profileRows.filter((profile) => profile.user_id === user.id);
|
||||||
const memberships = membershipRows.filter((membership) => membership.user_id === user.id);
|
const memberships = uniqueMembershipRows.filter((membership) => membership.user_id === user.id);
|
||||||
const roleAssignments = roleAssignmentRows.filter((assignment) => assignment.user_id === user.id);
|
const roleAssignments = roleAssignmentRows.filter((assignment) => assignment.user_id === user.id);
|
||||||
const preferredProfile = profiles.find((profile) => profile.active) || profiles[0];
|
const preferredProfile = profiles.find((profile) => profile.active) || profiles[0];
|
||||||
const fallbackName = deriveNameFromEmail(user.email);
|
const fallbackName = deriveNameFromEmail(user.email);
|
||||||
@@ -683,7 +689,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
const tenantsWithCounts = tenantRows.map((tenant) => ({
|
const tenantsWithCounts = tenantRows.map((tenant) => ({
|
||||||
...tenant,
|
...tenant,
|
||||||
user_count: membershipRows.filter((membership) => membership.tenant_id === tenant.id).length,
|
user_count: uniqueMembershipRows.filter((membership) => membership.tenant_id === tenant.id).length,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -691,7 +697,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
tenants: tenantsWithCounts,
|
tenants: tenantsWithCounts,
|
||||||
roles: roleRows,
|
roles: roleRows,
|
||||||
unassignedProfiles: profileRows.filter((profile) => !profile.user_id),
|
unassignedProfiles: profileRows.filter((profile) => !profile.user_id),
|
||||||
memberships: membershipRows,
|
memberships: uniqueMembershipRows,
|
||||||
roleAssignments: roleAssignmentRows,
|
roleAssignments: roleAssignmentRows,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export default async function meRoutes(server: FastifyInstance) {
|
|||||||
.innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id))
|
.innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id))
|
||||||
.where(eq(authTenantUsers.user_id, userId))
|
.where(eq(authTenantUsers.user_id, userId))
|
||||||
|
|
||||||
const tenantList = tenantRows ?? []
|
const tenantList = Array.from(
|
||||||
|
new Map((tenantRows ?? []).map((tenant) => [tenant.id, tenant])).values()
|
||||||
|
)
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// 3) ACTIVE TENANT
|
// 3) ACTIVE TENANT
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ const getCoordinatesForPDFLib = (x:number ,y:number, page:any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRightAlignedX = (rightEdgeMm: number, text: string, size: number, font: any, page: any) => {
|
||||||
|
return getCoordinatesForPDFLib(rightEdgeMm, 0, page).x - font.widthOfTextAtSize(text, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const getBackgroundSourceBuffer = async (server:FastifyInstance, path:string) => {
|
const getBackgroundSourceBuffer = async (server:FastifyInstance, path:string) => {
|
||||||
|
|
||||||
@@ -372,8 +376,10 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
font: fontBold
|
font: fontBold
|
||||||
})
|
})
|
||||||
|
|
||||||
pages[pageCounter - 1].drawText("Einheitspreis", {
|
const unitPriceHeader = "Einheitspreis"
|
||||||
...getCoordinatesForPDFLib(150, 137, page1),
|
pages[pageCounter - 1].drawText(unitPriceHeader, {
|
||||||
|
y: getCoordinatesForPDFLib(150, 137, page1).y,
|
||||||
|
x: getRightAlignedX(177, unitPriceHeader, 12, fontBold, page1),
|
||||||
size: 12,
|
size: 12,
|
||||||
color: rgb(0, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
lineHeight: 12,
|
lineHeight: 12,
|
||||||
@@ -399,6 +405,12 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
|
|
||||||
let pageIndex = 0
|
let pageIndex = 0
|
||||||
|
|
||||||
|
const getTitleKey = (row) => `${row.pos} - ${row.text}`
|
||||||
|
const getTransferSumText = (row) => {
|
||||||
|
const transferSum = invoiceData.total.titleSumsTransfer[getTitleKey(row)]
|
||||||
|
|
||||||
|
return transferSum ? `Übertrag: ${transferSum}` : null
|
||||||
|
}
|
||||||
|
|
||||||
invoiceData.rows.forEach((row, index) => {
|
invoiceData.rows.forEach((row, index) => {
|
||||||
|
|
||||||
@@ -502,7 +514,8 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
maxWidth: 240
|
maxWidth: 240
|
||||||
})
|
})
|
||||||
pages[pageCounter - 1].drawText(row.price, {
|
pages[pageCounter - 1].drawText(row.price, {
|
||||||
...getCoordinatesForPDFLib(150, rowHeight, page1),
|
y: getCoordinatesForPDFLib(150, rowHeight, page1).y,
|
||||||
|
x: getRightAlignedX(177, row.price, 10, font, page1),
|
||||||
size: 10,
|
size: 10,
|
||||||
color: rgb(0, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
lineHeight: 10,
|
lineHeight: 10,
|
||||||
@@ -579,18 +592,20 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
|
|
||||||
console.log(invoiceData.rows[index + 1])
|
console.log(invoiceData.rows[index + 1])
|
||||||
|
|
||||||
if (invoiceData.rows[index + 1].mode === 'title') {
|
if (invoiceData.rows[index + 1]?.mode === 'title') {
|
||||||
let transferSumText = `Übertrag: ${invoiceData.total.titleSumsTransfer[Object.keys(invoiceData.total.titleSums)[invoiceData.rows[index + 1].pos - 2]]}`
|
const transferSumText = getTransferSumText(invoiceData.rows[index + 1])
|
||||||
pages[pageCounter - 1].drawText(transferSumText, {
|
if (transferSumText) {
|
||||||
y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y,
|
pages[pageCounter - 1].drawText(transferSumText, {
|
||||||
x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10),
|
y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y,
|
||||||
size: 10,
|
x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10),
|
||||||
color: rgb(0, 0, 0),
|
size: 10,
|
||||||
lineHeight: 10,
|
color: rgb(0, 0, 0),
|
||||||
opacity: 1,
|
lineHeight: 10,
|
||||||
maxWidth: 240,
|
opacity: 1,
|
||||||
font: fontBold
|
maxWidth: 240,
|
||||||
})
|
font: fontBold
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -681,8 +696,10 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
font: fontBold
|
font: fontBold
|
||||||
})
|
})
|
||||||
|
|
||||||
page.drawText("Einheitspreis", {
|
const unitPriceHeader = "Einheitspreis"
|
||||||
...getCoordinatesForPDFLib(150, 22, page1),
|
page.drawText(unitPriceHeader, {
|
||||||
|
y: getCoordinatesForPDFLib(150, 22, page1).y,
|
||||||
|
x: getRightAlignedX(177, unitPriceHeader, 12, fontBold, page1),
|
||||||
size: 12,
|
size: 12,
|
||||||
color: rgb(0, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
lineHeight: 12,
|
lineHeight: 12,
|
||||||
@@ -713,17 +730,19 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
if (index === 0 || pageIndex === 0) {
|
if (index === 0 || pageIndex === 0) {
|
||||||
rowHeight += 3
|
rowHeight += 3
|
||||||
} else {
|
} else {
|
||||||
let transferSumText = `Übertrag: ${invoiceData.total.titleSumsTransfer[Object.keys(invoiceData.total.titleSums)[row.pos - 2]]}`
|
const transferSumText = getTransferSumText(row)
|
||||||
pages[pageCounter - 1].drawText(transferSumText, {
|
if (transferSumText) {
|
||||||
y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y,
|
pages[pageCounter - 1].drawText(transferSumText, {
|
||||||
x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10),
|
y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y,
|
||||||
size: 10,
|
x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10),
|
||||||
color: rgb(0, 0, 0),
|
size: 10,
|
||||||
lineHeight: 10,
|
color: rgb(0, 0, 0),
|
||||||
opacity: 1,
|
lineHeight: 10,
|
||||||
maxWidth: 240,
|
opacity: 1,
|
||||||
font: fontBold
|
maxWidth: 240,
|
||||||
})
|
font: fontBold
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pages[pageCounter - 1].drawLine({
|
pages[pageCounter - 1].drawLine({
|
||||||
start: getCoordinatesForPDFLib(20, rowHeight, page1),
|
start: getCoordinatesForPDFLib(20, rowHeight, page1),
|
||||||
@@ -735,6 +754,10 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
rowHeight += 5
|
rowHeight += 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const titleLevel = Math.min(Math.max(Number(row.titleLevel) || 1, 1), 3)
|
||||||
|
const titleTextSize = titleLevel === 1 ? 12 : titleLevel === 2 ? 11 : 10
|
||||||
|
const titleTextX = 35 + ((titleLevel - 1) * 7)
|
||||||
|
|
||||||
pages[pageCounter - 1].drawText(String(row.pos), {
|
pages[pageCounter - 1].drawText(String(row.pos), {
|
||||||
...getCoordinatesForPDFLib(21, rowHeight, page1),
|
...getCoordinatesForPDFLib(21, rowHeight, page1),
|
||||||
size: 10,
|
size: 10,
|
||||||
@@ -746,16 +769,16 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
})
|
})
|
||||||
|
|
||||||
pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 60).join("\n"), {
|
pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 60).join("\n"), {
|
||||||
...getCoordinatesForPDFLib(35, rowHeight, page1),
|
...getCoordinatesForPDFLib(titleTextX, rowHeight, page1),
|
||||||
size: 12,
|
size: titleTextSize,
|
||||||
color: rgb(0, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
lineHeight: 12,
|
lineHeight: titleTextSize,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
maxWidth: 500,
|
maxWidth: 500,
|
||||||
font: fontBold
|
font: fontBold
|
||||||
})
|
})
|
||||||
|
|
||||||
rowHeight += splitStringBySpace(row.text, 60).length * 4.5
|
rowHeight += splitStringBySpace(row.text, 60).length * (titleLevel === 1 ? 4.5 : 4)
|
||||||
} else if (row.mode === 'text') {
|
} else if (row.mode === 'text') {
|
||||||
if (index === 0 || pageIndex === 0) {
|
if (index === 0 || pageIndex === 0) {
|
||||||
rowHeight += 3
|
rowHeight += 3
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
|
||||||
export const renderAsCurrency = (value: string | number,currencyString = "€") => {
|
export const renderAsCurrency = (value: string | number,currencyString = "€") => {
|
||||||
return `${Number(value).toFixed(2).replace(".",",")} ${currencyString}`
|
const formattedValue = Number(value).toLocaleString("de-DE", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
|
||||||
|
return `${formattedValue} ${currencyString}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const splitStringBySpace = (input:string,maxSplitLength:number,removeLinebreaks = false) => {
|
export const splitStringBySpace = (input:string,maxSplitLength:number,removeLinebreaks = false) => {
|
||||||
@@ -48,4 +53,4 @@ export const splitStringBySpace = (input:string,maxSplitLength:number,removeLine
|
|||||||
})
|
})
|
||||||
|
|
||||||
return returnSplitStrings
|
return returnSplitStrings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -772,6 +772,37 @@ const insertRows = async (client: any, table: string, rows: Record<string, any>[
|
|||||||
return inserted
|
return inserted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const insertAuthTenantUserRows = async (client: any, rows: Record<string, any>[], metadata: TableMetadata) => {
|
||||||
|
if (!rows.length) return 0
|
||||||
|
|
||||||
|
let inserted = 0
|
||||||
|
const availableColumns = new Set(metadata.columns.filter((column) => !metadata.generatedColumns.has(column)))
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!row.tenant_id || !row.user_id) continue
|
||||||
|
|
||||||
|
const existing = await client.query(
|
||||||
|
`select 1 from ${quoteIdent("auth_tenant_users")} where ${quoteIdent("tenant_id")} = $1 and ${quoteIdent("user_id")} = $2 limit 1`,
|
||||||
|
[row.tenant_id, row.user_id]
|
||||||
|
)
|
||||||
|
if (existing.rows.length) continue
|
||||||
|
|
||||||
|
const rowColumns = Object.keys(row).filter((column) => availableColumns.has(column))
|
||||||
|
if (!rowColumns.length) continue
|
||||||
|
|
||||||
|
const placeholders = rowColumns.map((_, index) => `$${index + 1}`).join(", ")
|
||||||
|
const values = rowColumns.map((column) => prepareColumnValue(row[column], metadata.jsonColumns.has(column)))
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`insert into ${quoteIdent("auth_tenant_users")} (${rowColumns.map(quoteIdent).join(", ")}) values (${placeholders})`,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
inserted += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return inserted
|
||||||
|
}
|
||||||
|
|
||||||
const insertIdentityRowsWithRemap = async (
|
const insertIdentityRowsWithRemap = async (
|
||||||
client: any,
|
client: any,
|
||||||
table: string,
|
table: string,
|
||||||
@@ -882,6 +913,7 @@ export const importTenantFullExport = async (
|
|||||||
...Object.keys(exportData.tables).filter((table) => !importOrder.includes(table)).sort(),
|
...Object.keys(exportData.tables).filter((table) => !importOrder.includes(table)).sort(),
|
||||||
].filter((table, index, all) => all.indexOf(table) === index)
|
].filter((table, index, all) => all.indexOf(table) === index)
|
||||||
const specialTables = [
|
const specialTables = [
|
||||||
|
columnsByTable.has("auth_tenant_users") ? "auth_tenant_users" : null,
|
||||||
columnsByTable.has("bankaccounts") ? "bankaccounts" : null,
|
columnsByTable.has("bankaccounts") ? "bankaccounts" : null,
|
||||||
columnsByTable.has("bankstatements") ? "bankstatements" : null,
|
columnsByTable.has("bankstatements") ? "bankstatements" : null,
|
||||||
columnsByTable.has("letterheads") ? "letterheads" : null,
|
columnsByTable.has("letterheads") ? "letterheads" : null,
|
||||||
@@ -905,6 +937,14 @@ export const importTenantFullExport = async (
|
|||||||
await reportProgress("Dateien vorbereitet")
|
await reportProgress("Dateien vorbereitet")
|
||||||
|
|
||||||
const importedTables: { table: string; rows: number }[] = []
|
const importedTables: { table: string; rows: number }[] = []
|
||||||
|
const authTenantUsersMetadata = columnsByTable.get("auth_tenant_users")
|
||||||
|
if (authTenantUsersMetadata) {
|
||||||
|
const count = await insertAuthTenantUserRows(client, exportData.tables.auth_tenant_users || [], authTenantUsersMetadata)
|
||||||
|
importedTables.push({ table: "auth_tenant_users", rows: count })
|
||||||
|
progressDone += 1
|
||||||
|
await reportProgress("Tenant-Zuordnungen importiert")
|
||||||
|
}
|
||||||
|
|
||||||
const bankaccountMetadata = columnsByTable.get("bankaccounts")
|
const bankaccountMetadata = columnsByTable.get("bankaccounts")
|
||||||
if (bankaccountMetadata) {
|
if (bankaccountMetadata) {
|
||||||
const result = await insertIdentityRowsWithRemap(client, "bankaccounts", exportData.tables.bankaccounts || [], bankaccountMetadata)
|
const result = await insertIdentityRowsWithRemap(client, "bankaccounts", exportData.tables.bankaccounts || [], bankaccountMetadata)
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ const taxTypeItems = [
|
|||||||
{ key: '19 UStG', label: '19 UStG Kleinunternehmer' },
|
{ key: '19 UStG', label: '19 UStG Kleinunternehmer' },
|
||||||
{ key: '12.3 UStG', label: '12.3 UStG' }
|
{ key: '12.3 UStG', label: '12.3 UStG' }
|
||||||
]
|
]
|
||||||
|
const titleLevelItems = [
|
||||||
|
{ label: 'Titel Ebene 1', value: 1 },
|
||||||
|
{ label: 'Untertitel Ebene 2', value: 2 },
|
||||||
|
{ label: 'Untertitel Ebene 3', value: 3 }
|
||||||
|
]
|
||||||
const deliveryDateTypeItems = ['Lieferdatum', 'Lieferzeitraum', 'Leistungsdatum', 'Leistungszeitraum', 'Kein Lieferdatum anzeigen']
|
const deliveryDateTypeItems = ['Lieferdatum', 'Lieferzeitraum', 'Leistungsdatum', 'Leistungszeitraum', 'Kein Lieferdatum anzeigen']
|
||||||
const paymentTypeItems = [
|
const paymentTypeItems = [
|
||||||
{ key: 'transfer', label: 'Überweisung' },
|
{ key: 'transfer', label: 'Überweisung' },
|
||||||
@@ -832,6 +837,7 @@ const positionAddOptions = [
|
|||||||
{ mode: 'free', label: 'Freie Position', icon: 'i-heroicons-pencil-square' },
|
{ mode: 'free', label: 'Freie Position', icon: 'i-heroicons-pencil-square' },
|
||||||
{ mode: 'pagebreak', label: 'Seitenumbruch', icon: 'i-heroicons-document-minus' },
|
{ mode: 'pagebreak', label: 'Seitenumbruch', icon: 'i-heroicons-document-minus' },
|
||||||
{ mode: 'title', label: 'Titel', icon: 'i-heroicons-bars-3-bottom-left' },
|
{ mode: 'title', label: 'Titel', icon: 'i-heroicons-bars-3-bottom-left' },
|
||||||
|
{ mode: 'subtitle', label: 'Untertitel', icon: 'i-heroicons-list-bullet' },
|
||||||
{ mode: 'text', label: 'Text', icon: 'i-heroicons-document-text' }
|
{ mode: 'text', label: 'Text', icon: 'i-heroicons-document-text' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -895,10 +901,11 @@ const createPositionRow = (mode, taxPercentage) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "title") {
|
if (mode === "title" || mode === "subtitle") {
|
||||||
return {
|
return {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
mode: "title",
|
mode: "title",
|
||||||
|
titleLevel: mode === "subtitle" ? 2 : 1,
|
||||||
linkedEntitys: []
|
linkedEntitys: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1145,22 +1152,27 @@ const documentTotal = computed(() => {
|
|||||||
|
|
||||||
let titleSums = {}
|
let titleSums = {}
|
||||||
let titleSumsTransfer = {}
|
let titleSumsTransfer = {}
|
||||||
let lastTitle = ""
|
let activeTitleKeys = []
|
||||||
let transferCounter = 0
|
let transferCounter = 0
|
||||||
|
|
||||||
itemInfo.value.rows.forEach(row => {
|
itemInfo.value.rows.forEach(row => {
|
||||||
if (row.mode === 'title') {
|
if (row.mode === 'title') {
|
||||||
let title = `${row.pos} - ${row.text}`
|
let title = `${row.pos} - ${row.text}`
|
||||||
|
const titleLevel = Math.min(Math.max(Number(row.titleLevel) || 1, 1), 3)
|
||||||
titleSums[title] = 0
|
titleSums[title] = 0
|
||||||
lastTitle = title
|
if (activeTitleKeys.some(Boolean)) {
|
||||||
|
titleSumsTransfer[title] = transferCounter
|
||||||
|
}
|
||||||
|
activeTitleKeys = activeTitleKeys.slice(0, titleLevel - 1)
|
||||||
|
activeTitleKeys[titleLevel - 1] = title
|
||||||
|
activeTitleKeys = activeTitleKeys.slice(0, titleLevel)
|
||||||
|
} else if (!['pagebreak', 'text'].includes(row.mode) && activeTitleKeys.some(Boolean) && !row.optional && !row.alternative) {
|
||||||
|
const rowSum = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100))
|
||||||
|
|
||||||
//Übertrag berechnen
|
activeTitleKeys.forEach(titleKey => {
|
||||||
titleSumsTransfer[Object.keys(titleSums)[row.pos - 2]] = transferCounter
|
titleSums[titleKey] = Number(titleSums[titleKey]) + rowSum
|
||||||
|
})
|
||||||
|
transferCounter += rowSum
|
||||||
} else if (!['pagebreak', 'text'].includes(row.mode) && lastTitle !== "" && !row.optional && !row.alternative) {
|
|
||||||
titleSums[lastTitle] = Number(titleSums[lastTitle]) + Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100))
|
|
||||||
transferCounter += Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100))
|
|
||||||
console.log(transferCounter)
|
console.log(transferCounter)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1397,11 +1409,11 @@ const getDocumentData = async () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
rowAmount: `${getRowAmount(row)} €`,
|
rowAmount: renderCurrency(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100)),
|
||||||
quantity: String(row.quantity).replace(".", ","),
|
quantity: String(row.quantity).replace(".", ","),
|
||||||
unit: unit.short,
|
unit: unit.short,
|
||||||
pos: String(row.pos),
|
pos: String(row.pos),
|
||||||
price: `${String(row.price.toFixed(2)).replace(".", ",")} €`,
|
price: renderCurrency(row.price),
|
||||||
discountText: `(Rabatt: ${row.discountPercent} %)`
|
discountText: `(Rabatt: ${row.discountPercent} %)`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1610,16 +1622,33 @@ const onChangeTab = (index) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setPosNumbers = () => {
|
const setPosNumbers = () => {
|
||||||
let mainIndex = 1
|
let titleCounters = [0, 0, 0]
|
||||||
let subIndex = 1
|
let positionIndex = 1
|
||||||
|
let currentTitlePath = []
|
||||||
|
const hasTitles = itemInfo.value.rows.some(row => row.mode === "title")
|
||||||
|
|
||||||
let rows = itemInfo.value.rows.map(row => {
|
let rows = itemInfo.value.rows.map(row => {
|
||||||
if (row.mode === 'title') {
|
if (row.mode === 'title') {
|
||||||
row.pos = mainIndex
|
const titleLevel = Math.min(Math.max(Number(row.titleLevel) || 1, 1), titleCounters.length)
|
||||||
mainIndex += 1
|
row.titleLevel = titleLevel
|
||||||
subIndex = 1
|
|
||||||
|
for (let index = 0; index < titleLevel - 1; index += 1) {
|
||||||
|
if (titleCounters[index] === 0) {
|
||||||
|
titleCounters[index] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
titleCounters[titleLevel - 1] += 1
|
||||||
|
for (let index = titleLevel; index < titleCounters.length; index += 1) {
|
||||||
|
titleCounters[index] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTitlePath = titleCounters.slice(0, titleLevel)
|
||||||
|
row.pos = currentTitlePath.join(".")
|
||||||
|
positionIndex = 1
|
||||||
} else if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
|
} else if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
|
||||||
row.pos = itemInfo.value.rows.filter(i => i.mode === "title").length === 0 ? `${subIndex}` : `${mainIndex - 1}.${subIndex}`
|
row.pos = hasTitles && currentTitlePath.length > 0 ? `${currentTitlePath.join(".")}.${positionIndex}` : `${positionIndex}`
|
||||||
subIndex += 1
|
positionIndex += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!row.id) {
|
if (!row.id) {
|
||||||
@@ -3471,12 +3500,22 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
|||||||
v-if="row.mode === 'title'"
|
v-if="row.mode === 'title'"
|
||||||
:colspan="getTitlePositionColspan()"
|
:colspan="getTitlePositionColspan()"
|
||||||
>
|
>
|
||||||
<UInput
|
<InputGroup class="w-full">
|
||||||
:disabled="itemInfo.type === 'cancellationInvoices'"
|
<USelect
|
||||||
v-model="row.text"
|
:disabled="itemInfo.type === 'cancellationInvoices'"
|
||||||
placeholder="Titel"
|
v-model="row.titleLevel"
|
||||||
class="w-full"
|
:items="titleLevelItems"
|
||||||
/>
|
value-key="value"
|
||||||
|
class="w-44 shrink-0"
|
||||||
|
@update:model-value="setPosNumbers"
|
||||||
|
/>
|
||||||
|
<UInput
|
||||||
|
:disabled="itemInfo.type === 'cancellationInvoices'"
|
||||||
|
v-model="row.text"
|
||||||
|
placeholder="Titel"
|
||||||
|
class="w-full min-w-0"
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<UDropdownMenu
|
<UDropdownMenu
|
||||||
|
|||||||
Reference in New Issue
Block a user