Files
marketing-app/server/budget-helpers.js
fahed 49e1a796ed fix: code review — security, dead code, performance, consistency
Critical fixes:
- XSS: escapeHtml() on all user-supplied text in email notifications
- Budget PATCH: added mutex lock + availability validation (prevents corruption)
- batchResolveNames: fixed wrong signature for budget request earmark names

Dead code cleanup:
- Deleted 8 unused PostComposition* files (replaced by PostDetail full page)

Performance:
- budget-helpers: single-fetch with computeFromEntries(), optional prefetch param
- post-composition: parallelized text + thumbnail fetches with Promise.all

Consistency:
- PostDetail.jsx: native <select> → PortalSelect (matches all panels)
- Finance.jsx: 11 hardcoded English table headers → t() with i18n keys
- PostCalendar.jsx: day names, Month/Week labels → t() with i18n keys
- App.jsx Suspense: "Loading..." → brand spinner (can't use i18n in fallback)
- UploadZone: proper useRef pattern, no vanilla JS document.createElement
- All file inputs: className="hidden" → absolute w-0 h-0 opacity-0
- ArtefactDetailPanel: removed campaign/project selects (inherited from post)
- TranslationDetailPanel: removed brand/linked post selects (inherited from post)
- ApproverMultiSelect: portal-based dropdown (fixes clipping in modals)
- Thumbnail fix: post-composition constructs URL from filename (was undefined)
- Upload fix: UploadZone with drag-and-drop for design + video artefacts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 14:17:08 +03:00

51 lines
2.3 KiB
JavaScript

// server/budget-helpers.js — Budget availability calculations
// Single source of truth: BudgetEntries table
const nocodb = require('./nocodb');
function computeFromEntries(entries) {
const income = entries.filter(e => (e.type || 'income') === 'income');
const expenses = entries.filter(e => e.type === 'expense');
const totalReceived = income.reduce((s, e) => s + (e.amount || 0), 0);
const totalExpenses = expenses.reduce((s, e) => s + (e.amount || 0), 0);
const totalCampaignBudget = income.filter(e => e.campaign_id).reduce((s, e) => s + (e.amount || 0), 0);
const totalProjectBudget = income.filter(e => e.project_id).reduce((s, e) => s + (e.amount || 0), 0);
return { totalReceived, totalExpenses, totalCampaignBudget, totalProjectBudget, available: totalReceived - totalExpenses - totalCampaignBudget - totalProjectBudget };
}
async function getMainAvailable(prefetchedEntries) {
const entries = prefetchedEntries || await nocodb.list('BudgetEntries', { limit: 10000 });
return computeFromEntries(entries);
}
async function getCampaignAvailable(campaignId, prefetchedEntries) {
const entries = prefetchedEntries || await nocodb.list('BudgetEntries', { limit: 10000 });
const campaignIncome = entries.filter(e =>
e.campaign_id && Number(e.campaign_id) === Number(campaignId) && (e.type || 'income') === 'income'
);
const allocated = campaignIncome.reduce((s, e) => s + (e.amount || 0), 0);
const tracks = await nocodb.list('CampaignTracks', {
where: `(campaign_id,eq,${campaignId})`,
limit: 10000,
});
const trackAllocated = tracks.reduce((s, t) => s + (t.budget_allocated || 0), 0);
return { allocated, trackAllocated, available: allocated - trackAllocated };
}
async function getCampaignAllocatedFromEntries(campaignId, prefetchedEntries) {
const entries = prefetchedEntries || await nocodb.list('BudgetEntries', { limit: 10000 });
return entries
.filter(e => e.campaign_id && Number(e.campaign_id) === Number(campaignId) && (e.type || 'income') === 'income')
.reduce((s, e) => s + (e.amount || 0), 0);
}
async function getAllBudgetData() {
const entries = await nocodb.list('BudgetEntries', { limit: 10000 });
const main = computeFromEntries(entries);
return { entries, ...main };
}
module.exports = { getMainAvailable, getCampaignAvailable, getCampaignAllocatedFromEntries, getAllBudgetData, computeFromEntries };