e1d1c392eb
Audit & Quality: - RTL: replaced 121 LTR-only utilities (text-left, pl-, left-) with logical properties - A11y: focus traps + ARIA on Modal/SlidePanel/TabbedModal, clickable divs→buttons - Theming: bg-white→bg-surface (170 instances), text-gray→semantic tokens - Performance: useMemo on filters, loading="lazy" on 24 images - CSS: prefers-reduced-motion, removed dead animations Component Splits: - PostDetailPanel: 1332→623 lines + 4 sub-components - ArtefactDetailPanel: 972→590 lines + 1 sub-component Brand Identity — Rawaj (رواج): - New name, DM Sans font, deep teal palette (#0d9488) - Custom SVG logo, forest-tinted dark mode - All emails branded with app name in subject line Design Refinement: - Dashboard: merged posts+deadlines into tabbed ActivityFeed, inline stats - Quieter: removed card lift, brand glow, gradient text, mesh backgrounds - CampaignDetail: prominent budget card, compact team avatars, Lucide icons - Consistent page titles via Header.jsx, standardized section headers - Finance page fully i18n'd (20+ hardcoded strings replaced) Budget Allocation Redesign: - Single source of truth: BudgetEntries (Campaign.budget deprecated) - Validation at all levels: main→campaign→track, expenses blocked if insufficient - Budget request workflow with CEO approval via public link - BudgetRequests table, CRUD routes, public approval page - Budget mutex for race condition prevention - Idempotent migration for existing campaign budgets Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
49 lines
2.0 KiB
JavaScript
49 lines
2.0 KiB
JavaScript
// server/budget-helpers.js — Budget availability calculations
|
|
// Single source of truth: BudgetEntries table
|
|
|
|
const nocodb = require('./nocodb');
|
|
|
|
async function getMainAvailable() {
|
|
const entries = await nocodb.list('BudgetEntries', { limit: 10000 });
|
|
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 getCampaignAvailable(campaignId) {
|
|
const entries = 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) {
|
|
const entries = 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);
|
|
}
|
|
|
|
module.exports = { getMainAvailable, getCampaignAvailable, getCampaignAllocatedFromEntries };
|