feat: comprehensive UI overhaul + budget allocation redesign
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>
This commit is contained in:
+70
-5
@@ -4,8 +4,8 @@ const nocodb = require('./nocodb');
|
||||
const { parseApproverIds } = require('./helpers');
|
||||
|
||||
const APP_URL = process.env.APP_URL || process.env.CORS_ORIGIN || 'http://localhost:3001';
|
||||
const APP_NAME_EN = "Samaya's Digital Hub";
|
||||
const APP_NAME_AR = 'المركز الرقمي لسمايا';
|
||||
const APP_NAME_EN = 'Rawaj';
|
||||
const APP_NAME_AR = 'رواج';
|
||||
|
||||
// ─── TRANSLATIONS ───────────────────────────────────────────────
|
||||
|
||||
@@ -94,6 +94,21 @@ const t = {
|
||||
view: { en: 'View', ar: 'عرض' },
|
||||
viewTask: { en: 'View Task', ar: 'عرض المهمة' },
|
||||
viewIssue:{ en: 'View Issue', ar: 'عرض المشكلة' },
|
||||
|
||||
// Budget
|
||||
budgetRequest: { en: 'Budget Request', ar: 'طلب ميزانية' },
|
||||
budgetRequestHeading: { en: 'Budget Request', ar: 'طلب ميزانية' },
|
||||
budgetRequestBody: { en: (name, amount) => `<strong>${name}</strong> is requesting <strong>${amount}</strong>.`,
|
||||
ar: (name, amount) => `يطلب <strong>${name}</strong> مبلغ <strong>${amount}</strong>.` },
|
||||
budgetJustification: { en: 'Justification', ar: 'المبرر' },
|
||||
budgetEarmarkedFor: { en: 'Earmarked for', ar: 'مخصص لـ' },
|
||||
reviewRequest: { en: 'Review Request', ar: 'مراجعة الطلب' },
|
||||
budgetApproved: { en: 'Budget Request Approved', ar: 'تمت الموافقة على طلب الميزانية' },
|
||||
budgetApprovedBody: { en: (amount) => `Your budget request for <strong>${amount}</strong> has been approved. Funds are now available.`,
|
||||
ar: (amount) => `تمت الموافقة على طلب الميزانية بمبلغ <strong>${amount}</strong>. الأموال متاحة الآن.` },
|
||||
budgetRejected: { en: 'Budget Request Rejected', ar: 'تم رفض طلب الميزانية' },
|
||||
budgetRejectedBody: { en: (amount) => `Your budget request for <strong>${amount}</strong> has been rejected.`,
|
||||
ar: (amount) => `تم رفض طلب الميزانية بمبلغ <strong>${amount}</strong>.` },
|
||||
};
|
||||
|
||||
function tr(key, lang) { return t[key]?.[lang] || t[key]?.en || key; }
|
||||
@@ -111,7 +126,7 @@ function renderEmail({ heading, bodyHtml, ctaText, ctaUrl, lang = 'en' }) {
|
||||
<html dir="${dir}" lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||||
<body style="margin:0;padding:0;background:#f4f5f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
|
||||
<div style="max-width:600px;margin:0 auto;padding:20px;direction:${dir};text-align:${align}">
|
||||
<div style="background:#1e293b;color:#fff;padding:16px 24px;border-radius:12px 12px 0 0;font-size:16px;font-weight:600;text-align:${align}">
|
||||
<div style="background:#0a1f1c;color:#fff;padding:16px 24px;border-radius:12px 12px 0 0;font-size:16px;font-weight:600;text-align:${align}">
|
||||
${appName}
|
||||
</div>
|
||||
<div style="background:#fff;padding:32px 24px;border-radius:0 0 12px 12px;border:1px solid #e2e8f0;border-top:none">
|
||||
@@ -121,7 +136,7 @@ function renderEmail({ heading, bodyHtml, ctaText, ctaUrl, lang = 'en' }) {
|
||||
</div>
|
||||
${ctaText && ctaUrl ? `
|
||||
<div style="margin:24px 0 8px">
|
||||
<a href="${ctaUrl}" style="display:inline-block;background:#3b82f6;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600;font-size:14px">${ctaText}</a>
|
||||
<a href="${ctaUrl}" style="display:inline-block;background:#0d9488;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600;font-size:14px">${ctaText}</a>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<div style="text-align:center;padding:16px;color:#94a3b8;font-size:12px">
|
||||
@@ -151,8 +166,10 @@ async function getMultipleUsers(userIds) {
|
||||
}
|
||||
|
||||
function send({ to, subject, heading, bodyHtml, ctaText, ctaUrl, lang }) {
|
||||
const appName = lang === 'ar' ? APP_NAME_AR : APP_NAME_EN;
|
||||
const fullSubject = `${appName} — ${subject}`;
|
||||
const { html, text } = renderEmail({ heading, bodyHtml, ctaText, ctaUrl, lang });
|
||||
sendMail({ to, subject, html, text })
|
||||
sendMail({ to, subject: fullSubject, html, text })
|
||||
.then(() => console.log(`[notifications] Sent "${subject}" to ${to}`))
|
||||
.catch(err => console.error(`[notifications] FAILED "${subject}" to ${to}:`, err.message));
|
||||
}
|
||||
@@ -387,7 +404,52 @@ function notifyUserInvited({ email, name, password, inviterName, lang = 'en' })
|
||||
});
|
||||
}
|
||||
|
||||
// 11. Budget request → email CEO
|
||||
function notifyBudgetRequest({ ceoEmail, amount, requesterName, justification, earmarkedFor, approvalUrl }) {
|
||||
const earmarkHtml = earmarkedFor ? `<p><strong>${tr('budgetEarmarkedFor', 'en')}:</strong> ${earmarkedFor}</p>` : '';
|
||||
send({
|
||||
to: ceoEmail, lang: 'en',
|
||||
subject: `${tr('budgetRequest', 'en')}: ${amount}`,
|
||||
heading: tr('budgetRequestHeading', 'en'),
|
||||
bodyHtml: `
|
||||
<p>${tr('budgetRequestBody', 'en')(requesterName, amount)}</p>
|
||||
<p><strong>${tr('budgetJustification', 'en')}:</strong> ${justification}</p>
|
||||
${earmarkHtml}`,
|
||||
ctaText: tr('reviewRequest', 'en'),
|
||||
ctaUrl: approvalUrl,
|
||||
});
|
||||
}
|
||||
|
||||
// 12. Budget approved → notify requester
|
||||
function notifyBudgetApproved({ request, requesterEmail, requesterLang }) {
|
||||
const l = requesterLang || 'en';
|
||||
send({
|
||||
to: requesterEmail, lang: l,
|
||||
subject: `${tr('budgetApproved', l)}: ${request.amount}`,
|
||||
heading: tr('budgetApproved', l),
|
||||
bodyHtml: `
|
||||
<p>${tr('budgetApprovedBody', l)(String(request.amount))}</p>
|
||||
${request.response_note ? `<blockquote ${BLOCKQUOTE}>${request.response_note}</blockquote>` : ''}`,
|
||||
ctaText: null, ctaUrl: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 13. Budget rejected → notify requester
|
||||
function notifyBudgetRejected({ request, requesterEmail, requesterLang }) {
|
||||
const l = requesterLang || 'en';
|
||||
send({
|
||||
to: requesterEmail, lang: l,
|
||||
subject: `${tr('budgetRejected', l)}: ${request.amount}`,
|
||||
heading: tr('budgetRejected', l),
|
||||
bodyHtml: `
|
||||
<p>${tr('budgetRejectedBody', l)(String(request.amount))}</p>
|
||||
${request.response_note ? `<blockquote ${BLOCKQUOTE}>${request.response_note}</blockquote>` : ''}`,
|
||||
ctaText: null, ctaUrl: null,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renderEmail,
|
||||
notifyReviewSubmitted,
|
||||
notifyApproved,
|
||||
notifyRejected,
|
||||
@@ -398,4 +460,7 @@ module.exports = {
|
||||
notifyIssueStatusUpdate,
|
||||
notifyCampaignCreated,
|
||||
notifyUserInvited,
|
||||
notifyBudgetRequest,
|
||||
notifyBudgetApproved,
|
||||
notifyBudgetRejected,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user