Compare commits
13 Commits
4e3f1e0c15
...
d8273e794d
| Author | SHA1 | Date | |
|---|---|---|---|
| d8273e794d | |||
| 1711f975bb | |||
| 31ef408b66 | |||
| 5f66f81ade | |||
| b3067e0e00 | |||
| e30722995e | |||
| bbef553e6b | |||
| 53632d25ac | |||
| 2eaa49bf05 | |||
| 14026181c2 | |||
| d67da7e02a | |||
| 3b3a879b02 | |||
| 365cec206d |
@@ -12,6 +12,7 @@
|
||||
"start": "node dist/src/index.js",
|
||||
"schema:index": "ts-node scripts/generate-schema-index.ts",
|
||||
"bankcodes:update": "tsx scripts/generate-de-bank-codes.ts",
|
||||
"invoices:delete-range": "tsx scripts/delete-createddocument-range.ts",
|
||||
"members:import:csv": "tsx scripts/import-members-csv.ts",
|
||||
"profiles:import:mitarbeiterliste": "tsx scripts/import-mitarbeiterliste.ts",
|
||||
"accounts:import:skr42": "ts-node scripts/import-skr42-accounts.ts"
|
||||
|
||||
359
backend/scripts/delete-createddocument-range.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import "dotenv/config"
|
||||
import { Pool, type PoolClient } from "pg"
|
||||
|
||||
import { loadSecrets, secrets } from "../src/utils/secrets"
|
||||
|
||||
type Args = {
|
||||
tenantId?: number
|
||||
fromNumber?: string
|
||||
toNumber?: string
|
||||
fromId?: number
|
||||
toId?: number
|
||||
types: string[]
|
||||
execute: boolean
|
||||
unlinkRelated: boolean
|
||||
}
|
||||
|
||||
type TargetDocument = {
|
||||
id: number
|
||||
documentNumber: string | null
|
||||
type: string
|
||||
title: string | null
|
||||
state: string
|
||||
}
|
||||
|
||||
const DEFAULT_TYPES = ["invoices"]
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
Löscht einen Bereich von Ausgangsbelegen und deren direkt verknüpfte DB-Dokumente.
|
||||
|
||||
Standardmäßig läuft das Skript als Dry-Run. Erst --execute löscht wirklich.
|
||||
|
||||
Beispiele:
|
||||
npm run invoices:delete-range -- --tenant 41 --from RE-2026-001 --to RE-2026-010
|
||||
npm run invoices:delete-range -- --tenant 41 --from-id 1200 --to-id 1210 --execute
|
||||
npm run invoices:delete-range -- --tenant 41 --from RE-2026-001 --to RE-2026-010 --type invoices --type advanceInvoices --execute
|
||||
|
||||
Optionen:
|
||||
--tenant <id> Pflicht: Tenant-ID
|
||||
--from <nummer> Erste Rechnungsnummer im Bereich
|
||||
--to <nummer> Letzte Rechnungsnummer im Bereich
|
||||
--from-id <id> Erste Beleg-ID im Bereich
|
||||
--to-id <id> Letzte Beleg-ID im Bereich
|
||||
--type <type> Belegtyp, mehrfach möglich. Standard: invoices
|
||||
--execute Wirklich löschen. Ohne diese Option nur Vorschau.
|
||||
--unlink-related Externe Belege, die auf Zielbelege zeigen, werden entkoppelt.
|
||||
--help Hilfe anzeigen
|
||||
`)
|
||||
}
|
||||
|
||||
function readValue(args: string[], index: number, flag: string) {
|
||||
const value = args[index + 1]
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error(`Für ${flag} fehlt ein Wert.`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function readNumber(args: string[], index: number, flag: string) {
|
||||
const value = Number(readValue(args, index, flag))
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`${flag} muss eine positive Ganzzahl sein.`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): Args {
|
||||
const parsed: Args = {
|
||||
types: [],
|
||||
execute: false,
|
||||
unlinkRelated: false,
|
||||
}
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i]
|
||||
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
printUsage()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (arg === "--tenant") {
|
||||
parsed.tenantId = readNumber(argv, i, arg)
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === "--from") {
|
||||
parsed.fromNumber = readValue(argv, i, arg)
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === "--to") {
|
||||
parsed.toNumber = readValue(argv, i, arg)
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === "--from-id") {
|
||||
parsed.fromId = readNumber(argv, i, arg)
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === "--to-id") {
|
||||
parsed.toId = readNumber(argv, i, arg)
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === "--type") {
|
||||
parsed.types.push(readValue(argv, i, arg))
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === "--execute") {
|
||||
parsed.execute = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === "--unlink-related") {
|
||||
parsed.unlinkRelated = true
|
||||
continue
|
||||
}
|
||||
|
||||
throw new Error(`Unbekannte Option: ${arg}`)
|
||||
}
|
||||
|
||||
if (!parsed.tenantId) throw new Error("--tenant ist Pflicht.")
|
||||
|
||||
const hasNumberRange = parsed.fromNumber !== undefined || parsed.toNumber !== undefined
|
||||
const hasIdRange = parsed.fromId !== undefined || parsed.toId !== undefined
|
||||
|
||||
if (hasNumberRange && hasIdRange) {
|
||||
throw new Error("Bitte entweder Rechnungsnummernbereich oder ID-Bereich verwenden, nicht beides.")
|
||||
}
|
||||
|
||||
if (hasNumberRange && (!parsed.fromNumber || !parsed.toNumber)) {
|
||||
throw new Error("Für einen Rechnungsnummernbereich sind --from und --to Pflicht.")
|
||||
}
|
||||
|
||||
if (hasIdRange && (!parsed.fromId || !parsed.toId)) {
|
||||
throw new Error("Für einen ID-Bereich sind --from-id und --to-id Pflicht.")
|
||||
}
|
||||
|
||||
if (!hasNumberRange && !hasIdRange) {
|
||||
throw new Error("Bitte einen Bereich mit --from/--to oder --from-id/--to-id angeben.")
|
||||
}
|
||||
|
||||
if (parsed.fromNumber && parsed.toNumber && parsed.fromNumber > parsed.toNumber) {
|
||||
throw new Error("--from darf bei Textvergleich nicht größer als --to sein.")
|
||||
}
|
||||
|
||||
if (parsed.fromId && parsed.toId && parsed.fromId > parsed.toId) {
|
||||
throw new Error("--from-id darf nicht größer als --to-id sein.")
|
||||
}
|
||||
|
||||
if (parsed.types.length === 0) parsed.types = DEFAULT_TYPES
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
async function loadConnectionString() {
|
||||
if (process.env.DATABASE_URL) return process.env.DATABASE_URL
|
||||
|
||||
await loadSecrets()
|
||||
if (secrets.DATABASE_URL) return secrets.DATABASE_URL
|
||||
|
||||
throw new Error("DATABASE_URL ist nicht konfiguriert.")
|
||||
}
|
||||
|
||||
function buildTargetWhere(args: Args, startParam = 1) {
|
||||
const values: unknown[] = [args.tenantId, args.types]
|
||||
const clauses = [
|
||||
`"tenant" = $${startParam}`,
|
||||
`"type" = ANY($${startParam + 1}::text[])`,
|
||||
]
|
||||
|
||||
if (args.fromNumber && args.toNumber) {
|
||||
values.push(args.fromNumber, args.toNumber)
|
||||
clauses.push(`"documentNumber" >= $${startParam + 2}`)
|
||||
clauses.push(`"documentNumber" <= $${startParam + 3}`)
|
||||
} else {
|
||||
values.push(args.fromId, args.toId)
|
||||
clauses.push(`"id" >= $${startParam + 2}`)
|
||||
clauses.push(`"id" <= $${startParam + 3}`)
|
||||
}
|
||||
|
||||
return {
|
||||
values,
|
||||
whereSql: clauses.join(" AND "),
|
||||
}
|
||||
}
|
||||
|
||||
async function selectTargetDocuments(client: PoolClient, args: Args) {
|
||||
const target = buildTargetWhere(args)
|
||||
const result = await client.query<TargetDocument>(
|
||||
`
|
||||
SELECT "id", "documentNumber", "type", "title", "state"
|
||||
FROM "createddocuments"
|
||||
WHERE ${target.whereSql}
|
||||
ORDER BY "id"
|
||||
`,
|
||||
target.values
|
||||
)
|
||||
|
||||
return result.rows
|
||||
}
|
||||
|
||||
async function countByTarget(client: PoolClient, table: string, column: string, targetIds: number[]) {
|
||||
const result = await client.query<{ count: string }>(
|
||||
`SELECT COUNT(*)::int AS "count" FROM "${table}" WHERE "${column}" = ANY($1::bigint[])`,
|
||||
[targetIds]
|
||||
)
|
||||
|
||||
return Number(result.rows[0]?.count || 0)
|
||||
}
|
||||
|
||||
async function findExternalLinkedDocuments(client: PoolClient, targetIds: number[]) {
|
||||
const result = await client.query<TargetDocument>(
|
||||
`
|
||||
SELECT "id", "documentNumber", "type", "title", "state"
|
||||
FROM "createddocuments"
|
||||
WHERE "linkedDocument" = ANY($1::bigint[])
|
||||
AND NOT ("id" = ANY($1::bigint[]))
|
||||
ORDER BY "id"
|
||||
`,
|
||||
[targetIds]
|
||||
)
|
||||
|
||||
return result.rows
|
||||
}
|
||||
|
||||
async function deleteByTarget(client: PoolClient, table: string, column: string, targetIds: number[]) {
|
||||
const result = await client.query(
|
||||
`DELETE FROM "${table}" WHERE "${column}" = ANY($1::bigint[])`,
|
||||
[targetIds]
|
||||
)
|
||||
|
||||
return result.rowCount || 0
|
||||
}
|
||||
|
||||
function printTargets(targets: TargetDocument[]) {
|
||||
console.table(
|
||||
targets.map((item) => ({
|
||||
id: item.id,
|
||||
nummer: item.documentNumber,
|
||||
typ: item.type,
|
||||
status: item.state,
|
||||
titel: item.title,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
const connectionString = await loadConnectionString()
|
||||
const pool = new Pool({ connectionString, max: 1 })
|
||||
const client = await pool.connect()
|
||||
|
||||
try {
|
||||
await client.query("BEGIN")
|
||||
|
||||
const targets = await selectTargetDocuments(client, args)
|
||||
if (targets.length === 0) {
|
||||
console.log("Keine passenden Rechnungen gefunden.")
|
||||
await client.query("ROLLBACK")
|
||||
return
|
||||
}
|
||||
|
||||
const targetIds = targets.map((item) => item.id)
|
||||
const [fileCount, historyCount, allocationCount, externalLinks] = await Promise.all([
|
||||
countByTarget(client, "files", "createddocument", targetIds),
|
||||
countByTarget(client, "historyitems", "createddocument", targetIds),
|
||||
countByTarget(client, "statementallocations", "cd_id", targetIds),
|
||||
findExternalLinkedDocuments(client, targetIds),
|
||||
])
|
||||
|
||||
console.log(`Tenant: ${args.tenantId}`)
|
||||
console.log(`Belegtypen: ${args.types.join(", ")}`)
|
||||
console.log(`Modus: ${args.execute ? "Löschen" : "Dry-Run"}`)
|
||||
console.log("")
|
||||
printTargets(targets)
|
||||
console.log("")
|
||||
console.log("Betroffene verknüpfte DB-Zeilen:")
|
||||
console.log(`- Dateien: ${fileCount}`)
|
||||
console.log(`- Historie: ${historyCount}`)
|
||||
console.log(`- Bank-Zuordnungen: ${allocationCount}`)
|
||||
|
||||
if (externalLinks.length > 0 && !args.unlinkRelated) {
|
||||
console.log("")
|
||||
console.log("Abbruch: Andere Belege verweisen auf Zielbelege.")
|
||||
console.log("Nutze --unlink-related, wenn diese Verknüpfungen vor dem Löschen entfernt werden sollen.")
|
||||
printTargets(externalLinks)
|
||||
await client.query("ROLLBACK")
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
if (!args.execute) {
|
||||
console.log("")
|
||||
console.log("Dry-Run abgeschlossen. Es wurde nichts gelöscht. Zum Löschen --execute ergänzen.")
|
||||
await client.query("ROLLBACK")
|
||||
return
|
||||
}
|
||||
|
||||
if (externalLinks.length > 0) {
|
||||
await client.query(
|
||||
`
|
||||
UPDATE "createddocuments"
|
||||
SET "linkedDocument" = NULL
|
||||
WHERE "linkedDocument" = ANY($1::bigint[])
|
||||
AND NOT ("id" = ANY($1::bigint[]))
|
||||
`,
|
||||
[targetIds]
|
||||
)
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`UPDATE "createddocuments" SET "linkedDocument" = NULL WHERE "id" = ANY($1::bigint[])`,
|
||||
[targetIds]
|
||||
)
|
||||
|
||||
const deletedFiles = await deleteByTarget(client, "files", "createddocument", targetIds)
|
||||
const deletedHistory = await deleteByTarget(client, "historyitems", "createddocument", targetIds)
|
||||
const deletedAllocations = await deleteByTarget(client, "statementallocations", "cd_id", targetIds)
|
||||
|
||||
const deletedDocuments = await client.query(
|
||||
`DELETE FROM "createddocuments" WHERE "id" = ANY($1::bigint[])`,
|
||||
[targetIds]
|
||||
)
|
||||
|
||||
await client.query("COMMIT")
|
||||
|
||||
console.log("")
|
||||
console.log("Löschung abgeschlossen:")
|
||||
console.log(`- Rechnungen: ${deletedDocuments.rowCount || 0}`)
|
||||
console.log(`- Dateien: ${deletedFiles}`)
|
||||
console.log(`- Historie: ${deletedHistory}`)
|
||||
console.log(`- Bank-Zuordnungen: ${deletedAllocations}`)
|
||||
if (externalLinks.length > 0) {
|
||||
console.log(`- Entkoppelte externe Belege: ${externalLinks.length}`)
|
||||
}
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK")
|
||||
throw error
|
||||
} finally {
|
||||
client.release()
|
||||
await pool.end()
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Fehler beim Löschen der Rechnungen:")
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -351,6 +351,10 @@ const normalizeCreatedDocumentRow = (row) => {
|
||||
const normalizeCreatedDocumentRows = (rows) => Array.isArray(rows)
|
||||
? rows.map((row) => normalizeCreatedDocumentRow(row))
|
||||
: []
|
||||
const isImportedAdvanceInvoiceRow = (row) => {
|
||||
return Array.isArray(row.linkedEntitys)
|
||||
&& row.linkedEntitys.some(i => i.type === "createddocuments" && i.subtype === "advanceInvoices")
|
||||
}
|
||||
const setupPage = async () => {
|
||||
|
||||
await setupData()
|
||||
@@ -507,6 +511,8 @@ const setupPage = async () => {
|
||||
})
|
||||
|
||||
for await (const doc of linkedDocuments.filter(i => i.type === "advanceInvoices")) {
|
||||
const advanceInvoiceNet = useSum().getCreatedDocumentSumDetailed(doc).totalNet * -1
|
||||
|
||||
itemInfo.value.rows.push({
|
||||
mode: "free",
|
||||
text: `Abschlagsrechnung ${doc.documentNumber}`,
|
||||
@@ -514,7 +520,8 @@ const setupPage = async () => {
|
||||
taxPercent: 19, // TODO TAX PERCENTAGE
|
||||
discountPercent: 0,
|
||||
unit: 10,
|
||||
inputPrice: useSum().getCreatedDocumentSumDetailed(doc).totalNet * -1,
|
||||
inputPrice: advanceInvoiceNet,
|
||||
price: advanceInvoiceNet,
|
||||
linkedEntitys: [
|
||||
{
|
||||
type: "createddocuments",
|
||||
@@ -819,21 +826,18 @@ const getRowAmountUndiscounted = (row) => {
|
||||
return String(Number(Number(row.quantity) * Number(row.price)).toFixed(2)).replace('.', ',')
|
||||
}
|
||||
|
||||
const addPosition = (mode) => {
|
||||
|
||||
let lastId = 0
|
||||
itemInfo.value.rows.forEach(row => {
|
||||
if (row.id > lastId) lastId = row.id
|
||||
})
|
||||
|
||||
let taxPercentage = 19
|
||||
|
||||
if (['13b UStG', '19 UStG', '12.3 UStG'].includes(itemInfo.value.taxType)) {
|
||||
taxPercentage = 0
|
||||
}
|
||||
const positionAddOptions = [
|
||||
{ mode: 'service', label: 'Leistung', icon: 'i-heroicons-wrench-screwdriver' },
|
||||
{ mode: 'normal', label: 'Artikel', icon: 'i-heroicons-cube' },
|
||||
{ mode: 'free', label: 'Freie Position', icon: 'i-heroicons-pencil-square' },
|
||||
{ mode: 'pagebreak', label: 'Seitenumbruch', icon: 'i-heroicons-document-minus' },
|
||||
{ mode: 'title', label: 'Titel', icon: 'i-heroicons-bars-3-bottom-left' },
|
||||
{ mode: 'text', label: 'Text', icon: 'i-heroicons-document-text' }
|
||||
]
|
||||
|
||||
const createPositionRow = (mode, taxPercentage) => {
|
||||
if (mode === 'free') {
|
||||
let rowData = {
|
||||
const rowData = {
|
||||
id: uuidv4(),
|
||||
mode: "free",
|
||||
text: "",
|
||||
@@ -848,10 +852,11 @@ const addPosition = (mode) => {
|
||||
linkedEntitys: []
|
||||
}
|
||||
|
||||
itemInfo.value.rows.push({...rowData, ...auth.activeTenantData.extraModules.includes("agriculture") ? {agriculture: {}} : {}})
|
||||
return {...rowData, ...auth.activeTenantData.extraModules.includes("agriculture") ? {agriculture: {}} : {}}
|
||||
}
|
||||
|
||||
} else if (mode === 'normal') {
|
||||
itemInfo.value.rows.push({
|
||||
if (mode === 'normal') {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
mode: "normal",
|
||||
quantity: 1,
|
||||
@@ -862,9 +867,11 @@ const addPosition = (mode) => {
|
||||
unit: 1,
|
||||
costCentre: itemInfo.value.costcentre,
|
||||
linkedEntitys: []
|
||||
})
|
||||
} else if (mode === 'service') {
|
||||
let rowData = {
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'service') {
|
||||
const rowData = {
|
||||
id: uuidv4(),
|
||||
mode: "service",
|
||||
quantity: 1,
|
||||
@@ -877,26 +884,63 @@ const addPosition = (mode) => {
|
||||
linkedEntitys: []
|
||||
}
|
||||
|
||||
//Push Agriculture Holder only if Module is activated
|
||||
itemInfo.value.rows.push({...rowData, ...auth.activeTenantData.extraModules.includes("agriculture") ? {agriculture: {}} : {}})
|
||||
} else if (mode === "pagebreak") {
|
||||
itemInfo.value.rows.push({
|
||||
return {...rowData, ...auth.activeTenantData.extraModules.includes("agriculture") ? {agriculture: {}} : {}}
|
||||
}
|
||||
|
||||
if (mode === "pagebreak") {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
mode: "pagebreak",
|
||||
linkedEntitys: []
|
||||
})
|
||||
} else if (mode === "title") {
|
||||
itemInfo.value.rows.push({
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "title") {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
mode: "title",
|
||||
linkedEntitys: []
|
||||
})
|
||||
} else if (mode === "text") {
|
||||
itemInfo.value.rows.push({
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "text") {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
mode: "text",
|
||||
linkedEntitys: []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const getPositionAddMenuItems = (row) => [
|
||||
positionAddOptions.map(option => ({
|
||||
...option,
|
||||
onSelect: () => addPosition(option.mode, row?.id)
|
||||
}))
|
||||
]
|
||||
|
||||
const getFullWidthPositionColspan = () => deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type) ? 5 : 8
|
||||
|
||||
const getTitlePositionColspan = () => deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type) ? 4 : 7
|
||||
|
||||
const addPosition = (mode, afterRowId = null) => {
|
||||
let taxPercentage = 19
|
||||
|
||||
if (['13b UStG', '19 UStG', '12.3 UStG'].includes(itemInfo.value.taxType)) {
|
||||
taxPercentage = 0
|
||||
}
|
||||
|
||||
const newRow = createPositionRow(mode, taxPercentage)
|
||||
if (!newRow) return
|
||||
|
||||
const insertAfterIndex = afterRowId ? itemInfo.value.rows.findIndex(row => row.id === afterRowId) : -1
|
||||
|
||||
if (insertAfterIndex >= 0) {
|
||||
itemInfo.value.rows.splice(insertAfterIndex + 1, 0, newRow)
|
||||
} else {
|
||||
itemInfo.value.rows.push(newRow)
|
||||
}
|
||||
|
||||
setPosNumbers()
|
||||
@@ -1799,15 +1843,15 @@ const checkCompatibilityWithInputPrice = () => {
|
||||
|
||||
const updateCustomSurcharge = () => {
|
||||
itemInfo.value.rows.forEach(row => {
|
||||
if (!["pagebreak", "title", "text"].includes(row.mode) /*&& !row.linkedEntitys.find(i => i.type === "createddocuments" && i.subtype === "advanceInvoices")*/) {
|
||||
if (!["pagebreak", "title", "text"].includes(row.mode) && isImportedAdvanceInvoiceRow(row)) {
|
||||
row.price = Number(row.inputPrice || 0)
|
||||
} else if (!["pagebreak", "title", "text"].includes(row.mode)) {
|
||||
//setRowData(row)
|
||||
|
||||
row.price = Number((row.inputPrice * (1 + itemInfo.value.customSurchargePercentage / 100)).toFixed(2))
|
||||
|
||||
|
||||
}/* else if(!["pagebreak","title","text"].includes(row.mode) /!*&& row.linkedEntitys.find(i => i.type === "createddocuments" && i.subtype === "advanceInvoices")*!/) {
|
||||
row.price = row.inputPrice
|
||||
}*/
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2790,6 +2834,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Rabatt</th>-->
|
||||
<th class="pl-2"></th>
|
||||
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Gesamt</th>
|
||||
<th class="pl-2"></th>
|
||||
<th class="pl-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable
|
||||
@@ -2810,13 +2856,13 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
</td>
|
||||
<td
|
||||
v-if="row.mode === 'pagebreak'"
|
||||
colspan="8"
|
||||
:colspan="getFullWidthPositionColspan()"
|
||||
>
|
||||
<USeparator/>
|
||||
</td>
|
||||
<td
|
||||
v-if="row.mode === 'text'"
|
||||
colspan="8"
|
||||
:colspan="getFullWidthPositionColspan()"
|
||||
>
|
||||
<!-- <UInput
|
||||
v-model="row.text"
|
||||
@@ -3289,7 +3335,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
</InputGroup>
|
||||
|
||||
<UAlert
|
||||
v-if="row.linkedEntitys && row.linkedEntitys.find(i => i.type === 'createddocuments' && i.subtype === 'advanceInvoices')"
|
||||
v-if="isImportedAdvanceInvoiceRow(row)"
|
||||
title="Berechnung des Individuellen Aufschlags für die Zeile gesperrt"
|
||||
color="orange"
|
||||
variant="subtle"
|
||||
@@ -3423,7 +3469,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
</td>
|
||||
<td
|
||||
v-if="row.mode === 'title'"
|
||||
colspan="7"
|
||||
:colspan="getTitlePositionColspan()"
|
||||
>
|
||||
<UInput
|
||||
:disabled="itemInfo.type === 'cancellationInvoices'"
|
||||
@@ -3432,6 +3478,19 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
class="w-full"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<UDropdownMenu
|
||||
:items="getPositionAddMenuItems(row)"
|
||||
:content="{ align: 'end' }"
|
||||
>
|
||||
<UButton
|
||||
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
icon="i-heroicons-plus"
|
||||
/>
|
||||
</UDropdownMenu>
|
||||
</td>
|
||||
<td>
|
||||
<UButton
|
||||
:disabled="itemInfo.type === 'cancellationInvoices'"
|
||||
@@ -3455,46 +3514,13 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
|
||||
<InputGroup>
|
||||
<UButton
|
||||
@click="addPosition('service')"
|
||||
v-for="option in positionAddOptions"
|
||||
:key="option.mode"
|
||||
@click="addPosition(option.mode)"
|
||||
class="mt-3"
|
||||
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
|
||||
>
|
||||
+ Leistung
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="addPosition('normal')"
|
||||
class="mt-3"
|
||||
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
|
||||
>
|
||||
+ Artikel
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="addPosition('free')"
|
||||
class="mt-3"
|
||||
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
|
||||
>
|
||||
+ Freie Position
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="addPosition('pagebreak')"
|
||||
class="mt-3"
|
||||
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
|
||||
>
|
||||
+ Seitenumbruch
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="addPosition('title')"
|
||||
class="mt-3"
|
||||
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
|
||||
>
|
||||
+ Titel
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="addPosition('text')"
|
||||
class="mt-3"
|
||||
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
|
||||
>
|
||||
+ Text
|
||||
+ {{ option.label }}
|
||||
</UButton>
|
||||
</InputGroup>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "software.federspiel.fedeo",
|
||||
"buildNumber": "5",
|
||||
"buildNumber": "7",
|
||||
"infoPlist": {
|
||||
"NSCameraUsageDescription": "Die Kamera wird benötigt, um Fotos zu Projekten und Objekten als Dokumente hochzuladen.",
|
||||
"NSPhotoLibraryUsageDescription": "Der Zugriff auf Fotos wird benötigt, um Bilder als Dokumente hochzuladen.",
|
||||
@@ -28,9 +28,8 @@
|
||||
"android.permission.ACCESS_FINE_LOCATION"
|
||||
],
|
||||
"adaptiveIcon": {
|
||||
"backgroundColor": "#69c350",
|
||||
"backgroundColor": "#ffffff",
|
||||
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
||||
"backgroundImage": "./assets/images/android-icon-background.png",
|
||||
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
@@ -50,7 +49,8 @@
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff",
|
||||
"dark": {
|
||||
"backgroundColor": "#0f1f0d"
|
||||
"image": "./assets/images/splash-icon-dark.png",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Redirect } from 'expo-router';
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
|
||||
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
|
||||
export default function IndexScreen() {
|
||||
const { isBootstrapping, token, requiresTenantSelection } = useAuth();
|
||||
const { isBootstrapping, bootstrapError, token, requiresTenantSelection, retryBootstrap, resetLocalSession } = useAuth();
|
||||
|
||||
if (isBootstrapping) {
|
||||
return (
|
||||
@@ -15,6 +15,23 @@ export default function IndexScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
if (bootstrapError) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<Text style={styles.title}>Session konnte nicht gestartet werden</Text>
|
||||
<Text style={styles.copy}>{bootstrapError}</Text>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={styles.primaryButton} onPress={retryBootstrap}>
|
||||
<Text style={styles.primaryButtonText}>Erneut versuchen</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.secondaryButton} onPress={resetLocalSession}>
|
||||
<Text style={styles.secondaryButtonText}>Lokale Session löschen</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return <Redirect href="/login" />;
|
||||
}
|
||||
@@ -37,5 +54,43 @@ const styles = StyleSheet.create({
|
||||
copy: {
|
||||
fontSize: 14,
|
||||
color: '#4b5563',
|
||||
textAlign: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
textAlign: 'center',
|
||||
},
|
||||
actions: {
|
||||
width: '100%',
|
||||
gap: 10,
|
||||
marginTop: 4,
|
||||
},
|
||||
primaryButton: {
|
||||
minHeight: 44,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#69c350',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
primaryButtonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '700',
|
||||
},
|
||||
secondaryButton: {
|
||||
minHeight: 44,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
backgroundColor: '#ffffff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
secondaryButtonText: {
|
||||
color: '#374151',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 52 KiB |
BIN
mobile/assets/images/splash-icon-dark.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 39 KiB |
980
mobile/package-lock.json
generated
@@ -25,17 +25,18 @@
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"expo": "~54.0.34",
|
||||
"expo": "~54.0.35",
|
||||
"expo-camera": "~17.0.10",
|
||||
"expo-constants": "~18.0.13",
|
||||
"expo-document-picker": "^14.0.8",
|
||||
"expo-font": "~14.0.11",
|
||||
"expo-file-system": "~19.0.23",
|
||||
"expo-font": "~14.0.12",
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-image-picker": "~17.0.11",
|
||||
"expo-linking": "~8.0.12",
|
||||
"expo-notifications": "~0.32.17",
|
||||
"expo-router": "~6.0.23",
|
||||
"expo-router": "~6.0.24",
|
||||
"expo-secure-store": "^15.0.8",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
|
||||
@@ -196,6 +196,7 @@ type RequestOptions = {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
token?: string | null;
|
||||
body?: unknown;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type MatrixStatus = {
|
||||
@@ -314,17 +315,46 @@ async function parseJson(response: Response): Promise<unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 15000;
|
||||
|
||||
function createTimeoutSignal(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS): { signal: AbortSignal; cleanup: () => void } {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
return {
|
||||
signal: controller.signal,
|
||||
cleanup: () => clearTimeout(timeout),
|
||||
};
|
||||
}
|
||||
|
||||
function isAbortError(error: unknown): boolean {
|
||||
return error instanceof Error && error.name === 'AbortError';
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const hasJsonBody = options.body !== undefined;
|
||||
const response = await fetch(buildUrl(path), {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(hasJsonBody ? { 'Content-Type': 'application/json' } : {}),
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
},
|
||||
body: hasJsonBody ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
const { signal, cleanup } = createTimeoutSignal(options.timeoutMs);
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(buildUrl(path), {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(hasJsonBody ? { 'Content-Type': 'application/json' } : {}),
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
},
|
||||
body: hasJsonBody ? JSON.stringify(options.body) : undefined,
|
||||
signal,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
throw new Error(`Zeitüberschreitung beim Verbinden mit dem FEDEO-Server (${path}).`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
const payload = await parseJson(response);
|
||||
|
||||
@@ -340,14 +370,27 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
||||
}
|
||||
|
||||
async function apiFormRequest<T>(path: string, token: string, formData: FormData): Promise<T> {
|
||||
const response = await fetch(buildUrl(path), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
const { signal, cleanup } = createTimeoutSignal();
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(buildUrl(path), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
signal,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
throw new Error(`Zeitüberschreitung beim Hochladen zum FEDEO-Server (${path}).`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
const payload = await parseJson(response);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { fetchMe, loginWithEmailPassword, MeResponse, switchTenantRequest, Tenant } from '@/src/lib/api';
|
||||
import { hydrateApiBaseUrl } from '@/src/lib/server-config';
|
||||
@@ -8,6 +8,7 @@ export type AuthUser = MeResponse['user'];
|
||||
|
||||
type AuthContextValue = {
|
||||
isBootstrapping: boolean;
|
||||
bootstrapError: string | null;
|
||||
token: string | null;
|
||||
user: AuthUser | null;
|
||||
tenants: Tenant[];
|
||||
@@ -20,9 +21,12 @@ type AuthContextValue = {
|
||||
logout: () => Promise<void>;
|
||||
refreshUser: () => Promise<void>;
|
||||
switchTenant: (tenantId: number) => Promise<void>;
|
||||
retryBootstrap: () => Promise<void>;
|
||||
resetLocalSession: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||
const BOOTSTRAP_TIMEOUT_MS = 20000;
|
||||
|
||||
function normalizeTenantId(value: number | string | null | undefined): number | null {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
@@ -33,14 +37,27 @@ function normalizeTenantId(value: number | string | null | undefined): number |
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
|
||||
|
||||
promise
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.finally(() => clearTimeout(timeout));
|
||||
});
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isBootstrapping, setIsBootstrapping] = useState(true);
|
||||
const [bootstrapError, setBootstrapError] = useState<string | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [activeTenantId, setActiveTenantId] = useState<number | null>(null);
|
||||
const [profile, setProfile] = useState<Record<string, unknown> | null>(null);
|
||||
const [permissions, setPermissions] = useState<string[]>([]);
|
||||
const bootstrapRunRef = useRef(0);
|
||||
|
||||
const resetSession = useCallback(() => {
|
||||
setUser(null);
|
||||
@@ -65,6 +82,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const logout = useCallback(async () => {
|
||||
await clearStoredToken();
|
||||
setToken(null);
|
||||
setBootstrapError(null);
|
||||
resetSession();
|
||||
}, [resetSession]);
|
||||
|
||||
@@ -81,31 +99,59 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [hydrateSession, logout, resetSession, token]);
|
||||
|
||||
useEffect(() => {
|
||||
async function bootstrap() {
|
||||
await hydrateApiBaseUrl();
|
||||
const storedToken = await getStoredToken();
|
||||
const resetLocalSession = useCallback(async () => {
|
||||
setToken(null);
|
||||
setBootstrapError(null);
|
||||
setIsBootstrapping(false);
|
||||
resetSession();
|
||||
void clearStoredToken().catch(() => undefined);
|
||||
}, [resetSession]);
|
||||
|
||||
if (!storedToken) {
|
||||
setIsBootstrapping(false);
|
||||
return;
|
||||
}
|
||||
const bootstrap = useCallback(async () => {
|
||||
const runId = bootstrapRunRef.current + 1;
|
||||
bootstrapRunRef.current = runId;
|
||||
setIsBootstrapping(true);
|
||||
setBootstrapError(null);
|
||||
|
||||
try {
|
||||
await hydrateSession(storedToken);
|
||||
setToken(storedToken);
|
||||
} catch {
|
||||
await clearStoredToken();
|
||||
try {
|
||||
await withTimeout(
|
||||
(async () => {
|
||||
await hydrateApiBaseUrl();
|
||||
const storedToken = await getStoredToken();
|
||||
|
||||
if (!storedToken) {
|
||||
if (bootstrapRunRef.current !== runId) return;
|
||||
setToken(null);
|
||||
resetSession();
|
||||
return;
|
||||
}
|
||||
|
||||
await hydrateSession(storedToken);
|
||||
|
||||
if (bootstrapRunRef.current !== runId) return;
|
||||
setToken(storedToken);
|
||||
})(),
|
||||
BOOTSTRAP_TIMEOUT_MS,
|
||||
'Die mobile Session konnte nicht rechtzeitig initialisiert werden.'
|
||||
);
|
||||
} catch (err) {
|
||||
if (bootstrapRunRef.current === runId) {
|
||||
setToken(null);
|
||||
resetSession();
|
||||
} finally {
|
||||
setBootstrapError(err instanceof Error ? err.message : 'Die mobile Session konnte nicht initialisiert werden.');
|
||||
void clearStoredToken().catch(() => undefined);
|
||||
}
|
||||
} finally {
|
||||
if (bootstrapRunRef.current === runId) {
|
||||
setIsBootstrapping(false);
|
||||
}
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
}, [hydrateSession, resetSession]);
|
||||
|
||||
useEffect(() => {
|
||||
void bootstrap();
|
||||
}, [bootstrap]);
|
||||
|
||||
const login = useCallback(
|
||||
async (email: string, password: string) => {
|
||||
const nextToken = await loginWithEmailPassword(email, password);
|
||||
@@ -140,6 +186,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isBootstrapping,
|
||||
bootstrapError,
|
||||
token,
|
||||
user,
|
||||
tenants,
|
||||
@@ -152,10 +199,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
logout,
|
||||
refreshUser,
|
||||
switchTenant,
|
||||
retryBootstrap: bootstrap,
|
||||
resetLocalSession,
|
||||
}),
|
||||
[
|
||||
activeTenant,
|
||||
activeTenantId,
|
||||
bootstrap,
|
||||
bootstrapError,
|
||||
isBootstrapping,
|
||||
login,
|
||||
logout,
|
||||
@@ -163,6 +214,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
profile,
|
||||
refreshUser,
|
||||
requiresTenantSelection,
|
||||
resetLocalSession,
|
||||
switchTenant,
|
||||
tenants,
|
||||
token,
|
||||
|
||||
@@ -30,6 +30,14 @@ read_interactive() {
|
||||
fi
|
||||
}
|
||||
|
||||
docker_engine() {
|
||||
if [[ "${FEDEO_USE_SUDO_DOCKER:-false}" == "true" ]]; then
|
||||
sudo docker "$@"
|
||||
else
|
||||
docker "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
compose() {
|
||||
if [[ "${FEDEO_USE_SUDO_DOCKER:-false}" == "true" ]]; then
|
||||
sudo docker compose "$@"
|
||||
@@ -44,12 +52,27 @@ compose_stack() {
|
||||
|
||||
compose_start_command() {
|
||||
if [[ "${FEDEO_USE_SUDO_DOCKER:-false}" == "true" ]]; then
|
||||
echo "sudo docker compose --env-file $ENV_FILE -f $COMPOSE_FILE up -d"
|
||||
echo "sudo docker compose --env-file $ENV_FILE -f $COMPOSE_FILE up -d --remove-orphans"
|
||||
else
|
||||
echo "docker compose --env-file $ENV_FILE -f $COMPOSE_FILE up -d"
|
||||
echo "docker compose --env-file $ENV_FILE -f $COMPOSE_FILE up -d --remove-orphans"
|
||||
fi
|
||||
}
|
||||
|
||||
remove_selfhost_networks() {
|
||||
local network
|
||||
|
||||
for network in fedeo_web fedeo_internal; do
|
||||
if docker_engine network inspect "$network" >/dev/null 2>&1; then
|
||||
if docker_engine network rm "$network" >/dev/null 2>&1; then
|
||||
echo "Docker-Netzwerk entfernt: $network"
|
||||
else
|
||||
echo "Docker-Netzwerk konnte nicht entfernt werden: $network"
|
||||
echo "Prüfe laufende Container mit: docker network inspect $network"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
FEDEO Selfhost Setup
|
||||
@@ -461,6 +484,8 @@ uninstall_stack() {
|
||||
"$ROOT_DIR/traefik/letsencrypt" \
|
||||
"$ROOT_DIR/traefik/logs"
|
||||
|
||||
remove_selfhost_networks
|
||||
|
||||
echo
|
||||
echo "Uninstall inklusive lokaler Daten abgeschlossen."
|
||||
}
|
||||
@@ -597,7 +622,7 @@ main() {
|
||||
fi
|
||||
|
||||
if [[ "$START_STACK" == "yes" ]]; then
|
||||
compose_stack up -d
|
||||
compose_stack up -d --remove-orphans
|
||||
else
|
||||
echo
|
||||
echo "Start später mit:"
|
||||
|
||||