Password Policy/App rename/PM Scheduling Improvements
This commit is contained in:
@@ -14,6 +14,50 @@ function yearOf(value) {
|
||||
return new Date(`${value}T00:00:00`).getFullYear();
|
||||
}
|
||||
|
||||
function monthOf(value) {
|
||||
if (!value) return null;
|
||||
return new Date(`${value}T00:00:00`).getMonth() + 1;
|
||||
}
|
||||
|
||||
// A date falls in the period when the year matches and (no month filter, or the month matches).
|
||||
function inPeriod(date, year, month) {
|
||||
if (yearOf(date) !== year) return false;
|
||||
if (month && monthOf(date) !== month) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const REVALUATION_TYPES = ['revaluation', 'write_up', 'write_down'];
|
||||
|
||||
// Adjustment rows joined to their asset, filtered by period and (optionally) a set of types.
|
||||
function adjustmentRows(year, month, types, entityId) {
|
||||
return all(
|
||||
`SELECT adj.*, a.asset_id, a.description, a.category, e.name AS entity_name, u.name AS created_by_name
|
||||
FROM asset_adjustments adj
|
||||
JOIN assets a ON a.id = adj.asset_id
|
||||
LEFT JOIN entities e ON e.id = a.entity_id
|
||||
LEFT JOIN users u ON u.id = adj.created_by
|
||||
WHERE (? IS NULL OR a.entity_id = ?)
|
||||
ORDER BY adj.adjustment_date`,
|
||||
[entityId || null, entityId || null]
|
||||
).filter((row) => inPeriod(row.adjustment_date, year, month) && (!types || types.includes(row.type)));
|
||||
}
|
||||
|
||||
// Generic aggregation: group items by keyFn, summing the given numeric fields and counting rows.
|
||||
function groupSummary(items, keyFn, fields) {
|
||||
const groups = {};
|
||||
for (const item of items) {
|
||||
const key = keyFn(item) || 'Uncategorized';
|
||||
const group = groups[key] || (groups[key] = { key, count: 0 });
|
||||
group.count += 1;
|
||||
for (const field of fields) group[field] = (group[field] || 0) + Number(item[field] || 0);
|
||||
}
|
||||
return Object.values(groups).map((group) => {
|
||||
const out = { key: group.key, count: group.count };
|
||||
for (const field of fields) out[field] = money(group[field]);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
function sumBy(rows, keys) {
|
||||
const totals = {};
|
||||
for (const key of keys) totals[key] = money(rows.reduce((sum, row) => sum + Number(row[key] || 0), 0));
|
||||
@@ -210,13 +254,14 @@ function lifetimeDepreciation(options) {
|
||||
|
||||
function acquisitions(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const rows = all(
|
||||
`SELECT a.asset_id, a.description, a.category, a.acquired_date, a.vendor, a.invoice_number, a.acquisition_cost
|
||||
FROM assets a WHERE a.acquired_date IS NOT NULL
|
||||
AND (? IS NULL OR a.entity_id = ?)
|
||||
ORDER BY a.acquired_date`,
|
||||
[options.entity_id || null, options.entity_id || null]
|
||||
).filter((row) => yearOf(row.acquired_date) === year)
|
||||
).filter((row) => inPeriod(row.acquired_date, year, month))
|
||||
.map((row) => ({ ...row, acquisition_cost: money(row.acquisition_cost) }));
|
||||
return {
|
||||
columns: [
|
||||
@@ -241,7 +286,10 @@ function disposalRows(year, entityId) {
|
||||
|
||||
function disposals(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const rows = disposalRows(year, options.entity_id).map((row) => ({
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const rows = disposalRows(year, options.entity_id)
|
||||
.filter((row) => !month || monthOf(row.disposal_date) === month)
|
||||
.map((row) => ({
|
||||
asset_id: row.asset_id,
|
||||
description: row.description,
|
||||
method: row.method,
|
||||
@@ -351,15 +399,17 @@ function projection(options) {
|
||||
function journalDepreciation(options) {
|
||||
const book = options.book || 'GAAP';
|
||||
const year = Number(options.year) || currentYear();
|
||||
const factor = options.monthlyFactor || 1;
|
||||
const rules = ruleSetForBook(book);
|
||||
const accounts = {};
|
||||
for (const { asset, book: bk } of assetBookRows(book, options).map(toAssetBook)) {
|
||||
const c = computeYear(asset, bk, rules, year);
|
||||
if (!c.depreciation) continue;
|
||||
const amount = money(c.depreciation * factor);
|
||||
if (!amount) continue;
|
||||
const expense = asset.gl_expense_account || '6400';
|
||||
const accum = asset.gl_accumulated_account || '1590';
|
||||
accounts[expense] = (accounts[expense] || 0) + c.depreciation;
|
||||
accounts[`__accum__${accum}`] = (accounts[`__accum__${accum}`] || 0) + c.depreciation;
|
||||
accounts[expense] = (accounts[expense] || 0) + amount;
|
||||
accounts[`__accum__${accum}`] = (accounts[`__accum__${accum}`] || 0) + amount;
|
||||
}
|
||||
const rows = [];
|
||||
for (const [key, amount] of Object.entries(accounts)) {
|
||||
@@ -379,8 +429,10 @@ function journalDepreciation(options) {
|
||||
|
||||
function journalDisposals(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const rows = [];
|
||||
for (const d of disposalRows(year, options.entity_id)) {
|
||||
if (month && monthOf(d.disposal_date) !== month) continue;
|
||||
const proceeds = money(d.disposal_price - d.disposal_expense);
|
||||
rows.push({ asset_id: d.asset_id, memo: 'Cash / proceeds', debit: proceeds, credit: 0 });
|
||||
rows.push({ asset_id: d.asset_id, memo: 'Accumulated depreciation', debit: money(d.accumulated_depreciation), credit: 0 });
|
||||
@@ -721,6 +773,314 @@ function alertsSince(options) {
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Revaluation & write-off (transactions, journals, summaries) -----------
|
||||
|
||||
function revaluationTransactions(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const rows = adjustmentRows(year, month, REVALUATION_TYPES, options.entity_id).map((r) => ({
|
||||
adjustment_date: r.adjustment_date, asset_id: r.asset_id, description: r.description,
|
||||
type: String(r.type).replace(/_/g, ' '), book_type: r.book_type, amount: money(r.amount), reason: r.reason
|
||||
}));
|
||||
return {
|
||||
columns: [
|
||||
col('adjustment_date', 'Date', 'date'), col('asset_id', 'Asset ID'), col('description', 'Description'),
|
||||
col('type', 'Type'), col('book_type', 'Book'), col('amount', 'Amount', 'currency'), col('reason', 'Reason')
|
||||
],
|
||||
rows,
|
||||
totals: sumBy(rows, ['amount'])
|
||||
};
|
||||
}
|
||||
|
||||
// Write-offs are impairment adjustments plus abandonment disposals (asset removed at no value).
|
||||
function writeoffItems(year, month, entityId) {
|
||||
const impairments = adjustmentRows(year, month, ['impairment'], entityId).map((r) => ({
|
||||
date: r.adjustment_date, asset_id: r.asset_id, description: r.description, category: r.category,
|
||||
source: 'Impairment', book_type: r.book_type, amount: money(r.amount), reason: r.reason
|
||||
}));
|
||||
const abandonments = disposalRows(year, entityId)
|
||||
.filter((d) => d.method === 'abandonment' && (!month || monthOf(d.disposal_date) === month))
|
||||
.map((d) => ({
|
||||
date: d.disposal_date, asset_id: d.asset_id, description: d.description, category: d.category,
|
||||
source: 'Abandonment', book_type: d.book_type, amount: money(d.net_book_value), reason: d.notes
|
||||
}));
|
||||
return [...impairments, ...abandonments].sort((a, b) => String(a.date).localeCompare(String(b.date)));
|
||||
}
|
||||
|
||||
function writeoffTransactions(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const rows = writeoffItems(year, month, options.entity_id);
|
||||
return {
|
||||
columns: [
|
||||
col('date', 'Date', 'date'), col('asset_id', 'Asset ID'), col('description', 'Description'),
|
||||
col('source', 'Source'), col('book_type', 'Book'), col('amount', 'Amount', 'currency'), col('reason', 'Reason')
|
||||
],
|
||||
rows: rows.map(({ category, ...rest }) => rest),
|
||||
totals: sumBy(rows, ['amount'])
|
||||
};
|
||||
}
|
||||
|
||||
function journalRevaluation(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const rows = [];
|
||||
for (const r of adjustmentRows(year, month, REVALUATION_TYPES, options.entity_id)) {
|
||||
const amount = money(r.amount);
|
||||
if (!amount) continue;
|
||||
// write_up / revaluation increase carrying value (Dr asset, Cr revaluation surplus); write_down reverses.
|
||||
const up = r.type !== 'write_down';
|
||||
rows.push({ asset_id: r.asset_id, memo: 'Asset carrying value', debit: up ? amount : 0, credit: up ? 0 : amount });
|
||||
rows.push({ asset_id: r.asset_id, memo: 'Revaluation surplus / reserve', debit: up ? 0 : amount, credit: up ? amount : 0 });
|
||||
}
|
||||
return {
|
||||
columns: [col('asset_id', 'Asset ID'), col('memo', 'Memo'), col('debit', 'Debit', 'currency'), col('credit', 'Credit', 'currency')],
|
||||
rows,
|
||||
totals: sumBy(rows, ['debit', 'credit'])
|
||||
};
|
||||
}
|
||||
|
||||
function journalWriteoff(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const rows = [];
|
||||
for (const item of writeoffItems(year, month, options.entity_id)) {
|
||||
const amount = money(item.amount);
|
||||
if (!amount) continue;
|
||||
rows.push({ asset_id: item.asset_id, memo: `${item.source} loss / expense`, debit: amount, credit: 0 });
|
||||
rows.push({ asset_id: item.asset_id, memo: 'Asset / accumulated depreciation', debit: 0, credit: amount });
|
||||
}
|
||||
return {
|
||||
columns: [col('asset_id', 'Asset ID'), col('memo', 'Memo'), col('debit', 'Debit', 'currency'), col('credit', 'Credit', 'currency')],
|
||||
rows,
|
||||
totals: sumBy(rows, ['debit', 'credit'])
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Summaries -------------------------------------------------------------
|
||||
|
||||
function bookSummaryItems(options) {
|
||||
const book = options.book || 'GAAP';
|
||||
const year = Number(options.year) || currentYear();
|
||||
const rules = ruleSetForBook(book);
|
||||
return assetBookRows(book, options).map(toAssetBook).map(({ asset, book: bk }) => {
|
||||
const c = computeYear(asset, bk, rules, year);
|
||||
return {
|
||||
category: asset.category || 'Uncategorized',
|
||||
cost: c.cost, depreciation: c.depreciation, accumulated: c.accumulated_end, nbv: c.nbv_end
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function assetSummary(options) {
|
||||
const rows = groupSummary(bookSummaryItems(options), (i) => i.category, ['cost', 'accumulated', 'nbv'])
|
||||
.map((g) => ({ category: g.key || 'Uncategorized', count: g.count, cost: g.cost, accumulated: g.accumulated, nbv: g.nbv }));
|
||||
return {
|
||||
columns: [
|
||||
col('category', 'Category'), col('count', 'Assets', 'number'), col('cost', 'Cost', 'currency'),
|
||||
col('accumulated', 'Accum. depr.', 'currency'), col('nbv', 'Net book value', 'currency')
|
||||
],
|
||||
rows,
|
||||
totals: { count: rows.reduce((s, r) => s + r.count, 0), ...sumBy(rows, ['cost', 'accumulated', 'nbv']) }
|
||||
};
|
||||
}
|
||||
|
||||
function depreciationSummary(options) {
|
||||
const rows = groupSummary(bookSummaryItems(options), (i) => i.category, ['cost', 'depreciation', 'accumulated', 'nbv'])
|
||||
.map((g) => ({ category: g.key, count: g.count, cost: g.cost, depreciation: g.depreciation, accumulated: g.accumulated, nbv: g.nbv }));
|
||||
return {
|
||||
columns: [
|
||||
col('category', 'Category'), col('count', 'Assets', 'number'), col('cost', 'Cost', 'currency'),
|
||||
col('depreciation', 'Depreciation', 'currency'), col('accumulated', 'Accum. depr.', 'currency'), col('nbv', 'Net book value', 'currency')
|
||||
],
|
||||
rows,
|
||||
totals: { count: rows.reduce((s, r) => s + r.count, 0), ...sumBy(rows, ['cost', 'depreciation', 'accumulated', 'nbv']) }
|
||||
};
|
||||
}
|
||||
|
||||
function ytdDepreciationSummary(options) {
|
||||
const month = Math.min(12, Math.max(1, Number(options.month) || 12));
|
||||
const items = bookSummaryItems(options).map((i) => ({ ...i, ytd: money((i.depreciation * month) / 12) }));
|
||||
const rows = groupSummary(items, (i) => i.category, ['depreciation', 'ytd', 'accumulated', 'nbv'])
|
||||
.map((g) => ({ category: g.key, count: g.count, depreciation: g.depreciation, ytd: g.ytd, accumulated: g.accumulated, nbv: g.nbv }));
|
||||
return {
|
||||
columns: [
|
||||
col('category', 'Category'), col('count', 'Assets', 'number'), col('depreciation', 'Annual', 'currency'),
|
||||
col('ytd', `YTD through M${month}`, 'currency'), col('accumulated', 'Accum. depr.', 'currency'), col('nbv', 'Net book value', 'currency')
|
||||
],
|
||||
rows,
|
||||
totals: { count: rows.reduce((s, r) => s + r.count, 0), ...sumBy(rows, ['depreciation', 'ytd', 'accumulated', 'nbv']) }
|
||||
};
|
||||
}
|
||||
|
||||
function purchaseSummary(options) {
|
||||
const items = acquisitions(options).rows.map((r) => ({ category: r.category || 'Uncategorized', cost: r.acquisition_cost }));
|
||||
const rows = groupSummary(items, (i) => i.category, ['cost']).map((g) => ({ category: g.key, count: g.count, cost: g.cost }));
|
||||
return {
|
||||
columns: [col('category', 'Category'), col('count', 'Purchases', 'number'), col('cost', 'Cost', 'currency')],
|
||||
rows,
|
||||
totals: { count: rows.reduce((s, r) => s + r.count, 0), ...sumBy(rows, ['cost']) }
|
||||
};
|
||||
}
|
||||
|
||||
function disposalSummary(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const items = disposalRows(year, options.entity_id)
|
||||
.filter((d) => !month || monthOf(d.disposal_date) === month)
|
||||
.map((d) => ({
|
||||
method: String(d.method).replace(/_/g, ' '),
|
||||
proceeds: money(d.disposal_price - d.disposal_expense), nbv: money(d.net_book_value), gain_loss: money(d.gain_loss)
|
||||
}));
|
||||
const rows = groupSummary(items, (i) => i.method, ['proceeds', 'nbv', 'gain_loss'])
|
||||
.map((g) => ({ method: g.key, count: g.count, proceeds: g.proceeds, nbv: g.nbv, gain_loss: g.gain_loss }));
|
||||
return {
|
||||
columns: [
|
||||
col('method', 'Method'), col('count', 'Disposals', 'number'), col('proceeds', 'Net proceeds', 'currency'),
|
||||
col('nbv', 'Net book value', 'currency'), col('gain_loss', 'Gain / (loss)', 'currency')
|
||||
],
|
||||
rows,
|
||||
totals: { count: rows.reduce((s, r) => s + r.count, 0), ...sumBy(rows, ['proceeds', 'nbv', 'gain_loss']) }
|
||||
};
|
||||
}
|
||||
|
||||
function revaluationSummary(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const items = adjustmentRows(year, month, REVALUATION_TYPES, options.entity_id)
|
||||
.map((r) => ({ type: String(r.type).replace(/_/g, ' '), amount: money(r.amount) }));
|
||||
const rows = groupSummary(items, (i) => i.type, ['amount']).map((g) => ({ type: g.key, count: g.count, amount: g.amount }));
|
||||
return {
|
||||
columns: [col('type', 'Type'), col('count', 'Count', 'number'), col('amount', 'Net amount', 'currency')],
|
||||
rows,
|
||||
totals: { count: rows.reduce((s, r) => s + r.count, 0), ...sumBy(rows, ['amount']) }
|
||||
};
|
||||
}
|
||||
|
||||
function writeoffSummary(options) {
|
||||
const year = Number(options.year) || currentYear();
|
||||
const month = options.txn_month ? Number(options.txn_month) : null;
|
||||
const items = writeoffItems(year, month, options.entity_id);
|
||||
const rows = groupSummary(items, (i) => i.category, ['amount']).map((g) => ({ category: g.key, count: g.count, amount: g.amount }));
|
||||
return {
|
||||
columns: [col('category', 'Category'), col('count', 'Write-offs', 'number'), col('amount', 'Amount', 'currency')],
|
||||
rows,
|
||||
totals: { count: rows.reduce((s, r) => s + r.count, 0), ...sumBy(rows, ['amount']) }
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Registers, journals & history -----------------------------------------
|
||||
|
||||
function fixedAssetRegister(options) {
|
||||
const book = options.book || 'GAAP';
|
||||
const year = Number(options.year) || currentYear();
|
||||
const rules = ruleSetForBook(book);
|
||||
const rows = assetBookRows(book, options).map(toAssetBook).map(({ asset, book: bk }) => {
|
||||
const c = computeYear(asset, bk, rules, year);
|
||||
return {
|
||||
asset_id: asset.asset_id, description: asset.description, category: asset.category, status: asset.status,
|
||||
acquired_date: asset.acquired_date, in_service_date: asset.in_service_date,
|
||||
location: asset.location, custodian: asset.custodian,
|
||||
cost: c.cost, accumulated: c.accumulated_end, nbv: c.nbv_end
|
||||
};
|
||||
});
|
||||
return {
|
||||
columns: [
|
||||
col('asset_id', 'Asset ID'), col('description', 'Description'), col('category', 'Category'), col('status', 'Status'),
|
||||
col('acquired_date', 'Acquired', 'date'), col('in_service_date', 'In service', 'date'),
|
||||
col('location', 'Location'), col('custodian', 'Custodian'),
|
||||
col('cost', 'Cost', 'currency'), col('accumulated', 'Accum. depr.', 'currency'), col('nbv', 'Net book value', 'currency')
|
||||
],
|
||||
rows,
|
||||
totals: sumBy(rows, ['cost', 'accumulated', 'nbv'])
|
||||
};
|
||||
}
|
||||
|
||||
// Comprehensive per-asset dossier: per-book financials (reused from the asset card) plus rich
|
||||
// sections (maintenance, notes, warranties, adjustments, disposals) and photo thumbnails for PDF.
|
||||
function assetJournal(options) {
|
||||
const card = fixedAssetCard(options);
|
||||
if (!card.meta || !card.meta.asset) return card;
|
||||
const asset = one('SELECT * FROM assets WHERE id = ? OR asset_id = ? LIMIT 1', [options.asset_id, options.asset_id]);
|
||||
const id = asset.id;
|
||||
|
||||
const maintenance = all(
|
||||
`SELECT c.completed_at, p.name AS plan_name, u.name AS by_name, c.cost
|
||||
FROM pm_completions c JOIN asset_pm_plans ap ON ap.id = c.asset_pm_id
|
||||
LEFT JOIN pm_plans p ON p.id = ap.plan_id LEFT JOIN users u ON u.id = c.completed_by
|
||||
WHERE ap.asset_id = ? ORDER BY c.completed_at DESC`, [id]
|
||||
).map((r) => ({ completed_at: String(r.completed_at).slice(0, 10), plan_name: r.plan_name || 'Maintenance', by_name: r.by_name, cost: money(r.cost) }));
|
||||
|
||||
const notes = all(
|
||||
`SELECT n.created_at, n.body, u.name AS by_name FROM asset_notes n
|
||||
LEFT JOIN users u ON u.id = n.created_by WHERE n.asset_id = ? ORDER BY n.created_at DESC`, [id]
|
||||
).map((r) => ({ created_at: String(r.created_at).slice(0, 10), by_name: r.by_name, body: r.body }));
|
||||
|
||||
const warranties = all(
|
||||
'SELECT provider, start_date, expiration_date, warranty_limit, coverage_details FROM warranties WHERE asset_id = ? ORDER BY expiration_date', [id]
|
||||
);
|
||||
|
||||
const adjustments = all(
|
||||
'SELECT adjustment_date, type, book_type, amount, reason FROM asset_adjustments WHERE asset_id = ? ORDER BY adjustment_date DESC', [id]
|
||||
).map((r) => ({ adjustment_date: r.adjustment_date, type: String(r.type).replace(/_/g, ' '), book_type: r.book_type, amount: money(r.amount), reason: r.reason }));
|
||||
|
||||
const disposalsList = all(
|
||||
'SELECT disposal_date, method, net_book_value, gain_loss FROM disposals WHERE asset_id = ? ORDER BY disposal_date DESC', [id]
|
||||
).map((r) => ({ disposal_date: r.disposal_date, method: String(r.method).replace(/_/g, ' '), net_book_value: money(r.net_book_value), gain_loss: money(r.gain_loss) }));
|
||||
|
||||
const sections = [
|
||||
{ title: 'Maintenance history', columns: [col('completed_at', 'Completed', 'date'), col('plan_name', 'Plan'), col('by_name', 'By'), col('cost', 'Cost', 'currency')], rows: maintenance },
|
||||
{ title: 'Notes', columns: [col('created_at', 'Date', 'date'), col('by_name', 'By'), col('body', 'Note')], rows: notes },
|
||||
{ title: 'Warranties', columns: [col('provider', 'Provider'), col('start_date', 'Start', 'date'), col('expiration_date', 'Expires', 'date'), col('warranty_limit', 'Limit'), col('coverage_details', 'Coverage')], rows: warranties },
|
||||
{ title: 'Adjustments & revaluations', columns: [col('adjustment_date', 'Date', 'date'), col('type', 'Type'), col('book_type', 'Book'), col('amount', 'Amount', 'currency'), col('reason', 'Reason')], rows: adjustments }
|
||||
];
|
||||
if (disposalsList.length) {
|
||||
sections.push({ title: 'Disposals', columns: [col('disposal_date', 'Date', 'date'), col('method', 'Type'), col('net_book_value', 'NBV', 'currency'), col('gain_loss', 'Gain / (loss)', 'currency')], rows: disposalsList });
|
||||
}
|
||||
|
||||
const photoRows = all('SELECT name, caption, data_base64 FROM asset_photos WHERE asset_id = ? ORDER BY sort_order, id LIMIT 12', [id]);
|
||||
const photos = photoRows.map((p) => ({ caption: p.caption || p.name || 'Photo', data: p.data_base64 }));
|
||||
const photoCount = one('SELECT COUNT(*) AS c FROM asset_photos WHERE asset_id = ?', [id]).c;
|
||||
|
||||
return { ...card, meta: { ...card.meta, layout: 'dossier', sections, photos, photo_count: photoCount } };
|
||||
}
|
||||
|
||||
function assetHistory(options) {
|
||||
const row = options.asset_id ? one('SELECT * FROM assets WHERE id = ? OR asset_id = ? LIMIT 1', [options.asset_id, options.asset_id]) : null;
|
||||
const asset = row ? assetFromRow(row) : null;
|
||||
if (!asset) return { columns: [], rows: [], totals: {}, meta: { note: 'Select an asset to view its history.' } };
|
||||
const id = asset.id;
|
||||
const events = [];
|
||||
if (asset.acquired_date) events.push({ date: asset.acquired_date, event: 'Acquired', detail: [asset.vendor, asset.invoice_number ? `#${asset.invoice_number}` : ''].filter(Boolean).join(' '), by: '', amount: money(asset.acquisition_cost) });
|
||||
if (asset.in_service_date) events.push({ date: asset.in_service_date, event: 'Placed in service', detail: asset.description, by: '', amount: '' });
|
||||
for (const r of all('SELECT * FROM asset_adjustments WHERE asset_id = ?', [id])) {
|
||||
events.push({ date: r.adjustment_date, event: String(r.type).replace(/_/g, ' '), detail: [r.book_type, r.reason].filter(Boolean).join(' · '), by: '', amount: money(r.amount) });
|
||||
}
|
||||
for (const d of all('SELECT * FROM disposals WHERE asset_id = ?', [id])) {
|
||||
events.push({ date: d.disposal_date, event: `Disposal (${String(d.method).replace(/_/g, ' ')})`, detail: `Gain/(loss) ${money(d.gain_loss)}`, by: '', amount: money(d.net_book_value) });
|
||||
}
|
||||
for (const c of all('SELECT c.completed_at, c.cost, u.name AS by_name FROM pm_completions c JOIN asset_pm_plans ap ON ap.id = c.asset_pm_id LEFT JOIN users u ON u.id = c.completed_by WHERE ap.asset_id = ?', [id])) {
|
||||
events.push({ date: String(c.completed_at).slice(0, 10), event: 'PM completed', detail: '', by: c.by_name, amount: money(c.cost) });
|
||||
}
|
||||
for (const n of all('SELECT n.created_at, n.body, u.name AS by_name FROM asset_notes n LEFT JOIN users u ON u.id = n.created_by WHERE n.asset_id = ?', [id])) {
|
||||
events.push({ date: String(n.created_at).slice(0, 10), event: 'Note', detail: n.body, by: n.by_name, amount: '' });
|
||||
}
|
||||
for (const w of all('SELECT provider, start_date FROM warranties WHERE asset_id = ?', [id])) {
|
||||
events.push({ date: w.start_date || '', event: 'Warranty added', detail: w.provider || '', by: '', amount: '' });
|
||||
}
|
||||
for (const lg of all("SELECT al.action, al.created_at, u.name AS by_name FROM audit_logs al LEFT JOIN users u ON u.id = al.actor_id WHERE al.entity_type = 'asset' AND al.entity_id = ?", [String(id)])) {
|
||||
if (['dispose', 'adjust', 'reverse_disposal'].includes(lg.action)) continue;
|
||||
events.push({ date: String(lg.created_at).slice(0, 10), event: String(lg.action).replace(/_/g, ' '), detail: '', by: lg.by_name, amount: '' });
|
||||
}
|
||||
events.sort((a, b) => String(a.date).localeCompare(String(b.date)));
|
||||
return {
|
||||
columns: [col('date', 'Date', 'date'), col('event', 'Event'), col('detail', 'Detail'), col('by', 'By'), col('amount', 'Amount', 'currency')],
|
||||
rows: events,
|
||||
totals: {},
|
||||
meta: { asset: { asset_id: asset.asset_id, description: asset.description, status: asset.status }, note: `${events.length} event(s) recorded.` }
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Registry --------------------------------------------------------------
|
||||
|
||||
const REPORTS = {
|
||||
@@ -750,7 +1110,34 @@ const REPORTS = {
|
||||
pm_plan: { title: 'PM plan (printable)', group: 'Maintenance', params: ['pm_plan_id'], build: (o) => pmPlanPrint(o) },
|
||||
alerts_since: { title: 'Alerts since date', group: 'Maintenance', params: ['since_date'], build: (o) => alertsSince(o) },
|
||||
fixed_asset_card: { title: 'Fixed asset card', group: 'Detail', params: ['asset_id', 'year'], build: (o) => fixedAssetCard(o) },
|
||||
custom: { title: 'Report builder (custom)', group: 'Detail', params: ['columns', 'entity_id', 'category', 'status'], build: (o) => customReport(o) }
|
||||
custom: { title: 'Report builder (custom)', group: 'Detail', params: ['columns', 'entity_id', 'category', 'status'], build: (o) => customReport(o) },
|
||||
|
||||
// Journals & registers
|
||||
asset_journal: { title: 'Asset journal (full dossier)', group: 'Journals & registers', params: ['asset_id', 'year'], build: (o) => assetJournal(o) },
|
||||
fixed_asset_journal: { title: 'Fixed asset journal (register)', group: 'Journals & registers', params: ['book', 'year', 'entity_id', 'category', 'status'], build: (o) => fixedAssetRegister(o) },
|
||||
journal_depreciation_monthly: { title: 'Monthly depreciation journal entry', group: 'Journals & registers', params: ['book', 'year', 'month', 'entity_id'], build: (o) => journalDepreciation({ ...o, monthlyFactor: 1 / 12 }) },
|
||||
journal_disposals_monthly: { title: 'Monthly disposal journal entry', group: 'Journals & registers', params: ['year', 'txn_month', 'entity_id'], build: (o) => journalDisposals(o) },
|
||||
journal_revaluation: { title: 'Monthly revaluation journal entry', group: 'Journals & registers', params: ['book', 'year', 'txn_month', 'entity_id'], build: (o) => journalRevaluation(o) },
|
||||
journal_writeoff: { title: 'Monthly write-off journal entry', group: 'Journals & registers', params: ['book', 'year', 'txn_month', 'entity_id'], build: (o) => journalWriteoff(o) },
|
||||
|
||||
// Transactions
|
||||
txn_purchases: { title: 'Asset purchased (transactions)', group: 'Transactions', params: ['year', 'txn_month', 'entity_id'], build: (o) => acquisitions(o) },
|
||||
txn_disposals: { title: 'Asset disposed (transactions)', group: 'Transactions', params: ['year', 'txn_month', 'entity_id'], build: (o) => disposals(o) },
|
||||
txn_revaluations: { title: 'Asset revalued (transactions)', group: 'Transactions', params: ['year', 'txn_month', 'entity_id'], build: (o) => revaluationTransactions(o) },
|
||||
txn_writeoffs: { title: 'Asset write-off (transactions)', group: 'Transactions', params: ['year', 'txn_month', 'entity_id'], build: (o) => writeoffTransactions(o) },
|
||||
|
||||
// Summaries
|
||||
asset_summary: { title: 'Asset summary', group: 'Summaries', params: ['book', 'year', 'entity_id', 'category', 'status'], build: (o) => assetSummary(o) },
|
||||
depreciation_summary: { title: 'Asset depreciation summary', group: 'Summaries', params: ['book', 'year', 'entity_id', 'category', 'status'], build: (o) => depreciationSummary(o) },
|
||||
ytd_depreciation_summary: { title: 'YTD asset depreciation summary', group: 'Summaries', params: ['book', 'year', 'month', 'entity_id'], build: (o) => ytdDepreciationSummary(o) },
|
||||
purchase_summary: { title: 'Asset purchase summary', group: 'Summaries', params: ['year', 'txn_month', 'entity_id'], build: (o) => purchaseSummary(o) },
|
||||
disposal_summary: { title: 'Asset disposal summary', group: 'Summaries', params: ['year', 'txn_month', 'entity_id'], build: (o) => disposalSummary(o) },
|
||||
revaluation_summary: { title: 'Asset revaluation summary', group: 'Summaries', params: ['year', 'txn_month', 'entity_id'], build: (o) => revaluationSummary(o) },
|
||||
writeoff_summary: { title: 'Asset write-off summary', group: 'Summaries', params: ['year', 'txn_month', 'entity_id'], build: (o) => writeoffSummary(o) },
|
||||
|
||||
// Detail
|
||||
asset_history: { title: 'Asset history', group: 'Detail', params: ['asset_id'], build: (o) => assetHistory(o) },
|
||||
future_depreciation: { title: 'Future depreciation calculation', group: 'Depreciation', params: ['book', 'startYear', 'endYear', 'entity_id'], build: (o) => projection(o) }
|
||||
};
|
||||
|
||||
function paramSpecs() {
|
||||
@@ -775,6 +1162,7 @@ function paramSpecs() {
|
||||
book: { label: 'Book', type: 'select', options: bookCodes, default: bookCodes[0] || 'GAAP' },
|
||||
year: { label: 'Year', type: 'number', default: year },
|
||||
month: { label: 'Through month', type: 'select', options: Array.from({ length: 12 }, (_, i) => i + 1), default: 12 },
|
||||
txn_month: { label: 'Month (optional)', type: 'select', options: Array.from({ length: 12 }, (_, i) => i + 1), clearable: true, default: null },
|
||||
startYear: { label: 'Start year', type: 'number', default: year },
|
||||
endYear: { label: 'End year', type: 'number', default: year + 5 },
|
||||
entity_id: { label: 'Entity', type: 'select', options: entities, clearable: true, default: null },
|
||||
|
||||
Reference in New Issue
Block a user