Finanzen: Steuerruecklage, Kleinunternehmer-Zaehler und Aufgabenliste
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
34dfe181e4
commit
7dbf2ce75f
4 files changed
+1039
No files matched your search
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Steuer-Auswertung der Finanzen (rein, ohne I/O): Rücklage auf den Gewinn des laufenden Jahres, Zähler für die
|
||||
* Kleinunternehmer-Grenzen (§ 19 UStG) und die nächste Einkommensteuer-Vorauszahlung. Beträge sind ganze Cent;
|
||||
* "heute" kommt als Berliner Kalendertag von außen.
|
||||
*/
|
||||
import { daysBetween } from './dates.js'
|
||||
|
||||
const GRENZE_FOLGEJAHR_CENTS = 2_500_000 // 25.000 EUR: wer im Jahr darüber liegt, ist im Folgejahr kein Kleinunternehmer mehr
|
||||
const GRENZE_HART_CENTS = 10_000_000 // 100.000 EUR: darüber endet die Regelung sofort
|
||||
const GEWERBESTEUER_FREIBETRAG_CENTS = 2_450_000 // 24.500 EUR Gewinn bei Personengesellschaften
|
||||
const DEFAULT_RESERVE_PERCENT = 30 // wie DEFAULT_SETTINGS.taxReservePercent (settings.js, hier nicht importiert: Modul bleibt rein)
|
||||
const EST_MONTHS = [3, 6, 9, 12] // ESt-Vorauszahlung jeweils am 10.
|
||||
const EST_DAY = 10
|
||||
|
||||
const isCents = (n) => Number.isSafeInteger(n)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
|
||||
/** Nächster ESt-Vorauszahlungstermin ab heute (heute zählt mit) als YYYY-MM-DD. */
|
||||
function nextEstDate(today) {
|
||||
const year = Number(today.slice(0, 4))
|
||||
for (const month of EST_MONTHS) {
|
||||
const date = `${String(year).padStart(4, '0')}-${pad(month)}-${pad(EST_DAY)}`
|
||||
if (date >= today) return date
|
||||
}
|
||||
return `${String(year + 1).padStart(4, '0')}-${pad(EST_MONTHS[0])}-${pad(EST_DAY)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} args
|
||||
* @param {string} args.today Berliner Tag YYYY-MM-DD
|
||||
* @param {Array} [args.transactions] Bankbuchungen { status, bookingDate, amountCents (+ Eingang / - Ausgang), category }
|
||||
* @param {Array} [args.invoices] Stripe-Rechnungen (gemappt) { paidCents, paidDate }
|
||||
* @param {object} [args.settings] { taxReservePercent, previousYearRevenueCents, estPrepaymentCents }
|
||||
*
|
||||
* Einnahmen und Ausgaben: gebuchte Bankbuchungen (BOOK) des Kalenderjahres von today ohne Kategorie "privat".
|
||||
* Der Kleinunternehmer-Zähler nimmt statt der Bankeingänge die Zahltage der Stripe-Rechnungen plus Buchungen
|
||||
* "rechnung-extern"; Stripe-Auszahlungen und Rechnungsbuchungen zählen dort nicht (sonst doppelt).
|
||||
*/
|
||||
export function computeTax({ today, transactions = [], invoices = [], settings } = {}) {
|
||||
daysBetween(today, today) // wirft bei einem ungültigen Tag
|
||||
const jahr = Number(today.slice(0, 4))
|
||||
const prefix = `${today.slice(0, 4)}-`
|
||||
|
||||
let einnahmenCents = 0
|
||||
let ausgabenCents = 0
|
||||
let externCents = 0
|
||||
for (const tx of transactions || []) {
|
||||
if (!tx || tx.status !== 'BOOK' || typeof tx.bookingDate !== 'string' || !tx.bookingDate.startsWith(prefix)) continue
|
||||
if (!isCents(tx.amountCents) || tx.category === 'privat') continue
|
||||
if (tx.amountCents > 0) {
|
||||
einnahmenCents += tx.amountCents
|
||||
if (tx.category === 'rechnung-extern') externCents += tx.amountCents
|
||||
} else {
|
||||
ausgabenCents -= tx.amountCents
|
||||
}
|
||||
}
|
||||
|
||||
let stripeCents = 0
|
||||
for (const invoice of invoices || []) {
|
||||
if (!invoice || typeof invoice.paidDate !== 'string' || !invoice.paidDate.startsWith(prefix)) continue
|
||||
if (isCents(invoice.paidCents) && invoice.paidCents > 0) stripeCents += invoice.paidCents
|
||||
}
|
||||
|
||||
const gewinnCents = einnahmenCents - ausgabenCents
|
||||
const prozent = Number.isFinite(settings?.taxReservePercent) ? settings.taxReservePercent : DEFAULT_RESERVE_PERCENT
|
||||
const jahrCents = stripeCents + externCents
|
||||
|
||||
const prepayment = settings?.estPrepaymentCents
|
||||
return {
|
||||
jahr,
|
||||
einnahmenCents,
|
||||
ausgabenCents,
|
||||
gewinnCents,
|
||||
ruecklageCents: Math.round((Math.max(0, gewinnCents) * prozent) / 100),
|
||||
prozent,
|
||||
gewerbesteuerHinweis: gewinnCents > GEWERBESTEUER_FREIBETRAG_CENTS,
|
||||
kleinunternehmer: {
|
||||
jahrCents,
|
||||
vorjahrCents: isCents(settings?.previousYearRevenueCents) ? settings.previousYearRevenueCents : null,
|
||||
grenzeFolgejahrCents: GRENZE_FOLGEJAHR_CENTS,
|
||||
grenzeHartCents: GRENZE_HART_CENTS,
|
||||
anteilFolgejahr: jahrCents / GRENZE_FOLGEJAHR_CENTS,
|
||||
},
|
||||
naechsteVorauszahlung: isCents(prepayment) && prepayment > 0 ? { date: nextEstDate(today), amountCents: prepayment } : null,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Aufgabenliste "Zu erledigen" der Finanzen (rein, ohne I/O). Jede Aufgabe gehört zu einer festen Art
|
||||
* (Spec "Zu erledigen – Arten"), hat eine Dringlichkeit (hoch, mittel, info), einen deutschen Titel und Text,
|
||||
* ein Datum (YYYY-MM-DD, wonach innerhalb der Dringlichkeit sortiert wird) und eine Aktion für die Oberfläche.
|
||||
* Beträge sind ganze Cent; "heute" (Berliner Tag) und "jetzt" kommen von außen. Kaputte Datensätze fallen weg,
|
||||
* statt die ganze Liste zu kippen.
|
||||
*/
|
||||
import { addDays, berlinDate, daysBetween, formatDe } from './dates.js'
|
||||
import { euroDe } from './format.js'
|
||||
|
||||
const HOCH = 'hoch'
|
||||
const MITTEL = 'mittel'
|
||||
const INFO = 'info'
|
||||
const RANK = { [HOCH]: 0, [MITTEL]: 1, [INFO]: 2 }
|
||||
|
||||
const BANK_WARN_DAYS = 30 // Zugang läuft in <= 30 Tagen ab: mittel
|
||||
const BANK_URGENT_DAYS = 14 // ... in <= 14 Tagen: hoch
|
||||
const SYNC_MAX_AGE_MS = 24 * 3_600_000
|
||||
const OVERDUE_URGENT_DAYS = 30 // ab 30 Tagen Verzug: hoch
|
||||
const DRAFT_MAX_AGE_DAYS = 3 // Entwurf älter als 3 Tage
|
||||
const RECURRING_MAX_AGE_DAYS = 60 // Vorschläge nur für Ausgaben, die zuletzt in den letzten 60 Tagen liefen
|
||||
const KLEIN_WARN_SHARE = 0.7
|
||||
// Wie in tax.js; gelten nur, wenn das tax-Objekt die Grenzen nicht mitgibt
|
||||
const KLEIN_LIMIT_CENTS = 2_500_000
|
||||
const KLEIN_HARD_LIMIT_CENTS = 10_000_000
|
||||
const MAX_REMITTANCE = 80
|
||||
|
||||
const INTERVAL_TEXT = { monat: 'monatlich', quartal: 'vierteljährlich', jahr: 'jährlich' }
|
||||
|
||||
const isCents = (n) => Number.isSafeInteger(n)
|
||||
|
||||
/** Wert, der ein echter Kalendertag sein muss, sonst null. */
|
||||
function validYmd(ymd) {
|
||||
try {
|
||||
daysBetween(ymd, ymd)
|
||||
return ymd
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Zeitstempel (ISO-Text) als Date, null bei fehlendem oder unlesbarem Wert. */
|
||||
function parseInstant(iso) {
|
||||
if (!iso || typeof iso !== 'string') return null
|
||||
const date = new Date(iso)
|
||||
return Number.isNaN(date.getTime()) ? null : date
|
||||
}
|
||||
|
||||
const trimmed = (value) => (typeof value === 'string' ? value.trim() : '')
|
||||
|
||||
/** "1 Tag" / "5 Tage" (Nominativ) bzw. "1 Tag" / "5 Tagen" (Dativ). */
|
||||
const days = (n, dative = false) => `${n} ${n === 1 ? 'Tag' : dative ? 'Tagen' : 'Tage'}`
|
||||
|
||||
const clip = (text, max) => (text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text)
|
||||
|
||||
function todo(art, dringlichkeit, titel, text, datum, aktion) {
|
||||
return { art, dringlichkeit, titel, text, datum, aktion }
|
||||
}
|
||||
|
||||
// --- Bank ---------------------------------------------------------------------------------------------------
|
||||
|
||||
function bankTodos({ today, now, connection }) {
|
||||
const result = []
|
||||
if (!connection) {
|
||||
result.push(
|
||||
todo(
|
||||
'bank-zugang',
|
||||
HOCH,
|
||||
'Bankkonto verbinden',
|
||||
'Es ist noch kein Bankkonto verbunden. Verbinden Sie das Konto, damit Zahlungseingänge abgerufen werden.',
|
||||
today,
|
||||
{ typ: 'bank-verbinden' }
|
||||
)
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
const validUntilDate = parseInstant(connection.validUntil)
|
||||
const validUntil = validUntilDate ? berlinDate(validUntilDate) : null
|
||||
const daysLeft = validUntil ? daysBetween(today, validUntil) : null
|
||||
const expired = connection.status === 'abgelaufen' || (daysLeft !== null && daysLeft < 0)
|
||||
|
||||
if (expired) {
|
||||
const when = validUntil && validUntil < today ? `am ${formatDe(validUntil)} abgelaufen` : 'abgelaufen oder widerrufen worden'
|
||||
result.push(
|
||||
todo(
|
||||
'bank-zugang',
|
||||
HOCH,
|
||||
'Bankzugang abgelaufen',
|
||||
`Der Zugang zum Bankkonto ist ${when}. Verbinden Sie das Konto neu, damit wieder Buchungen abgerufen werden.`,
|
||||
validUntil && validUntil < today ? validUntil : today,
|
||||
{ typ: 'bank-verbinden' }
|
||||
)
|
||||
)
|
||||
} else if (daysLeft !== null && daysLeft <= BANK_WARN_DAYS) {
|
||||
const phrase = daysLeft === 0 ? 'heute' : daysLeft === 1 ? 'morgen' : `in ${days(daysLeft, true)}`
|
||||
result.push(
|
||||
todo(
|
||||
'bank-zugang',
|
||||
daysLeft <= BANK_URGENT_DAYS ? HOCH : MITTEL,
|
||||
'Bankzugang läuft ab',
|
||||
`Der Zugang zum Bankkonto läuft am ${formatDe(validUntil)} ab (${phrase}). Verbinden Sie das Konto neu, damit der Abruf nicht unterbrochen wird.`,
|
||||
validUntil,
|
||||
{ typ: 'bank-verbinden' }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (connection.status === 'aktiv') {
|
||||
const lastSync = parseInstant(connection.lastSyncAt)
|
||||
const stale = !lastSync || now.getTime() - lastSync.getTime() > SYNC_MAX_AGE_MS
|
||||
if (connection.lastSyncOk === false || stale) {
|
||||
const lastDay = lastSync ? berlinDate(lastSync) : today
|
||||
const failed = connection.lastSyncOk === false
|
||||
const error = trimmed(connection.lastError)
|
||||
const last = lastSync ? `Der letzte erfolgreiche Abruf war am ${formatDe(lastDay)}.` : 'Es gab noch keinen erfolgreichen Abruf.'
|
||||
result.push(
|
||||
todo(
|
||||
'bank-abruf',
|
||||
MITTEL,
|
||||
failed ? 'Bankabruf fehlgeschlagen' : 'Bankabruf veraltet',
|
||||
`${last}${failed && error ? ` Fehler: ${error}` : ''} Rufen Sie die Buchungen jetzt neu ab.`,
|
||||
lastDay,
|
||||
{ typ: 'bank-aktualisieren' }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- Rechnungen ---------------------------------------------------------------------------------------------
|
||||
|
||||
function overdueTodos({ today, invoices }) {
|
||||
const result = []
|
||||
for (const invoice of invoices) {
|
||||
if (!invoice || (invoice.status !== 'ueberfaellig' && invoice.status !== 'offen')) continue
|
||||
const due = validYmd(invoice.dueDate)
|
||||
if (!due || due >= today || !isCents(invoice.remainingCents)) continue
|
||||
const late = daysBetween(due, today)
|
||||
const customer = trimmed(invoice.customerName)
|
||||
result.push(
|
||||
todo(
|
||||
'ueberfaellig',
|
||||
late >= OVERDUE_URGENT_DAYS ? HOCH : MITTEL,
|
||||
['Rechnung', trimmed(invoice.number), 'überfällig'].filter(Boolean).join(' '),
|
||||
`${customer ? `${customer}: ` : ''}${euroDe(invoice.remainingCents)} offen, fällig am ${formatDe(due)} (${days(late)} überfällig).`,
|
||||
due,
|
||||
{ typ: 'rechnung-oeffnen', invoiceId: invoice.id, hostedUrl: invoice.hostedUrl || null }
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function draftTodos({ today, invoices }) {
|
||||
const result = []
|
||||
for (const invoice of invoices) {
|
||||
if (!invoice || invoice.status !== 'entwurf') continue
|
||||
const created = validYmd(invoice.createdDate)
|
||||
if (!created) continue
|
||||
const age = daysBetween(created, today)
|
||||
if (age <= DRAFT_MAX_AGE_DAYS) continue
|
||||
const number = trimmed(invoice.number)
|
||||
const customer = trimmed(invoice.customerName)
|
||||
const amount = isCents(invoice.totalCents) ? invoice.totalCents : invoice.remainingCents
|
||||
result.push(
|
||||
todo(
|
||||
'entwurf',
|
||||
INFO,
|
||||
'Rechnungsentwurf nicht versendet',
|
||||
`Der Entwurf${number ? ` ${number}` : ''}${customer ? ` für ${customer}` : ''}${isCents(amount) ? ` über ${euroDe(amount)}` : ''} wurde vor ${days(age, true)} angelegt und noch nicht gesendet.`,
|
||||
created,
|
||||
{ typ: 'rechnung-oeffnen', invoiceId: invoice.id, hostedUrl: invoice.hostedUrl || null }
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- Buchungen ----------------------------------------------------------------------------------------------
|
||||
|
||||
/** Erster Vorschlag mit Sicherheit "hoch" als Satzende, sonst leer. */
|
||||
function hint(transaction) {
|
||||
const best = Array.isArray(transaction.vorschlaege) ? transaction.vorschlaege.find((item) => item?.confidence === 'hoch') : null
|
||||
if (!best) return ''
|
||||
if (best.kind === 'payout') return 'Vorschlag: Stripe-Auszahlung'
|
||||
if (best.kind === 'invoice') return `Vorschlag: Rechnung ${trimmed(best.number) || best.id || ''}`.trimEnd()
|
||||
return ''
|
||||
}
|
||||
|
||||
function assignTodos({ transactions }) {
|
||||
const result = []
|
||||
for (const tx of transactions) {
|
||||
if (!tx || tx.status !== 'BOOK' || tx.category || !isCents(tx.amountCents) || tx.amountCents <= 0) continue
|
||||
const date = validYmd(tx.bookingDate)
|
||||
if (!date) continue
|
||||
const from = trimmed(tx.counterpartyName)
|
||||
const remittance = clip(trimmed(tx.remittance), MAX_REMITTANCE)
|
||||
const suggestion = hint(tx)
|
||||
result.push(
|
||||
todo(
|
||||
'zuordnung',
|
||||
MITTEL,
|
||||
'Zahlungseingang zuordnen',
|
||||
`${euroDe(tx.amountCents)}${from ? ` von ${from}` : ''} am ${formatDe(date)}${remittance ? `: „${remittance}“` : ''}.${suggestion ? ` ${suggestion}` : ''}`,
|
||||
date,
|
||||
{ typ: 'zuordnen', transactionId: tx.$id }
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function recurringTodos({ today, recurring }) {
|
||||
const result = []
|
||||
const earliest = addDays(today, -RECURRING_MAX_AGE_DAYS)
|
||||
for (const entry of recurring) {
|
||||
const last = entry ? validYmd(entry.lastDate) : null
|
||||
if (!last || last < earliest || !isCents(entry.amountCents)) continue
|
||||
const rhythm = INTERVAL_TEXT[entry.interval]
|
||||
result.push(
|
||||
todo(
|
||||
'fixkosten-vorschlag',
|
||||
INFO,
|
||||
'Wiederkehrende Ausgabe erkannt',
|
||||
`${trimmed(entry.name) || 'Eine Ausgabe'}: ${euroDe(entry.amountCents)}${rhythm ? ` ${rhythm}` : ''}, zuletzt am ${formatDe(last)}${isCents(entry.count) ? ` (${entry.count} Buchungen)` : ''}. Als Fixkosten übernehmen?`,
|
||||
last,
|
||||
{ typ: 'fixkosten-uebernehmen', vorschlag: entry }
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- Abos und Kleinunternehmer ------------------------------------------------------------------------------
|
||||
|
||||
function subscriptionTodos({ today, subscriptions }) {
|
||||
const result = []
|
||||
for (const subscription of subscriptions) {
|
||||
if (!subscription || (subscription.status !== 'past_due' && subscription.status !== 'unpaid')) continue
|
||||
const failed = subscription.status === 'past_due'
|
||||
const customer = trimmed(subscription.customerName)
|
||||
const product = trimmed(subscription.productKey) || trimmed(subscription.planKey)
|
||||
result.push(
|
||||
todo(
|
||||
'abo-zahlung',
|
||||
HOCH,
|
||||
failed ? 'Abo-Zahlung fehlgeschlagen' : 'Abo unbezahlt',
|
||||
`${customer ? `${customer}: ` : ''}Das Abo${product ? ` „${product}“` : ''}${isCents(subscription.priceAmount) ? ` (${euroDe(subscription.priceAmount)})` : ''} ${failed ? 'konnte nicht abgebucht werden.' : 'ist unbezahlt: Die Zahlung blieb aus.'}`,
|
||||
today,
|
||||
{ typ: 'kunde-oeffnen', customerId: subscription.customerId }
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function smallBusinessTodos({ today, tax }) {
|
||||
const small = tax?.kleinunternehmer
|
||||
const share = small?.anteilFolgejahr
|
||||
if (typeof share !== 'number' || !Number.isFinite(share) || share < KLEIN_WARN_SHARE) return []
|
||||
|
||||
const limit = isCents(small.grenzeFolgejahrCents) ? small.grenzeFolgejahrCents : KLEIN_LIMIT_CENTS
|
||||
const hardLimit = isCents(small.grenzeHartCents) ? small.grenzeHartCents : KLEIN_HARD_LIMIT_CENTS
|
||||
const revenue = isCents(small.jahrCents) ? small.jahrCents : Math.round(share * limit)
|
||||
const percent = Math.floor(share * 100 + 1e-9)
|
||||
const year = today.slice(0, 4)
|
||||
|
||||
if (revenue > hardLimit) {
|
||||
return [
|
||||
todo(
|
||||
'kleinunternehmer',
|
||||
HOCH,
|
||||
'Kleinunternehmer-Grenze überschritten',
|
||||
`Die Einnahmen ${year} betragen ${euroDe(revenue)} und liegen über der Grenze von ${euroDe(hardLimit)}. Die Kleinunternehmerregelung (§ 19 UStG) ist davon sofort betroffen. Bitte umgehend mit der Steuerberatung klären.`,
|
||||
today,
|
||||
{ typ: 'einstellungen' }
|
||||
),
|
||||
]
|
||||
}
|
||||
if (share >= 1) {
|
||||
return [
|
||||
todo(
|
||||
'kleinunternehmer',
|
||||
HOCH,
|
||||
'Kleinunternehmer-Grenze erreicht',
|
||||
`Die Einnahmen ${year} betragen ${euroDe(revenue)} (${percent} % der Grenze von ${euroDe(limit)}). Über ${euroDe(limit)} endet die Kleinunternehmerregelung (§ 19 UStG) im Folgejahr, über ${euroDe(hardLimit)} schon im laufenden Jahr. Bitte mit der Steuerberatung klären.`,
|
||||
today,
|
||||
{ typ: 'einstellungen' }
|
||||
),
|
||||
]
|
||||
}
|
||||
return [
|
||||
todo(
|
||||
'kleinunternehmer',
|
||||
MITTEL,
|
||||
'Kleinunternehmer-Grenze rückt näher',
|
||||
`Die Einnahmen ${year} liegen bei ${euroDe(revenue)} und damit bei ${percent} % der Grenze von ${euroDe(limit)}. Wird sie überschritten, endet die Kleinunternehmerregelung (§ 19 UStG) im Folgejahr.`,
|
||||
today,
|
||||
{ typ: 'einstellungen' }
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} args
|
||||
* @param {string} args.today Berliner Tag YYYY-MM-DD
|
||||
* @param {Date} args.now Zeitpunkt (Alter des letzten Bankabrufs)
|
||||
* @param {object|null} args.connection Bankverbindung { status, validUntil, lastSyncAt, lastSyncOk, lastError } oder null
|
||||
* @param {Array} [args.invoices] gemappte Stripe-Rechnungen
|
||||
* @param {Array} [args.transactions] Bankbuchungen, optional mit vorschlaege
|
||||
* @param {Array} [args.subscriptions] Abos { customerId, status, priceAmount, productKey|planKey, customerName? }
|
||||
* @param {Array} [args.recurring] erkannte wiederkehrende Ausgaben (detectRecurring)
|
||||
* @param {object} [args.tax] Ergebnis von computeTax
|
||||
* @returns {Array<{ art, dringlichkeit, titel, text, datum, aktion }>} hoch vor mittel vor info, darin ältestes Datum zuerst
|
||||
*/
|
||||
export function buildTodos({ today, now = new Date(), connection, invoices, transactions, subscriptions, recurring, tax } = {}) {
|
||||
daysBetween(today, today) // wirft bei einem ungültigen Tag
|
||||
const list = (value) => (Array.isArray(value) ? value : [])
|
||||
const all = [
|
||||
...bankTodos({ today, now, connection }),
|
||||
...overdueTodos({ today, invoices: list(invoices) }),
|
||||
...assignTodos({ transactions: list(transactions) }),
|
||||
...subscriptionTodos({ today, subscriptions: list(subscriptions) }),
|
||||
...draftTodos({ today, invoices: list(invoices) }),
|
||||
...recurringTodos({ today, recurring: list(recurring) }),
|
||||
...smallBusinessTodos({ today, tax }),
|
||||
]
|
||||
// Array.prototype.sort ist stabil: bei gleicher Dringlichkeit und gleichem Datum bleibt die Reihenfolge der Arten
|
||||
return all.sort((a, b) => RANK[a.dringlichkeit] - RANK[b.dringlichkeit] || (a.datum < b.datum ? -1 : a.datum > b.datum ? 1 : 0))
|
||||
}
|
||||
Reference in new issue
Block a user