Kunden: - Neues Feld customers.archived; Archivieren/Wiederherstellen-Button in der Kundendetailseite, 'Archiviert'-Badge - CustomersPage: vierter Filter-Tab 'Archiviert'; archivierte Kunden aus Alle/Leads/Feste Kunden ausgeblendet, nur im Archiv-Tab sichtbar Tickets: - Status-Mehrfachauswahl per Checkbox im Filterbereich; Default alle Status ausser 'Closed' (= archiviert). 'Standard' / 'Alle inkl. Archiv' Shortcuts - useWorkorders: Statusfilter serverseitig via Query.equal(array) statt Client-Filter nach dem Limit (behebt Bug, bei dem archivierte Tickets die neuesten Slots belegten und aktive verdraengten) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
4.4 KiB
JavaScript
131 lines
4.4 KiB
JavaScript
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from './appwrite'
|
|
import { integrationsApi } from './integrationsApi'
|
|
|
|
/** Pflichtfelder, damit eine Rechnung erstellt werden kann (wie InvoiceNinja). */
|
|
export function isInvoiceReady(c) {
|
|
return Boolean(c && c.name && c.email && c.street && c.city && c.postalCode)
|
|
}
|
|
|
|
export function customerStage(c) {
|
|
const s = (c?.customerStatus || '').toLowerCase()
|
|
if (s === 'lead') return 'lead'
|
|
if (s === 'lost') return 'lost'
|
|
return 'customer' // Bestandsdaten ohne Status gelten als fester Kunde
|
|
}
|
|
|
|
/** Archiviert = aus der Standardansicht ausgeblendet (orthogonal zur CRM-Stufe). */
|
|
export function isArchived(c) {
|
|
return Boolean(c?.archived)
|
|
}
|
|
|
|
/** Kunde archivieren / wiederherstellen. */
|
|
export async function setCustomerArchived(customer, archived) {
|
|
return databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, {
|
|
archived: Boolean(archived),
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
}
|
|
|
|
function ninjaPayload(c) {
|
|
return {
|
|
name: c.companyName || c.name,
|
|
email: c.email || '',
|
|
phone: c.phone || '',
|
|
street: c.street || '',
|
|
city: c.city || '',
|
|
postalCode: c.postalCode || '',
|
|
country: c.country || '',
|
|
vatNumber: c.vatNumber || '',
|
|
contactName: c.contactName || '',
|
|
}
|
|
}
|
|
|
|
/** Erstellt/aktualisiert den InvoiceNinja-Client und speichert die ID am Kunden. */
|
|
export async function syncCustomerToNinja(customer) {
|
|
if (customer.invoiceNinjaClientId) {
|
|
await integrationsApi.updateClient(customer.invoiceNinjaClientId, ninjaPayload(customer))
|
|
return customer.invoiceNinjaClientId
|
|
}
|
|
const r = await integrationsApi.createClient(ninjaPayload(customer))
|
|
const id = r.client?.id
|
|
if (id) {
|
|
await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, {
|
|
invoiceNinjaClientId: id,
|
|
})
|
|
}
|
|
return id
|
|
}
|
|
|
|
async function nextWoid() {
|
|
try {
|
|
const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [
|
|
Query.orderDesc('$createdAt'),
|
|
Query.limit(200),
|
|
])
|
|
const nums = res.documents.map((d) => parseInt(d.woid)).filter((n) => !isNaN(n) && n > 0)
|
|
return String((nums.length ? Math.max(...nums) : 9999) + 1)
|
|
} catch {
|
|
return String(10000)
|
|
}
|
|
}
|
|
|
|
/** Auto-Akquise-Ticket fuer einen potenziellen Kunden. */
|
|
export async function createAcquisitionTicket(customer, user) {
|
|
const woid = await nextWoid()
|
|
const data = {
|
|
topic: `Akquise: ${customer.name}`,
|
|
status: 'Open',
|
|
priority: 2,
|
|
woid,
|
|
type: 'Akquise',
|
|
systemType: 'n/a',
|
|
serviceType: 'Remote',
|
|
customerId: customer.$id,
|
|
customerName: customer.name,
|
|
customerLocation: customer.location || '',
|
|
requestedBy: customer.email || '',
|
|
assignedTo: user?.$id || '',
|
|
details:
|
|
`Automatisch erstelltes Akquise-Ticket fuer den potenziellen Kunden "${customer.name}".\n` +
|
|
`Kontakt: ${customer.email || '-'}\n\n` +
|
|
`Worksheets dokumentieren den Kontaktverlauf (sichtbar in den WSIDs).\n` +
|
|
`Ergebnis im Worksheet auf "Zugesagt" -> Lead wird automatisch fester Kunde. "Abgesagt" -> Ticket geschlossen.`,
|
|
createdAt: new Date().toISOString(),
|
|
}
|
|
return databases.createDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, ID.unique(), data)
|
|
}
|
|
|
|
/** Legt einen Lead an (nur Unternehmen + E-Mail) und erstellt das Akquise-Ticket. */
|
|
export async function createLead({ company, email }, user) {
|
|
const customer = await databases.createDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, ID.unique(), {
|
|
name: company,
|
|
companyName: company,
|
|
email: email || '',
|
|
customerStatus: 'lead',
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
let ticket = null
|
|
try {
|
|
ticket = await createAcquisitionTicket(customer, user)
|
|
} catch {
|
|
/* Ticket ist optional - Lead bleibt bestehen */
|
|
}
|
|
return { customer, ticket }
|
|
}
|
|
|
|
/** Macht aus einem Lead einen festen Kunden (mit vollstaendigen Daten) und legt den InvoiceNinja-Client an. */
|
|
export async function upgradeToCustomer(customer, fullData) {
|
|
const updated = await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, {
|
|
...fullData,
|
|
customerStatus: 'customer',
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
let clientId = updated.invoiceNinjaClientId
|
|
try {
|
|
clientId = await syncCustomerToNinja(updated)
|
|
} catch (e) {
|
|
return { customer: updated, clientId: null, syncError: e.message }
|
|
}
|
|
return { customer: { ...updated, invoiceNinjaClientId: clientId }, clientId }
|
|
}
|