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:
@@ -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.
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user