diff --git a/backend/src/db/seeders/20260404090000-role-demo-users.js b/backend/src/db/seeders/20260404090000-role-demo-users.js
new file mode 100644
index 0000000..49f8e72
--- /dev/null
+++ b/backend/src/db/seeders/20260404090000-role-demo-users.js
@@ -0,0 +1,162 @@
+'use strict';
+
+const bcrypt = require('bcrypt');
+const { QueryTypes } = require('sequelize');
+const { v4: uuidv4 } = require('uuid');
+const config = require('../../config');
+
+const demoAccounts = [
+ {
+ email: 'super_admin@flatlogic.com',
+ password: config.admin_pass,
+ roleName: 'Super Administrator',
+ firstName: 'Super',
+ lastName: 'Administrator',
+ },
+ {
+ email: 'admin@flatlogic.com',
+ password: config.admin_pass,
+ roleName: 'Administrator',
+ firstName: 'Organization',
+ lastName: 'Administrator',
+ },
+ {
+ email: 'director.general@flatlogic.com',
+ previousEmails: ['client@hello.com'],
+ password: config.user_pass,
+ roleName: 'Director General',
+ firstName: 'Director',
+ lastName: 'General',
+ },
+ {
+ email: 'finance.director@flatlogic.com',
+ previousEmails: ['john@doe.com'],
+ password: config.user_pass,
+ roleName: 'Finance Director',
+ firstName: 'Finance',
+ lastName: 'Director',
+ },
+ {
+ email: 'procurement.lead@flatlogic.com',
+ password: config.user_pass,
+ roleName: 'Procurement Lead',
+ firstName: 'Procurement',
+ lastName: 'Lead',
+ },
+ {
+ email: 'compliance.audit@flatlogic.com',
+ password: config.user_pass,
+ roleName: 'Compliance and Audit Lead',
+ firstName: 'Compliance',
+ lastName: 'Audit Lead',
+ },
+ {
+ email: 'project.delivery@flatlogic.com',
+ password: config.user_pass,
+ roleName: 'Project Delivery Lead',
+ firstName: 'Project',
+ lastName: 'Delivery Lead',
+ },
+];
+
+module.exports = {
+ async up(queryInterface) {
+ const transaction = await queryInterface.sequelize.transaction();
+
+ try {
+ for (const account of demoAccounts) {
+ const role = await queryInterface.sequelize.query(
+ 'SELECT id FROM "roles" WHERE name = :roleName LIMIT 1',
+ {
+ replacements: { roleName: account.roleName },
+ type: QueryTypes.SELECT,
+ transaction,
+ },
+ );
+
+ if (!role[0]?.id) {
+ throw new Error(`Role not found for demo account: ${account.roleName}`);
+ }
+
+ let existingUser = null;
+
+ for (const candidateEmail of [account.email, ...(account.previousEmails || [])]) {
+ const user = await queryInterface.sequelize.query(
+ 'SELECT id, email FROM "users" WHERE email = :email LIMIT 1',
+ {
+ replacements: { email: candidateEmail },
+ type: QueryTypes.SELECT,
+ transaction,
+ },
+ );
+
+ if (user[0]?.id) {
+ existingUser = user[0];
+ break;
+ }
+ }
+
+ const passwordHash = bcrypt.hashSync(account.password, config.bcrypt.saltRounds);
+ const now = new Date();
+
+ if (existingUser?.id) {
+ await queryInterface.sequelize.query(
+ `UPDATE "users"
+ SET "firstName" = :firstName,
+ "lastName" = :lastName,
+ "email" = :email,
+ "password" = :password,
+ "emailVerified" = true,
+ "provider" = :provider,
+ "disabled" = false,
+ "app_roleId" = :roleId,
+ "deletedAt" = NULL,
+ "updatedAt" = :updatedAt
+ WHERE id = :id`,
+ {
+ replacements: {
+ id: existingUser.id,
+ firstName: account.firstName,
+ lastName: account.lastName,
+ email: account.email,
+ password: passwordHash,
+ provider: config.providers.LOCAL,
+ roleId: role[0].id,
+ updatedAt: now,
+ },
+ transaction,
+ },
+ );
+ } else {
+ await queryInterface.bulkInsert(
+ 'users',
+ [
+ {
+ id: uuidv4(),
+ firstName: account.firstName,
+ lastName: account.lastName,
+ email: account.email,
+ emailVerified: true,
+ provider: config.providers.LOCAL,
+ password: passwordHash,
+ disabled: false,
+ app_roleId: role[0].id,
+ createdAt: now,
+ updatedAt: now,
+ },
+ ],
+ { transaction },
+ );
+ }
+ }
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ console.error('Error upserting role demo users:', error);
+ throw error;
+ }
+ },
+
+ async down() {},
+};
diff --git a/backend/src/db/seeders/20260404110000-workspace-read-permissions.js b/backend/src/db/seeders/20260404110000-workspace-read-permissions.js
new file mode 100644
index 0000000..ec3fd28
--- /dev/null
+++ b/backend/src/db/seeders/20260404110000-workspace-read-permissions.js
@@ -0,0 +1,114 @@
+'use strict';
+
+const { QueryTypes } = require('sequelize');
+
+const rolePermissionMap = {
+ 'Director General': [
+ 'READ_APPROVALS',
+ 'READ_PROJECTS',
+ 'READ_CONTRACTS',
+ 'READ_NOTIFICATIONS',
+ ],
+ 'Finance Director': [
+ 'READ_PAYMENT_REQUESTS',
+ 'READ_ALLOCATIONS',
+ 'READ_CONTRACTS',
+ 'READ_APPROVALS',
+ 'READ_NOTIFICATIONS',
+ ],
+ 'Procurement Lead': [
+ 'READ_REQUISITIONS',
+ 'READ_TENDERS',
+ 'READ_VENDORS',
+ 'READ_VENDOR_COMPLIANCE_DOCUMENTS',
+ 'READ_BIDS',
+ 'READ_BID_EVALUATIONS',
+ 'READ_AWARDS',
+ 'READ_CONTRACTS',
+ 'READ_APPROVALS',
+ 'READ_NOTIFICATIONS',
+ ],
+ 'Compliance and Audit Lead': [
+ 'READ_COMPLIANCE_ALERTS',
+ 'READ_AUDIT_LOGS',
+ 'READ_DOCUMENTS',
+ 'READ_APPROVALS',
+ 'READ_CONTRACTS',
+ 'READ_NOTIFICATIONS',
+ ],
+ 'Project Delivery Lead': [
+ 'READ_PROJECTS',
+ 'READ_PROJECT_MILESTONES',
+ 'READ_RISKS',
+ 'READ_ISSUES',
+ 'READ_FIELD_VERIFICATIONS',
+ 'READ_PROGRAMS',
+ 'READ_PROVINCES',
+ 'READ_CONTRACTS',
+ 'READ_APPROVALS',
+ 'READ_PAYMENT_REQUESTS',
+ 'READ_NOTIFICATIONS',
+ ],
+};
+
+module.exports = {
+ async up(queryInterface) {
+ const transaction = await queryInterface.sequelize.transaction();
+ const now = new Date();
+
+ try {
+ for (const [roleName, permissionNames] of Object.entries(rolePermissionMap)) {
+ const roles = await queryInterface.sequelize.query(
+ 'SELECT id FROM "roles" WHERE name = :roleName LIMIT 1',
+ {
+ replacements: { roleName },
+ type: QueryTypes.SELECT,
+ transaction,
+ },
+ );
+
+ if (!roles[0]?.id) {
+ throw new Error(`Role not found while assigning workspace permissions: ${roleName}`);
+ }
+
+ for (const permissionName of permissionNames) {
+ const permissions = await queryInterface.sequelize.query(
+ 'SELECT id FROM "permissions" WHERE name = :permissionName LIMIT 1',
+ {
+ replacements: { permissionName },
+ type: QueryTypes.SELECT,
+ transaction,
+ },
+ );
+
+ if (!permissions[0]?.id) {
+ throw new Error(`Permission not found while assigning workspace permissions: ${permissionName}`);
+ }
+
+ await queryInterface.sequelize.query(
+ `INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
+ VALUES (:createdAt, :updatedAt, :roleId, :permissionId)
+ ON CONFLICT ("roles_permissionsId", "permissionId") DO NOTHING`,
+ {
+ replacements: {
+ createdAt: now,
+ updatedAt: now,
+ roleId: roles[0].id,
+ permissionId: permissions[0].id,
+ },
+ transaction,
+ },
+ );
+ }
+ }
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ console.error('Error assigning workspace read permissions:', error);
+ throw error;
+ }
+ },
+
+ async down() {},
+};
diff --git a/backend/src/db/seeders/20260404123000-cleanup-sample-business-data.js b/backend/src/db/seeders/20260404123000-cleanup-sample-business-data.js
new file mode 100644
index 0000000..c4d0e20
--- /dev/null
+++ b/backend/src/db/seeders/20260404123000-cleanup-sample-business-data.js
@@ -0,0 +1,373 @@
+'use strict';
+
+const nameMap = {
+ 'Grace Hopper': 'Kinshasa Capital Delivery Office',
+ 'Alan Turing': 'Eastern Corridor Operations Unit',
+ 'Ada Lovelace': 'National Public Investment Office',
+ 'Marie Curie': 'Provincial Services Coordination Office',
+};
+
+const provinceNameMap = {
+ 'Grace Hopper': 'Kinshasa',
+ 'Alan Turing': 'North Kivu',
+ 'Ada Lovelace': 'Haut-Katanga',
+ 'Marie Curie': 'Kasaï Central',
+};
+
+const provinceCodeMap = {
+ 'Grace Hopper': 'KIN',
+ 'Alan Turing': 'NKV',
+ 'Ada Lovelace': 'HKT',
+ 'Marie Curie': 'KSC',
+};
+
+const departmentNameMap = {
+ 'Grace Hopper': 'Procurement Directorate',
+ 'Alan Turing': 'Finance Directorate',
+ 'Ada Lovelace': 'Project Delivery Unit',
+ 'Marie Curie': 'Internal Audit Office',
+};
+
+const departmentCodeMap = {
+ 'Grace Hopper': 'PD',
+ 'Alan Turing': 'FD',
+ 'Ada Lovelace': 'PDU',
+ 'Marie Curie': 'IAO',
+};
+
+const programmeNameMap = {
+ 'Grace Hopper': 'Road Connectivity Programme',
+ 'Alan Turing': 'Water Access Programme',
+ 'Ada Lovelace': 'School Rehabilitation Programme',
+ 'Marie Curie': 'Primary Health Strengthening Programme',
+};
+
+const programmeCodeMap = {
+ 'Grace Hopper': 'RCP',
+ 'Alan Turing': 'WAP',
+ 'Ada Lovelace': 'SRP',
+ 'Marie Curie': 'PHSP',
+};
+
+const projectNameMap = {
+ 'Grace Hopper': 'Bukavu Water Network Upgrade',
+ 'Alan Turing': 'Goma School Rehabilitation Phase 1',
+ 'Ada Lovelace': 'Kananga Rural Roads Package A',
+ 'Marie Curie': 'Lubumbashi Health Facilities Modernization',
+};
+
+const projectCodeMap = {
+ 'Grace Hopper': 'BWU',
+ 'Alan Turing': 'GSR',
+ 'Ada Lovelace': 'KRR',
+ 'Marie Curie': 'LHF',
+};
+
+const vendorNameMap = {
+ 'Grace Hopper': 'Congo Build Partners',
+ 'Alan Turing': 'Great Lakes Engineering',
+ 'Ada Lovelace': 'Equator Supply Services',
+ 'Marie Curie': 'Civic Works Consortium',
+};
+
+const contractTitleMap = {
+ 'Grace Hopper': 'Road works package',
+ 'Alan Turing': 'Medical equipment supply',
+ 'Ada Lovelace': 'School rehabilitation lot',
+ 'Marie Curie': 'Water network expansion',
+};
+
+const workflowNameMap = {
+ 'Grace Hopper': 'Capital Project Approval',
+ 'Alan Turing': 'Vendor Compliance Review',
+ 'Ada Lovelace': 'Budget Reallocation Approval',
+ 'Marie Curie': 'Payment Authorization Workflow',
+};
+
+const stepNameMap = {
+ 'Grace Hopper': 'Initial Review',
+ 'Alan Turing': 'Department Approval',
+ 'Ada Lovelace': 'Finance Clearance',
+ 'Marie Curie': 'Executive Sign-off',
+};
+
+const recordTypeMap = {
+ 'Grace Hopper': 'projects',
+ 'Alan Turing': 'contracts',
+ 'Ada Lovelace': 'payment_requests',
+ 'Marie Curie': 'requisitions',
+};
+
+const referencePrefixMap = {
+ 'Grace Hopper': 'REF-OPS',
+ 'Alan Turing': 'REF-CTL',
+ 'Ada Lovelace': 'REF-DEL',
+ 'Marie Curie': 'REF-CMP',
+};
+
+const notificationTitleMap = {
+ 'Grace Hopper': 'Contract action required',
+ 'Alan Turing': 'Project update available',
+ 'Ada Lovelace': 'Approval decision pending',
+ 'Marie Curie': 'Compliance evidence due',
+};
+
+const narrativeMap = {
+ 'Grace Hopper': 'Priority follow-up is required on this record.',
+ 'Alan Turing': 'Operational review is in progress for this item.',
+ 'Ada Lovelace': 'Programme delivery details are under active review.',
+ 'Marie Curie': 'Supporting evidence and control checks are pending.',
+};
+
+const alertTitleMap = {
+ 'Grace Hopper': 'Contract monitoring alert',
+ 'Alan Turing': 'Project delivery alert',
+ 'Ada Lovelace': 'Payment control alert',
+ 'Marie Curie': 'Compliance evidence alert',
+};
+
+const escapeValue = (queryInterface, value) => queryInterface.sequelize.escape(value);
+
+const updateExactValues = async (queryInterface, transaction, table, column, valueMap) => {
+ const entries = Object.entries(valueMap);
+
+ if (!entries.length) {
+ return;
+ }
+
+ const whenClauses = entries
+ .map(([from, to]) => `WHEN ${escapeValue(queryInterface, from)} THEN ${escapeValue(queryInterface, to)}`)
+ .join(' ');
+
+ const fromList = entries.map(([from]) => escapeValue(queryInterface, from)).join(', ');
+
+ await queryInterface.sequelize.query(
+ `UPDATE "${table}"
+ SET "${column}" = CASE "${column}" ${whenClauses} ELSE "${column}" END
+ WHERE "${column}" IN (${fromList})`,
+ { transaction },
+ );
+};
+
+const updateWithIdSuffix = async (queryInterface, transaction, table, column, valueMap, separator = ' ') => {
+ const entries = Object.entries(valueMap);
+
+ if (!entries.length) {
+ return;
+ }
+
+ const whenClauses = entries
+ .map(([from, to]) => {
+ const base = escapeValue(queryInterface, to);
+ const sep = escapeValue(queryInterface, separator);
+ return `WHEN ${escapeValue(queryInterface, from)} THEN CONCAT(${base}, ${sep}, RIGHT(REPLACE(id::text, '-', ''), 4))`;
+ })
+ .join(' ');
+
+ const fromList = entries.map(([from]) => escapeValue(queryInterface, from)).join(', ');
+
+ await queryInterface.sequelize.query(
+ `UPDATE "${table}"
+ SET "${column}" = CASE "${column}" ${whenClauses} ELSE "${column}" END
+ WHERE "${column}" IN (${fromList})`,
+ { transaction },
+ );
+};
+
+module.exports = {
+ async up(queryInterface) {
+ const transaction = await queryInterface.sequelize.transaction();
+
+ try {
+ await updateWithIdSuffix(queryInterface, transaction, 'organizations', 'name', nameMap);
+ await updateExactValues(queryInterface, transaction, 'provinces', 'name', provinceNameMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'provinces', 'code', provinceCodeMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'departments', 'name', departmentNameMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'departments', 'code', departmentCodeMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'approval_workflows', 'name', workflowNameMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'approval_steps', 'name', stepNameMap);
+ await updateExactValues(queryInterface, transaction, 'approvals', 'record_type', recordTypeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'notifications', 'title', notificationTitleMap);
+ await updateExactValues(queryInterface, transaction, 'notifications', 'message', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'notifications', 'record_type', recordTypeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'fiscal_years', 'name', {
+ 'Grace Hopper': 'FY 2024/25',
+ 'Alan Turing': 'FY 2025/26',
+ 'Ada Lovelace': 'FY 2026/27',
+ 'Marie Curie': 'FY 2027/28',
+ });
+ await updateWithIdSuffix(queryInterface, transaction, 'funding_sources', 'name', {
+ 'Grace Hopper': 'Treasury Capital Grant',
+ 'Alan Turing': 'Provincial Development Transfer',
+ 'Ada Lovelace': 'Infrastructure Recovery Fund',
+ 'Marie Curie': 'Service Delivery Support Fund',
+ });
+ await updateWithIdSuffix(queryInterface, transaction, 'funding_sources', 'reference_code', referencePrefixMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'budget_programs', 'name', programmeNameMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'budget_programs', 'code', programmeCodeMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'budget_lines', 'name', {
+ 'Grace Hopper': 'Civil Works Envelope',
+ 'Alan Turing': 'Equipment and Materials',
+ 'Ada Lovelace': 'Professional Services',
+ 'Marie Curie': 'Monitoring and Evaluation',
+ });
+ await updateWithIdSuffix(queryInterface, transaction, 'budget_lines', 'code', {
+ 'Grace Hopper': 'BL-CW',
+ 'Alan Turing': 'BL-EQ',
+ 'Ada Lovelace': 'BL-PS',
+ 'Marie Curie': 'BL-ME',
+ }, '-');
+ await updateExactValues(queryInterface, transaction, 'budget_lines', 'description', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'procurement_plans', 'name', {
+ 'Grace Hopper': 'Annual Infrastructure Sourcing Plan',
+ 'Alan Turing': 'Education Recovery Procurement Plan',
+ 'Ada Lovelace': 'Provincial Delivery Procurement Plan',
+ 'Marie Curie': 'Health Services Procurement Plan',
+ });
+ await updateWithIdSuffix(queryInterface, transaction, 'requisitions', 'requisition_number', referencePrefixMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'requisitions', 'title', {
+ 'Grace Hopper': 'Road maintenance works request',
+ 'Alan Turing': 'Laboratory equipment request',
+ 'Ada Lovelace': 'School repair materials request',
+ 'Marie Curie': 'Water network extension request',
+ });
+ await updateExactValues(queryInterface, transaction, 'requisitions', 'scope_of_work', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'tenders', 'tender_number', referencePrefixMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'tenders', 'title', {
+ 'Grace Hopper': 'Open tender for civil works',
+ 'Alan Turing': 'Restricted tender for equipment supply',
+ 'Ada Lovelace': 'Framework tender for school rehabilitation',
+ 'Marie Curie': 'Competitive tender for water services expansion',
+ });
+ await updateExactValues(queryInterface, transaction, 'tenders', 'eligibility_criteria', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'tenders', 'submission_instructions', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'vendors', 'name', vendorNameMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'vendors', 'trade_name', vendorNameMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'vendors', 'contact_name', {
+ 'Grace Hopper': 'Patrick Ilunga',
+ 'Alan Turing': 'Aline Mbuyi',
+ 'Ada Lovelace': 'David Kasongo',
+ 'Marie Curie': 'Ruth Mukendi',
+ });
+ await updateExactValues(queryInterface, transaction, 'vendors', 'address', {
+ 'Grace Hopper': 'Kinshasa headquarters office',
+ 'Alan Turing': 'Goma regional office',
+ 'Ada Lovelace': 'Kananga delivery office',
+ 'Marie Curie': 'Lubumbashi programme office',
+ });
+ await updateWithIdSuffix(queryInterface, transaction, 'vendor_compliance_documents', 'reference_number', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'vendor_compliance_documents', 'review_comment', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'bids', 'bid_reference', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'bids', 'notes', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'bid_evaluations', 'justification', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'awards', 'award_number', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'awards', 'award_memo', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'programs', 'name', programmeNameMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'programs', 'code', programmeCodeMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'projects', 'name', projectNameMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'projects', 'project_code', projectCodeMap, '-');
+ await updateExactValues(queryInterface, transaction, 'projects', 'implementing_entity', {
+ 'Grace Hopper': 'Directorate of Public Works',
+ 'Alan Turing': 'Directorate of Basic Education',
+ 'Ada Lovelace': 'Provincial Infrastructure Unit',
+ 'Marie Curie': 'Directorate of Health Services',
+ });
+ await updateExactValues(queryInterface, transaction, 'projects', 'objectives', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'project_milestones', 'name', {
+ 'Grace Hopper': 'Mobilization complete',
+ 'Alan Turing': 'Site preparation complete',
+ 'Ada Lovelace': 'Materials delivered',
+ 'Marie Curie': 'Final inspection complete',
+ });
+ await updateExactValues(queryInterface, transaction, 'project_milestones', 'description', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'risks', 'title', {
+ 'Grace Hopper': 'Contract schedule slippage risk',
+ 'Alan Turing': 'Funding delay risk',
+ 'Ada Lovelace': 'Approval bottleneck risk',
+ 'Marie Curie': 'Evidence gap risk',
+ });
+ await updateExactValues(queryInterface, transaction, 'risks', 'description', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'risks', 'mitigation_plan', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'issues', 'title', {
+ 'Grace Hopper': 'Procurement handoff issue',
+ 'Alan Turing': 'Payment processing issue',
+ 'Ada Lovelace': 'Delivery coordination issue',
+ 'Marie Curie': 'Compliance follow-up issue',
+ });
+ await updateExactValues(queryInterface, transaction, 'issues', 'description', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'field_verifications', 'findings', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'field_verifications', 'actions_required', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'contracts', 'title', contractTitleMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'contracts', 'contract_number', {
+ 'Grace Hopper': 'CT-RD',
+ 'Alan Turing': 'CT-ME',
+ 'Ada Lovelace': 'CT-SR',
+ 'Marie Curie': 'CT-WN',
+ }, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'grants', 'funding_window_name', {
+ 'Grace Hopper': 'Community Infrastructure Grant Window',
+ 'Alan Turing': 'Education Access Grant Window',
+ 'Ada Lovelace': 'Provincial Services Grant Window',
+ 'Marie Curie': 'Health Systems Grant Window',
+ });
+ await updateWithIdSuffix(queryInterface, transaction, 'grants', 'call_reference', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'grants', 'eligibility_rules', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'grants', 'notes', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'beneficiaries', 'name', {
+ 'Grace Hopper': 'Bukavu Community Water Board',
+ 'Alan Turing': 'Goma Education Improvement Council',
+ 'Ada Lovelace': 'Kananga Roads Maintenance Group',
+ 'Marie Curie': 'Lubumbashi Health Access Network',
+ });
+ await updateWithIdSuffix(queryInterface, transaction, 'beneficiaries', 'registration_number', referencePrefixMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'grant_applications', 'application_reference', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'grant_applications', 'proposal_summary', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'grant_evaluations', 'comments', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'grant_tranches', 'conditions', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'expense_categories', 'name', {
+ 'Grace Hopper': 'Civil works expense',
+ 'Alan Turing': 'Equipment purchase expense',
+ 'Ada Lovelace': 'Consultancy services expense',
+ 'Marie Curie': 'Monitoring activity expense',
+ });
+ await updateWithIdSuffix(queryInterface, transaction, 'expense_categories', 'code', {
+ 'Grace Hopper': 'EXP-CW',
+ 'Alan Turing': 'EXP-EQ',
+ 'Ada Lovelace': 'EXP-CS',
+ 'Marie Curie': 'EXP-ME',
+ }, '-');
+ await updateExactValues(queryInterface, transaction, 'expense_categories', 'description', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'invoices', 'invoice_number', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'invoices', 'notes', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'payment_requests', 'request_number', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'payment_requests', 'justification', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'payment_batches', 'batch_number', referencePrefixMap, '-');
+ await updateWithIdSuffix(queryInterface, transaction, 'payments', 'payment_reference', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'payments', 'failure_reason', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'obligations', 'obligation_number', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'obligations', 'notes', narrativeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'ledger_entries', 'entry_reference', referencePrefixMap, '-');
+ await updateExactValues(queryInterface, transaction, 'ledger_entries', 'description', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'ledger_entries', 'record_type', recordTypeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'documents', 'title', {
+ 'Grace Hopper': 'Signed contract dossier',
+ 'Alan Turing': 'Project monitoring report',
+ 'Ada Lovelace': 'Approval support memo',
+ 'Marie Curie': 'Compliance evidence file',
+ });
+ await updateExactValues(queryInterface, transaction, 'documents', 'description', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'documents', 'record_type', recordTypeMap);
+ await updateWithIdSuffix(queryInterface, transaction, 'compliance_alerts', 'title', alertTitleMap);
+ await updateExactValues(queryInterface, transaction, 'compliance_alerts', 'details', narrativeMap);
+ await updateExactValues(queryInterface, transaction, 'compliance_alerts', 'record_type', recordTypeMap);
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ console.error('Error cleaning sample business data:', error);
+ throw error;
+ }
+ },
+
+ async down() {},
+};
diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
index 31d62cb..5a60405 100644
--- a/backend/src/routes/auth.js
+++ b/backend/src/routes/auth.js
@@ -22,11 +22,11 @@ const router = express.Router();
* email:
* type: string
* default: admin@flatlogic.com
- * description: User email
+ * description: User email. Additional role demo accounts are listed on the login page.
* password:
* type: string
- * default: password
- * description: User password
+ * default: 5e8f2960
+ * description: User password. Additional role demo passwords are listed on the login page.
*/
/**
diff --git a/backend/src/routes/executive_summary.js b/backend/src/routes/executive_summary.js
index 6782f14..b661ba6 100644
--- a/backend/src/routes/executive_summary.js
+++ b/backend/src/routes/executive_summary.js
@@ -53,6 +53,21 @@ const WORKSPACE_ROLES = {
projectDeliveryLead: 'Project Delivery Lead',
};
+const SUPPORTED_RECORD_TYPES = new Set([
+ 'requisitions',
+ 'contracts',
+ 'projects',
+ 'vendors',
+ 'tenders',
+ 'awards',
+ 'invoices',
+ 'payment_requests',
+ 'budget_reallocations',
+ 'grants',
+ 'compliance_alerts',
+]);
+
+
const formatCurrencyValue = (value, currency) =>
new Intl.NumberFormat(currency === 'CDF' ? 'fr-CD' : 'en-US', {
style: 'currency',
@@ -86,60 +101,38 @@ const getCollectionCount = (datasets, collectionKey) => {
const workspacePayloadConfigs = {
[WORKSPACE_ROLES.superAdmin]: {
- summaryMetricKeys: ['pendingApprovals', 'openRiskAlerts', 'unreadNotifications', 'vendorComplianceAlerts', 'activeProjects', 'contractsNearingExpiry'],
+ summaryMetricKeys: ['organizationCount', 'platformUserCount', 'roleCount', 'permissionCount', 'auditEventCount', 'unreadNotifications'],
focusCards: [
{
- key: 'super-admin-approvals',
- title: 'Governance approvals waiting attention',
- metricKey: 'pendingApprovals',
- note: 'Pending approvals visible to the Super Admin that may need reassignment, policy review, or escalation.',
- href: '/approvals/approvals-list',
+ key: 'super-admin-organizations',
+ title: 'Organizations on the platform',
+ metricKey: 'organizationCount',
+ note: 'Tenant footprint currently governed from the platform layer.',
+ href: '/organizations/organizations-list',
},
{
- key: 'super-admin-risks',
- title: 'Cross-organization control alerts',
- metricKey: 'openRiskAlerts',
- note: 'Open high-priority alerts that may indicate access, compliance, or platform-governance problems.',
- href: '/compliance_alerts/compliance_alerts-list',
+ key: 'super-admin-users',
+ title: 'Platform users under access governance',
+ metricKey: 'platformUserCount',
+ note: 'Accounts that may require provisioning, review, or top-level access support.',
+ href: '/users/users-list',
},
{
- key: 'super-admin-notifications',
- title: 'Unread platform notices',
- metricKey: 'unreadNotifications',
- note: 'Recent notices that can reveal governance, setup, or platform support issues.',
- href: '/notifications/notifications-list',
+ key: 'super-admin-roles',
+ title: 'Roles and access structure in use',
+ metricKey: 'roleCount',
+ note: 'Role definitions shaping segregation of duties and approval boundaries.',
+ href: '/roles/roles-list',
},
{
- key: 'super-admin-vendor-alerts',
- title: 'Vendor compliance exposure',
- metricKey: 'vendorComplianceAlerts',
- note: 'Supplier evidence and expiry issues that may require top-level visibility or policy follow-up.',
- href: '/vendor_compliance_documents/vendor_compliance_documents-list',
- },
- ],
- watchlistCards: [
- {
- key: 'super-admin-approval-watch',
- title: 'Governance approval queue',
- collectionKey: 'approvalQueue',
- note: 'A quick read on pending decisions that are building institutional pressure.',
- href: '/approvals/approvals-list',
- },
- {
- key: 'super-admin-risk-watch',
- title: 'Control red-flag watchlist',
- collectionKey: 'riskPanel',
- note: 'The most severe open alerts currently demanding platform or executive follow-up.',
- href: '/compliance_alerts/compliance_alerts-list',
- },
- {
- key: 'super-admin-notice-watch',
- title: 'Platform notice watchlist',
- collectionKey: 'recentNotifications',
- note: 'Recent notices that can reveal workflow, access, or setup issues across organizations.',
- href: '/notifications/notifications-list',
+ key: 'super-admin-audit',
+ title: 'Audit events available for review',
+ metricKey: 'auditEventCount',
+ note: 'Recorded platform and tenant actions available for investigation or control review.',
+ href: '/audit_logs/audit_logs-list',
},
],
+ watchlistCards: [],
},
[WORKSPACE_ROLES.administrator]: {
summaryMetricKeys: ['pendingApprovals', 'unreadNotifications', 'openRiskAlerts', 'vendorComplianceAlerts'],
@@ -549,6 +542,11 @@ router.get(
vendorComplianceAlerts,
openRiskAlerts,
unreadNotifications,
+ organizationCount,
+ platformUserCount,
+ roleCount,
+ permissionCount,
+ auditEventCount,
averageProjectProgress,
highRiskProjects,
topContracts,
@@ -633,6 +631,17 @@ router.get(
read: false,
},
}),
+ db.organizations.count(),
+ db.users.count({
+ where: scopeByOrganization(currentUser, 'organizationsId'),
+ }),
+ db.roles.count(),
+ db.permissions.count(),
+ db.audit_logs.count({
+ where: {
+ ...scopeByOrganization(currentUser),
+ },
+ }),
db.projects.aggregate('completion_percent', 'avg', {
plain: true,
where: {
@@ -804,7 +813,7 @@ router.get(
]);
const provinceRolloutMap = rolloutProjects.reduce((accumulator, project) => {
- const provinceName = project.province?.name || 'Unassigned province';
+ const provinceName = project.province?.name || 'Province not assigned';
if (!accumulator[provinceName]) {
accumulator[provinceName] = {
@@ -837,21 +846,23 @@ router.get(
.sort((left, right) => right.totalProjects - left.totalProjects)
.slice(0, 6);
- const formattedApprovalQueue = approvalQueue.map((approval) => ({
+ const formattedApprovalQueue = approvalQueue
+ .filter((approval) => SUPPORTED_RECORD_TYPES.has(approval.record_type))
+ .map((approval) => ({
id: approval.id,
recordType: approval.record_type,
recordKey: approval.record_key,
status: approval.status,
requestedAt: approval.requested_at,
- workflowName: approval.workflow?.name || 'Workflow not assigned',
- stepName: approval.step?.name || 'No active step',
+ workflowName: approval.workflow?.name || 'Workflow not configured',
+ stepName: approval.step?.name || 'Pending step setup',
stepOrder: approval.step?.step_order || null,
requestedBy: approval.requested_by_user
? `${approval.requested_by_user.firstName || ''} ${approval.requested_by_user.lastName || ''}`.trim() || approval.requested_by_user.email
- : 'Unknown requester',
+ : 'Requester unavailable',
assignedTo: approval.assigned_to_user
? `${approval.assigned_to_user.firstName || ''} ${approval.assigned_to_user.lastName || ''}`.trim() || approval.assigned_to_user.email
- : 'Unassigned',
+ : 'Not assigned',
}));
const formattedProcurementQueue = procurementQueue.map((requisition) => ({
@@ -863,10 +874,12 @@ router.get(
currency: requisition.currency,
neededByDate: requisition.needed_by_date,
status: requisition.status,
- provinceName: requisition.province?.name || 'Unassigned province',
+ provinceName: requisition.province?.name || 'Province not assigned',
}));
- const formattedContractWatchlist = contractWatchlist.map((contract) => ({
+ const formattedContractWatchlist = contractWatchlist
+ .filter((contract) => contract.vendor?.name || contract.project?.name)
+ .map((contract) => ({
id: contract.id,
contractNumber: contract.contract_number,
title: contract.title,
@@ -881,7 +894,9 @@ router.get(
: null,
}));
- const formattedTopContracts = topContracts.map((contract) => ({
+ const formattedTopContracts = topContracts
+ .filter((contract) => contract.vendor?.name || contract.project?.name)
+ .map((contract) => ({
id: contract.id,
contractNumber: contract.contract_number,
title: contract.title,
@@ -893,7 +908,9 @@ router.get(
projectName: contract.project?.name || 'Project not linked',
}));
- const formattedRiskPanel = riskPanel.map((alert) => ({
+ const formattedRiskPanel = riskPanel
+ .filter((alert) => !alert.record_type || SUPPORTED_RECORD_TYPES.has(alert.record_type))
+ .map((alert) => ({
id: alert.id,
alertType: alert.alert_type,
severity: alert.severity,
@@ -905,10 +922,12 @@ router.get(
status: alert.status,
assignedTo: alert.assigned_to_user
? `${alert.assigned_to_user.firstName || ''} ${alert.assigned_to_user.lastName || ''}`.trim() || alert.assigned_to_user.email
- : 'Unassigned',
+ : 'Not assigned',
}));
- const formattedNotifications = recentNotifications.map((notification) => ({
+ const formattedNotifications = recentNotifications
+ .filter((notification) => !notification.record_type || SUPPORTED_RECORD_TYPES.has(notification.record_type))
+ .map((notification) => ({
id: notification.id,
type: notification.type,
title: notification.title,
@@ -944,6 +963,11 @@ router.get(
vendorComplianceAlerts,
openRiskAlerts,
unreadNotifications,
+ organizationCount,
+ platformUserCount,
+ roleCount,
+ permissionCount,
+ auditEventCount,
averageProjectProgress: Math.round(toNumber(averageProjectProgress) || 0),
highRiskProjects,
};
diff --git a/frontend/src/components/TableSampleClients.tsx b/frontend/src/components/TableSampleClients.tsx
index 3fb3411..f77d610 100644
--- a/frontend/src/components/TableSampleClients.tsx
+++ b/frontend/src/components/TableSampleClients.tsx
@@ -35,7 +35,7 @@ const TableSampleClients = () => {
return (
<>
Lorem ipsum dolor sit amet adipiscing elit
This is sample modal This is a preview modal.
Lorem ipsum dolor sit amet adipiscing elit
This is sample modal This is a preview modal.