Aufträge: Arbeitsblätter laden, normieren und als abgerechnet erkennen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9cd5ea3038
commit
47a92811a3
2 files changed
+304
No files matched your search
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Arbeitsblätter (Collection worksheets, gehört dem Ticketsystem) für die Projektabrechnung: laden, normieren
|
||||
* (Berliner Tag, auf 15 Minuten aufgerundete Minuten, kundentauglicher Text) und als abgerechnet erkennen
|
||||
* (Übersicht V3/V4). „Abgerechnet“ gibt es nur über aktive Projektrechnungen: ein stornierter Eintrag gibt seine
|
||||
* Blätter frei, ein Entwurf sperrt sie. Abo-Zeit und Kommentare sind nie abrechenbar.
|
||||
*/
|
||||
import { config } from '../../config.js'
|
||||
import { listAllDocuments, Query } from '../appwriteAdmin.js'
|
||||
import { customerWorkEntries } from '../customerProjectView.js'
|
||||
import { berlinDate } from '../finance/dates.js'
|
||||
import { worksheetMinutes } from '../worksheetMinutes.js'
|
||||
import { VEREINBARTE_STATUS, VIERTELSTUNDE } from './konstanten.js'
|
||||
import { listProjektRechnungen } from './projektRechnungenStore.js'
|
||||
|
||||
const BLOCK = 100 // Werte je equal-Abfrage (Appwrite begrenzt die Werteliste)
|
||||
const MAX_TEXT = 200
|
||||
const DE_DATUM = /^(\d{2})\.(\d{2})\.(\d{4})$/
|
||||
|
||||
/** Berliner Kalendertag eines Arbeitsblatts; nie ein Wurf. Regel wie parseSheetDate in hourLedger.js. */
|
||||
function blattDatum(doc) {
|
||||
const roh = String(doc.startDate || doc.endDate || '').trim()
|
||||
const de = DE_DATUM.exec(roh)
|
||||
if (de) {
|
||||
const [tag, monat, jahr] = [Number(de[1]), Number(de[2]), Number(de[3])]
|
||||
const pruefung = new Date(Date.UTC(jahr, monat - 1, tag))
|
||||
if (pruefung.getUTCFullYear() === jahr && pruefung.getUTCMonth() === monat - 1 && pruefung.getUTCDate() === tag) return `${de[3]}-${de[2]}-${de[1]}`
|
||||
// ein unmögliches Datum (31.02.) ist nie ein Kandidat für new Date: V8 läse „12.13.2026“ als Dezember
|
||||
}
|
||||
const kandidaten = [de ? '' : roh, doc.createdAt, doc.$createdAt]
|
||||
for (const kandidat of kandidaten) {
|
||||
if (!kandidat) continue
|
||||
const datum = new Date(kandidat)
|
||||
if (!Number.isNaN(datum.getTime())) return berlinDate(datum)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** Text wie in der Kundenansicht (customerWorkEntries): eine Zeile, höchstens 200 Zeichen. */
|
||||
function blattText(doc) {
|
||||
const [eintrag] = customerWorkEntries([doc], { includeUpdates: true })
|
||||
if (!eintrag) return ''
|
||||
const roh = Array.isArray(eintrag.changes) ? eintrag.changes.join('; ') : String(eintrag.text || '')
|
||||
const text = roh.replace(/\s+/g, ' ').trim()
|
||||
return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT - 1).trimEnd()}…` : text
|
||||
}
|
||||
|
||||
function docToBlatt(doc) {
|
||||
return {
|
||||
id: doc.$id,
|
||||
ticketId: String(doc.workorderId ?? ''),
|
||||
woid: String(doc.woid ?? ''),
|
||||
datum: blattDatum(doc),
|
||||
minuten: Math.ceil(worksheetMinutes(doc) / VIERTELSTUNDE) * VIERTELSTUNDE,
|
||||
text: blattText(doc),
|
||||
aboZeit: doc.countsToPlan === true,
|
||||
kommentar: doc.isComment === true,
|
||||
}
|
||||
}
|
||||
|
||||
const sortiert = (blaetter) => blaetter.sort((a, b) => a.datum.localeCompare(b.datum) || a.id.localeCompare(b.id))
|
||||
|
||||
async function ladeNach(attribut, werte) {
|
||||
const liste = Array.isArray(werte) ? [...new Set(werte.filter((wert) => typeof wert === 'string' && wert !== ''))] : []
|
||||
const blaetter = []
|
||||
for (let i = 0; i < liste.length; i += BLOCK) {
|
||||
const docs = await listAllDocuments(config.collections.worksheets, [Query.equal(attribut, liste.slice(i, i + BLOCK))])
|
||||
blaetter.push(...docs.map(docToBlatt))
|
||||
}
|
||||
return sortiert(blaetter)
|
||||
}
|
||||
|
||||
export const ladeArbeitsblaetter = (ticketIds) => ladeNach('workorderId', ticketIds)
|
||||
|
||||
/** Liefert nur gefundene Blätter; der Aufrufer vergleicht die Anzahl. */
|
||||
export const ladeArbeitsblaetterNachIds = (ids) => ladeNach('$id', ids)
|
||||
|
||||
export const abrechenbar = (blatt) => !blatt.aboZeit && !blatt.kommentar && blatt.minuten > 0
|
||||
|
||||
/** Karte Arbeitsblatt-ID → { invoiceId, rechnungsnummer } aus aktiven Projektrechnungen (Entwurf: Nummer ''). */
|
||||
export async function abgerechneteArbeitsblaetter(kundenId) {
|
||||
const karte = new Map()
|
||||
for (const rechnung of await listProjektRechnungen({ kundenId })) {
|
||||
for (const id of rechnung.arbeitsblattIds) {
|
||||
if (!karte.has(id)) karte.set(id, { invoiceId: rechnung.invoiceId, rechnungsnummer: rechnung.rechnungsnummer })
|
||||
}
|
||||
}
|
||||
return karte
|
||||
}
|
||||
|
||||
export async function offeneArbeitsblaetter(ticketIds, kundenId) {
|
||||
const abgerechnet = await abgerechneteArbeitsblaetter(kundenId)
|
||||
return (await ladeArbeitsblaetter(ticketIds)).filter((blatt) => abrechenbar(blatt) && !abgerechnet.has(blatt.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeitquelle eines Projekts: Vereinigung der zeitquelle aller vereinbarten ABs (VEREINBARTE_STATUS), sonst das
|
||||
* Ticket des Projekts. Der Aufrufer übergibt nur die ABs dieses Projekts; hier wird nicht nach Projekt gefiltert.
|
||||
*/
|
||||
export function zeitquelleFuerProjekt(projekt, auftraege) {
|
||||
const quelle = new Set()
|
||||
for (const auftrag of Array.isArray(auftraege) ? auftraege : []) {
|
||||
if (!VEREINBARTE_STATUS.includes(auftrag?.status)) continue
|
||||
for (const id of auftrag.zeitquelle || []) if (typeof id === 'string' && id !== '') quelle.add(id)
|
||||
}
|
||||
if (quelle.size) return [...quelle]
|
||||
const ticketId = projekt?.ticketId
|
||||
return typeof ticketId === 'string' && ticketId !== '' ? [ticketId] : []
|
||||
}
|
||||
Reference in new issue
Block a user