refactor(time): app-wide setting-aware TimeField for all time inputs

Add shared components/common/TimeField (displays per general_time_format,
stores canonical HH:MM, parses tolerant free-text, browser-independent)
and migrate every native <input type="time"> to it: business hours,
HoursSection, HourEntryInlinePopover, CreateEventPage, Quote/Bill/Contract
editors. Removes the unreliable lang-hint plumbing.
This commit is contained in:
Luca
2026-06-02 17:24:27 +02:00
parent 2149f38cd1
commit 626ab45e0b
11 changed files with 180 additions and 137 deletions
@@ -0,0 +1,37 @@
/**
* Migration: backfill paid_amount_minor for imported PAID invoices that
* stored 0.
*
* Background: the historical-invoice import only sent paidAmountMinor when
* the admin separately filled the "paid amount" field. Left blank (easy to
* miss — the total was already entered), it stored paid_amount_minor = 0
* even with status='paid'. The dashboard revenue windows sum
* paid_amount_minor (not total), so those paid imports contributed NOTHING
* to revenue. The import route now defaults a blank paid amount to the
* total; this fixes the rows already created before that change.
*
* Scope: imported (imported_pdf_path set) + status='paid' + paid_amount_minor
* 0/null → set paid_amount_minor = total_amount_minor (fully paid). Operational
* payment field, not immutable legal content (same reasoning as migration 111).
*
* Idempotent: re-running sets the same value; rows already > 0 are untouched.
*/
exports.up = async function(knex) {
if (!(await knex.schema.hasTable('invoices'))) return;
if (!(await knex.schema.hasColumn('invoices', 'imported_pdf_path'))) return;
if (!(await knex.schema.hasColumn('invoices', 'paid_amount_minor'))) return;
await knex('invoices')
.whereNotNull('imported_pdf_path')
.where('status', 'paid')
.andWhere(function() {
this.where('paid_amount_minor', 0).orWhereNull('paid_amount_minor');
})
.update({ paid_amount_minor: knex.raw('total_amount_minor') });
};
exports.down = async function() {
// Irreversible data backfill — we can't tell which rows we changed apart
// from legitimately-full payments. No-op.
};
+8 -1
View File
@@ -471,8 +471,15 @@ router.post(
}
const totalMinor = parseInt(req.body.totalAmountMinor, 10);
const paidMinor = parseInt(req.body.paidAmountMinor || '0', 10) || 0;
const status = req.body.status || 'sent';
// A paid import with no explicit paid amount means FULLY paid — default
// paid_amount_minor to the total. The dashboard revenue windows sum
// paid_amount_minor (not total), so a blank paid amount used to store 0
// and the paid invoice contributed nothing to revenue.
const explicitPaid = req.body.paidAmountMinor != null && String(req.body.paidAmountMinor) !== '';
const paidMinor = explicitPaid
? (parseInt(req.body.paidAmountMinor, 10) || 0)
: (status === 'paid' ? totalMinor : 0);
const issueDate = req.body.issueDate;
const dueDate = req.body.dueDate || issueDate;
// Imported docs are historical: their real send/payment dates are