Kundenansicht P6: Vorschuss im Stundenkonto (Einrichtung statt Mehraufwand)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57d5db2344
commit
f016d7ae6c
2 files changed
+108
-13
No files matched your search
@@ -11,6 +11,13 @@ import { worksheetMinutes } from './worksheetMinutes.js'
|
||||
* Verrechnung: FIFO - die aelteste noch gueltige Gutschrift wird zuerst verbraucht.
|
||||
* Was kein Guthaben mehr findet, ist Mehraufwand (separat abzurechnen).
|
||||
* Vertragsende: Restguthaben verfaellt (siehe Abo-Bedingungen Ziff. 4).
|
||||
*
|
||||
* Vorschuss (Abo-Modell der Funktionen): Arbeitsblaetter auf Tickets mit einrichtungAbrechnung
|
||||
* 'abo' sind Einrichtung. Was davon kein Guthaben findet, ist Vorschuss
|
||||
* (vorschussMinutes), nie Mehraufwand; normale Arbeit bleibt Mehraufwand. Eine spaetere
|
||||
* Gutschrift gleicht zuerst den Vorschuss aus (Durchgang 2: Einrichtung vor den
|
||||
* uebrigen Eintraegen). Nach Vertragsende bleibt der Vorschuss stehen und wird nicht
|
||||
* berechnet (active false setzt nur Guthaben und credits[].remaining auf 0).
|
||||
*/
|
||||
|
||||
function parseSheetDate(ws) {
|
||||
@@ -68,6 +75,7 @@ export async function listPlanUsage(customerId) {
|
||||
supportMinutes: total - (Number(ws.totalTime) || 0),
|
||||
details: String(ws.details || '').slice(0, 300),
|
||||
employee: ws.employeeName || ws.employeeShort || '',
|
||||
einrichtung: wo.einrichtungAbrechnung === 'abo',
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.date.localeCompare(b.date))
|
||||
@@ -86,20 +94,54 @@ export function computeLedger(credits, usage, { now = new Date(), active = true
|
||||
.sort((a, b) => String(a.grantedAt).localeCompare(String(b.grantedAt)))
|
||||
|
||||
let overageMinutes = 0
|
||||
const entries = usage.map((u) => {
|
||||
let need = u.minutes
|
||||
const at = new Date(u.date)
|
||||
for (const c of pool) {
|
||||
if (!need) break
|
||||
// Nur Gutschriften, die zum Arbeitszeitpunkt noch gueltig waren
|
||||
if (c.remaining <= 0 || new Date(c.expiresAt) <= at) continue
|
||||
const take = Math.min(need, c.remaining)
|
||||
c.remaining -= take
|
||||
need -= take
|
||||
let vorschussMinutes = 0
|
||||
let entries
|
||||
if (!usage.some((u) => u.einrichtung === true)) {
|
||||
entries = usage.map((u) => {
|
||||
let need = u.minutes
|
||||
const at = new Date(u.date)
|
||||
for (const c of pool) {
|
||||
if (!need) break
|
||||
// Nur Gutschriften, die zum Arbeitszeitpunkt noch gueltig waren
|
||||
if (c.remaining <= 0 || new Date(c.expiresAt) <= at) continue
|
||||
const take = Math.min(need, c.remaining)
|
||||
c.remaining -= take
|
||||
need -= take
|
||||
}
|
||||
overageMinutes += need
|
||||
return { ...u, coveredMinutes: u.minutes - need, overageMinutes: need, einrichtung: false, vorschussMinutes: 0 }
|
||||
})
|
||||
} else {
|
||||
// Zwei Durchgaenge in Datumsreihenfolge (bei gleichem Datum bleibt die Eingabereihenfolge)
|
||||
const ordered = usage
|
||||
.map((u, index) => ({ u, index, at: new Date(u.date), need: u.minutes }))
|
||||
.sort((a, b) => String(a.u.date).localeCompare(String(b.u.date)) || a.index - b.index)
|
||||
const decke = (e, nurGebucht) => {
|
||||
for (const c of pool) {
|
||||
if (!e.need) break
|
||||
if (c.remaining <= 0 || new Date(c.expiresAt) <= e.at) continue
|
||||
if (nurGebucht && new Date(c.grantedAt) > e.at) continue
|
||||
const take = Math.min(e.need, c.remaining)
|
||||
c.remaining -= take
|
||||
e.need -= take
|
||||
}
|
||||
}
|
||||
overageMinutes += need
|
||||
return { ...u, coveredMinutes: u.minutes - need, overageMinutes: need }
|
||||
})
|
||||
// Durchgang 1: nur Gutschriften, die zur Arbeitszeit schon gebucht und noch gueltig waren
|
||||
for (const e of ordered) decke(e, true)
|
||||
// Durchgang 2: den Rest rueckwirkend, zuerst alle Einrichtungseintraege, dann die uebrigen
|
||||
for (const e of ordered.filter((x) => x.u.einrichtung === true)) decke(e, false)
|
||||
for (const e of ordered.filter((x) => x.u.einrichtung !== true)) decke(e, false)
|
||||
const byIndex = new Map(ordered.map((e) => [e.index, e]))
|
||||
entries = usage.map((u, index) => {
|
||||
const e = byIndex.get(index)
|
||||
const einrichtung = u.einrichtung === true
|
||||
const vorschuss = einrichtung ? e.need : 0
|
||||
const mehr = einrichtung ? 0 : e.need
|
||||
overageMinutes += mehr
|
||||
vorschussMinutes += vorschuss
|
||||
return { ...u, coveredMinutes: u.minutes - e.need, overageMinutes: mehr, einrichtung, vorschussMinutes: vorschuss }
|
||||
})
|
||||
}
|
||||
|
||||
const valid = pool.filter((c) => c.remaining > 0 && new Date(c.expiresAt) > now)
|
||||
const expiredMinutes = pool
|
||||
@@ -115,6 +157,7 @@ export function computeLedger(credits, usage, { now = new Date(), active = true
|
||||
grantedMinutes: pool.reduce((s, c) => s + c.minutes, 0),
|
||||
usedMinutes: entries.reduce((s, e) => s + e.coveredMinutes, 0),
|
||||
overageMinutes,
|
||||
vorschussMinutes,
|
||||
expiredMinutes,
|
||||
nextExpiry: nextExpiry ? { minutes: nextExpiry.remaining, at: nextExpiry.expiresAt } : null,
|
||||
credits: pool.map((c) => ({
|
||||
|
||||
Reference in new issue
Block a user