Marketing Hub: RBAC, i18n (AR/EN), tasks overhaul, team/user merge, tutorial

Features:
- Full RBAC with 3 roles (superadmin/manager/contributor)
- Ownership tracking on posts, tasks, campaigns, projects
- Task system: assign to anyone, filter combobox, visibility scoping
- Team members merged into users table (single source of truth)
- Post thumbnails on kanban cards from attachments
- Publication link validation before publishing
- Interactive onboarding tutorial with Settings restart
- Full Arabic/English i18n with RTL layout support
- Language toggle in sidebar, IBM Plex Sans Arabic font
- Brand-based visibility filtering for non-superadmins
- Manager can only create contributors
- Profile completion flow for new users
- Cookie-based sessions (express-session + SQLite)
This commit is contained in:
fahed
2026-02-08 20:46:58 +03:00
commit 35d84b6bff
2240 changed files with 846749 additions and 0 deletions

125
client/src/utils/api.js Normal file
View File

@@ -0,0 +1,125 @@
const API = '/api';
// Map SQLite fields to frontend-friendly format
const toCamel = (s) => s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
const normalize = (data) => {
if (Array.isArray(data)) return data.map(normalize);
if (data && typeof data === 'object' && !Array.isArray(data)) {
const out = {};
for (const [k, v] of Object.entries(data)) {
const camelKey = toCamel(k);
out[camelKey] = v;
if (camelKey !== k) out[k] = v;
}
// Add _id alias
if (out.id !== undefined && out._id === undefined) out._id = out.id;
// Map brand_name → brand (frontend expects post.brand as string)
if (out.brandName && !out.brand) out.brand = out.brandName;
// Map assigned_name for display
if (out.assignedName && !out.assignedToName) out.assignedToName = out.assignedName;
return out;
}
return data;
};
const handleResponse = async (r, label) => {
if (!r.ok) {
if (r.status === 401 || r.status === 403) {
// Unauthorized - redirect to login if not already there
if (!window.location.pathname.includes('/login')) {
window.location.href = '/login';
}
}
throw new Error(`${label} failed: ${r.status}`);
}
const json = await r.json();
return normalize(json);
};
export const api = {
get: (path) => fetch(`${API}${path}`, {
credentials: 'include'
}).then(r => handleResponse(r, `GET ${path}`)),
post: (path, data) => fetch(`${API}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
}).then(r => handleResponse(r, `POST ${path}`)),
patch: (path, data) => fetch(`${API}${path}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
}).then(r => handleResponse(r, `PATCH ${path}`)),
delete: (path) => fetch(`${API}${path}`, {
method: 'DELETE',
credentials: 'include',
}).then(r => handleResponse(r, `DELETE ${path}`)),
upload: (path, formData) => fetch(`${API}${path}`, {
method: 'POST',
credentials: 'include',
body: formData,
}).then(r => handleResponse(r, `UPLOAD ${path}`)),
};
// Brand colors map — matches Samaya brands from backend
export const BRAND_COLORS = {
'Samaya Investment': { bg: 'bg-indigo-100', text: 'text-indigo-700', dot: 'bg-indigo-500' },
'Hira Cultural District': { bg: 'bg-amber-100', text: 'text-amber-700', dot: 'bg-amber-500' },
'Holy Quran Museum': { bg: 'bg-emerald-100', text: 'text-emerald-700', dot: 'bg-emerald-500' },
'Al-Safiya Museum': { bg: 'bg-purple-100', text: 'text-purple-700', dot: 'bg-purple-500' },
'Hayhala': { bg: 'bg-red-100', text: 'text-red-700', dot: 'bg-red-500' },
'Jabal Thawr': { bg: 'bg-stone-100', text: 'text-stone-700', dot: 'bg-stone-500' },
'Coffee Chain': { bg: 'bg-orange-100', text: 'text-orange-700', dot: 'bg-orange-500' },
'Taibah Gifts': { bg: 'bg-pink-100', text: 'text-pink-700', dot: 'bg-pink-500' },
'Google Maps': { bg: 'bg-blue-100', text: 'text-blue-700', dot: 'bg-blue-500' },
'default': { bg: 'bg-gray-100', text: 'text-gray-700', dot: 'bg-gray-500' },
};
export const getBrandColor = (brand) => BRAND_COLORS[brand] || BRAND_COLORS['default'];
// Platform icons helper — svg paths for inline icons
export const PLATFORMS = {
instagram: { label: 'Instagram', color: '#E4405F', icon: 'M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z' },
twitter: { label: 'X', color: '#000000', icon: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' },
facebook: { label: 'Facebook', color: '#1877F2', icon: 'M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z' },
linkedin: { label: 'LinkedIn', color: '#0A66C2', icon: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z' },
tiktok: { label: 'TikTok', color: '#000000', icon: 'M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z' },
youtube: { label: 'YouTube', color: '#FF0000', icon: 'M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z' },
snapchat: { label: 'Snapchat', color: '#FFFC00', icon: 'M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 01.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.631-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12.017 24c6.624 0 11.99-5.367 11.99-11.988C24.007 5.367 18.641 0 12.017 0z' },
google_ads: { label: 'Google Ads', color: '#4285F4', icon: 'M12 0C5.372 0 0 5.373 0 12s5.372 12 12 12 12-5.373 12-12S18.628 0 12 0zm5.82 16.32l-2.16 1.25c-.37.21-.84.09-1.05-.28l-5.82-10.08c-.21-.37-.09-.84.28-1.05l2.16-1.25c.37-.21.84-.09 1.05.28l5.82 10.08c.21.37.09.84-.28 1.05z' },
};
// Status config
export const STATUS_CONFIG = {
draft: { label: 'Draft', bg: 'bg-gray-100', text: 'text-gray-600', dot: 'bg-gray-400' },
in_review: { label: 'In Review', bg: 'bg-amber-50', text: 'text-amber-700', dot: 'bg-amber-400' },
approved: { label: 'Approved', bg: 'bg-blue-50', text: 'text-blue-700', dot: 'bg-blue-400' },
scheduled: { label: 'Scheduled', bg: 'bg-purple-50', text: 'text-purple-700', dot: 'bg-purple-400' },
published: { label: 'Published', bg: 'bg-emerald-50', text: 'text-emerald-700', dot: 'bg-emerald-400' },
rejected: { label: 'Rejected', bg: 'bg-red-50', text: 'text-red-700', dot: 'bg-red-400' },
todo: { label: 'To Do', bg: 'bg-gray-100', text: 'text-gray-600', dot: 'bg-gray-400' },
in_progress: { label: 'In Progress', bg: 'bg-blue-50', text: 'text-blue-700', dot: 'bg-blue-400' },
done: { label: 'Done', bg: 'bg-emerald-50', text: 'text-emerald-700', dot: 'bg-emerald-400' },
active: { label: 'Active', bg: 'bg-emerald-50', text: 'text-emerald-700', dot: 'bg-emerald-400' },
paused: { label: 'Paused', bg: 'bg-amber-50', text: 'text-amber-700', dot: 'bg-amber-400' },
completed: { label: 'Completed', bg: 'bg-blue-50', text: 'text-blue-700', dot: 'bg-blue-400' },
cancelled: { label: 'Cancelled', bg: 'bg-red-50', text: 'text-red-700', dot: 'bg-red-400' },
planning: { label: 'Planning', bg: 'bg-gray-100', text: 'text-gray-600', dot: 'bg-gray-400' },
};
export const getStatusConfig = (status) => STATUS_CONFIG[status] || STATUS_CONFIG['draft'];
// Priority config
export const PRIORITY_CONFIG = {
low: { label: 'Low', color: 'bg-gray-400' },
medium: { label: 'Medium', color: 'bg-amber-400' },
high: { label: 'High', color: 'bg-orange-500' },
urgent: { label: 'Urgent', color: 'bg-red-500' },
};