Bug & tweak sprint: fix financial calculations, add quarterly report, enhance dashboard
- Fix Accounts page: include investment accounts in Est. Monthly Interest calc, add Fund column to investment table, split summary cards into Operating/Reserve - Fix Cash Flow: ending balance now respects includeInvestments toggle - Fix Budget Manager: separate operating/reserve income in summary cards - Fix Projects: default sort by planned_date instead of name - Add Vendors: last_negotiated date field with migration, CSV import/export - New Quarterly Financial Report: budget vs actuals, over-budget flagging, YTD - Enhance Dashboard: separate Operating/Reserve fund cards, expanded Quick Stats with monthly interest, YTD interest earned, planned capital spend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
backend/src/modules/vendors/vendors.service.ts
vendored
27
backend/src/modules/vendors/vendors.service.ts
vendored
@@ -17,10 +17,10 @@ export class VendorsService {
|
||||
|
||||
async create(dto: any) {
|
||||
const rows = await this.tenant.query(
|
||||
`INSERT INTO vendors (name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible, default_account_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`,
|
||||
`INSERT INTO vendors (name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible, default_account_id, last_negotiated)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`,
|
||||
[dto.name, dto.contact_name, dto.email, dto.phone, dto.address_line1, dto.city, dto.state, dto.zip_code,
|
||||
dto.tax_id, dto.is_1099_eligible || false, dto.default_account_id || null],
|
||||
dto.tax_id, dto.is_1099_eligible || false, dto.default_account_id || null, dto.last_negotiated || null],
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
@@ -32,24 +32,25 @@ export class VendorsService {
|
||||
email = COALESCE($4, email), phone = COALESCE($5, phone), address_line1 = COALESCE($6, address_line1),
|
||||
city = COALESCE($7, city), state = COALESCE($8, state), zip_code = COALESCE($9, zip_code),
|
||||
tax_id = COALESCE($10, tax_id), is_1099_eligible = COALESCE($11, is_1099_eligible),
|
||||
default_account_id = COALESCE($12, default_account_id), updated_at = NOW()
|
||||
default_account_id = COALESCE($12, default_account_id), last_negotiated = $13, updated_at = NOW()
|
||||
WHERE id = $1 RETURNING *`,
|
||||
[id, dto.name, dto.contact_name, dto.email, dto.phone, dto.address_line1, dto.city, dto.state,
|
||||
dto.zip_code, dto.tax_id, dto.is_1099_eligible, dto.default_account_id],
|
||||
dto.zip_code, dto.tax_id, dto.is_1099_eligible, dto.default_account_id, dto.last_negotiated || null],
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async exportCSV(): Promise<string> {
|
||||
const rows = await this.tenant.query(
|
||||
`SELECT name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible
|
||||
`SELECT name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible, last_negotiated
|
||||
FROM vendors WHERE is_active = true ORDER BY name`,
|
||||
);
|
||||
const headers = ['name', 'contact_name', 'email', 'phone', 'address_line1', 'city', 'state', 'zip_code', 'tax_id', 'is_1099_eligible'];
|
||||
const headers = ['name', 'contact_name', 'email', 'phone', 'address_line1', 'city', 'state', 'zip_code', 'tax_id', 'is_1099_eligible', 'last_negotiated'];
|
||||
const lines = [headers.join(',')];
|
||||
for (const r of rows) {
|
||||
lines.push(headers.map((h) => {
|
||||
const v = r[h] ?? '';
|
||||
let v = r[h] ?? '';
|
||||
if (v instanceof Date) v = v.toISOString().split('T')[0];
|
||||
const s = String(v);
|
||||
return s.includes(',') || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
}).join(','));
|
||||
@@ -80,20 +81,22 @@ export class VendorsService {
|
||||
zip_code = COALESCE(NULLIF($8, ''), zip_code),
|
||||
tax_id = COALESCE(NULLIF($9, ''), tax_id),
|
||||
is_1099_eligible = COALESCE(NULLIF($10, '')::boolean, is_1099_eligible),
|
||||
last_negotiated = COALESCE(NULLIF($11, '')::date, last_negotiated),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
[existing[0].id, row.contact_name, row.email, row.phone, row.address_line1,
|
||||
row.city, row.state, row.zip_code, row.tax_id, row.is_1099_eligible],
|
||||
row.city, row.state, row.zip_code, row.tax_id, row.is_1099_eligible, row.last_negotiated],
|
||||
);
|
||||
updated++;
|
||||
} else {
|
||||
await this.tenant.query(
|
||||
`INSERT INTO vendors (name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
`INSERT INTO vendors (name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible, last_negotiated)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
||||
[name, row.contact_name || null, row.email || null, row.phone || null,
|
||||
row.address_line1 || null, row.city || null, row.state || null,
|
||||
row.zip_code || null, row.tax_id || null,
|
||||
row.is_1099_eligible === 'true' || row.is_1099_eligible === true || false],
|
||||
row.is_1099_eligible === 'true' || row.is_1099_eligible === true || false,
|
||||
row.last_negotiated || null],
|
||||
);
|
||||
created++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user