Autosave: 20260404-161532

This commit is contained in:
Flatlogic Bot 2026-04-04 16:15:32 +00:00
parent 4d4711eef8
commit d7c816c260
12 changed files with 1000 additions and 540 deletions

View File

@ -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() {},
};

View File

@ -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() {},
};

View File

@ -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() {},
};

View File

@ -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.
*/
/**

View File

@ -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,
};

View File

@ -35,7 +35,7 @@ const TableSampleClients = () => {
return (
<>
<CardBoxModal
title="Sample modal"
title="Record preview"
buttonColor="info"
buttonLabel="Done"
isActive={isModalInfoActive}
@ -45,7 +45,7 @@ const TableSampleClients = () => {
<p>
Lorem ipsum dolor sit amet <b>adipiscing elit</b>
</p>
<p>This is sample modal</p>
<p>This is a preview modal.</p>
</CardBoxModal>
<CardBoxModal
@ -59,7 +59,7 @@ const TableSampleClients = () => {
<p>
Lorem ipsum dolor sit amet <b>adipiscing elit</b>
</p>
<p>This is sample modal</p>
<p>This is a preview modal.</p>
</CardBoxModal>
<table>

View File

@ -34,7 +34,12 @@ export type WorkspaceMetricKey =
| 'highRiskProjects'
| 'overduePayments'
| 'activeProjects'
| 'unreadNotifications';
| 'unreadNotifications'
| 'organizationCount'
| 'platformUserCount'
| 'roleCount'
| 'permissionCount'
| 'auditEventCount';
export type WorkspaceDetailBlockKey =
| 'focus'
@ -220,75 +225,67 @@ const workspaceConfigs: Record<string, WorkspaceConfig> = {
sidebarLabel: 'Platform Administration',
pageTitle: 'Platform Administration',
eyebrow: 'FDSU ERP · Platform Administration',
heroTitle: 'Set the platform structure, govern access, and watch cross-organization control risk.',
heroTitle: 'Govern tenants, access structure, and audit visibility across the platform.',
heroDescription:
'This workspace is for the Super Admin: organizations, top-level users, roles, permissions, and audit visibility. It keeps the platform safe and usable for every organization without turning the Super Admin into a day-to-day operator.',
'This workspace is for the Super Admin only. It focuses on organizations, platform users, role design, permission governance, and audit visibility across tenants without turning the Super Admin into an organization-level ERP operator.',
primaryAction: { href: '/organizations/organizations-list', label: 'Review organizations' },
secondaryAction: { href: '/roles/roles-list', label: 'Review roles' },
secondaryAction: { href: '/permissions/permissions-list', label: 'Review permissions' },
briefingCards: [
{
title: 'Should see',
items: ['Organizations and tenant coverage', 'Platform users, roles, and permissions', 'Audit activity and cross-organization access exceptions'],
items: ['Organizations using the platform', 'Platform users, roles, permissions, and access assignments', 'Audit activity and cross-tenant control exceptions'],
},
{
title: 'Should do',
items: ['Set platform governance and permission boundaries', 'Resolve top-level access issues', 'Support administrators without running daily transactions'],
items: ['Create and govern organizations', 'Maintain the platform access model and segregation of duties', 'Support administrators without owning daily ERP transactions'],
},
{
title: 'Receives from',
items: ['Tenant setup requests', 'Access-governance escalations', 'Platform-wide audit and control signals'],
items: ['Tenant setup requests', 'Access-governance escalations', 'Audit findings and platform support issues'],
},
{
title: 'Hands off to',
items: ['Administrators for organization execution', 'Functional leads for business decisions', 'Tenant owners for local follow-through'],
items: ['Administrators for organization execution', 'Functional leads for business workflows', 'Director General or tenant owners when decisions become operational'],
},
],
highlightedMetricKeys: ['pendingApprovals', 'openRiskAlerts', 'unreadNotifications', 'vendorComplianceAlerts', 'activeProjects', 'contractsNearingExpiry'],
heroMetricKeys: ['pendingApprovals', 'openRiskAlerts', 'unreadNotifications', 'vendorComplianceAlerts'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
highlightedMetricKeys: ['organizationCount', 'platformUserCount', 'roleCount', 'permissionCount', 'auditEventCount', 'unreadNotifications'],
heroMetricKeys: ['organizationCount', 'platformUserCount', 'roleCount', 'auditEventCount'],
blockOrder: ['summary', 'focus', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Governance queue',
title: 'Escalations and approvals that need platform-level attention',
actionLabel: 'Open governance queue',
},
riskPanel: {
eyebrow: 'Platform control exposure',
title: 'Cross-organization exceptions, access issues, and audit signals',
},
recentNotifications: {
eyebrow: 'Cross-organization notices',
title: 'Recent notices that can reveal access, setup, or control problems',
actionLabel: 'Open notices',
},
quickActions: {
eyebrow: 'Platform administration shortcuts',
title: 'Go directly to organizations, users, roles, and permissions',
title: 'Go directly to organizations, access models, permissions, audit logs, and notices',
},
},
quickLinks: [
{
href: '/organizations/organizations-list',
label: 'Organizations',
description: 'Review tenant setup, ownership, and coverage.',
description: 'Review tenants, owners, and platform coverage.',
icon: 'organizations',
},
{
href: '/users/users-list',
label: 'Users',
description: 'Manage top-level accounts and platform access.',
label: 'Platform users',
description: 'Inspect who has access across the platform.',
icon: 'users',
},
{
href: '/roles/roles-list',
label: 'Roles',
description: 'Define responsibility boundaries across the platform.',
description: 'Maintain responsibility boundaries and access structure.',
icon: 'users',
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
description: 'Inspect and tighten the permission matrix.',
description: 'Review the permission catalogue and control surface.',
icon: 'audit',
},
{
href: '/audit_logs/audit_logs-list',
label: 'Audit logs',
description: 'Trace platform activity and investigate access changes.',
icon: 'audit',
},
],

View File

@ -8,9 +8,9 @@ const optionalIcon = (name: string, fallback: string = icon.mdiTable): string =>
return iconSet[name] || fallback
}
const superAdminWorkspaceRoles = [WORKSPACE_ROLES.superAdmin]
const adminWorkspaceRoles = [WORKSPACE_ROLES.administrator]
const sharedEntityRoles = [
WORKSPACE_ROLES.superAdmin,
WORKSPACE_ROLES.directorGeneral,
WORKSPACE_ROLES.financeDirector,
WORKSPACE_ROLES.procurementLead,
@ -18,6 +18,78 @@ const sharedEntityRoles = [
WORKSPACE_ROLES.projectDeliveryLead,
]
const superAdminGroupedNavigation: MenuAsideItem[] = [
{
label: 'Platform Governance',
icon: icon.mdiShieldAccountOutline,
roles: superAdminWorkspaceRoles,
withDevider: true,
menu: [
{
href: '/organizations/organizations-list',
label: 'Organizations',
icon: optionalIcon('mdiDomain'),
permissions: 'READ_ORGANIZATIONS',
},
{
href: '/users/users-list',
label: 'Users',
icon: icon.mdiAccountGroup,
permissions: 'READ_USERS',
},
{
href: '/roles/roles-list',
label: 'Roles',
icon: optionalIcon('mdiShieldAccountVariantOutline'),
permissions: 'READ_ROLES',
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
icon: optionalIcon('mdiShieldAccountOutline'),
permissions: 'READ_PERMISSIONS',
},
{
href: '/role_permissions/role_permissions-list',
label: 'Role permissions',
icon: optionalIcon('mdiLinkVariant'),
permissions: 'READ_ROLE_PERMISSIONS',
},
],
},
{
label: 'Platform Workflow',
icon: icon.mdiSitemap,
roles: superAdminWorkspaceRoles,
menu: [
{
href: '/approval_workflows/approval_workflows-list',
label: 'Approval workflows',
icon: optionalIcon('mdiSitemap'),
permissions: 'READ_APPROVAL_WORKFLOWS',
},
{
href: '/approval_steps/approval_steps-list',
label: 'Approval steps',
icon: optionalIcon('mdiStairs'),
permissions: 'READ_APPROVAL_STEPS',
},
{
href: '/notifications/notifications-list',
label: 'Notifications',
icon: optionalIcon('mdiBell'),
permissions: 'READ_NOTIFICATIONS',
},
{
href: '/audit_logs/audit_logs-list',
label: 'Audit logs',
icon: optionalIcon('mdiClipboardTextClock'),
permissions: 'READ_AUDIT_LOGS',
},
],
},
]
const adminGroupedNavigation: MenuAsideItem[] = [
{
label: 'Workflow Readiness',
@ -354,351 +426,8 @@ const adminGroupedNavigation: MenuAsideItem[] = [
},
]
const sharedEntityNavigation: MenuAsideItem[] = [
{
href: '/users/users-list',
label: 'Users',
roles: [WORKSPACE_ROLES.superAdmin],
icon: icon.mdiAccountGroup,
permissions: 'READ_USERS',
},
{
href: '/roles/roles-list',
label: 'Roles',
roles: [WORKSPACE_ROLES.superAdmin],
icon: optionalIcon('mdiShieldAccountVariantOutline'),
permissions: 'READ_ROLES',
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
roles: [WORKSPACE_ROLES.superAdmin],
icon: optionalIcon('mdiShieldAccountOutline'),
permissions: 'READ_PERMISSIONS',
},
{
href: '/organizations/organizations-list',
label: 'Organizations',
roles: [WORKSPACE_ROLES.superAdmin],
icon: optionalIcon('mdiDomain'),
permissions: 'READ_ORGANIZATIONS',
},
{
href: '/provinces/provinces-list',
label: 'Provinces',
roles: sharedEntityRoles,
icon: optionalIcon('mdiMapMarker'),
permissions: 'READ_PROVINCES',
},
{
href: '/departments/departments-list',
label: 'Departments',
roles: sharedEntityRoles,
icon: optionalIcon('mdiOfficeBuilding'),
permissions: 'READ_DEPARTMENTS',
},
{
href: '/role_permissions/role_permissions-list',
label: 'Role permissions',
roles: sharedEntityRoles,
icon: optionalIcon('mdiLinkVariant'),
permissions: 'READ_ROLE_PERMISSIONS',
},
{
href: '/approval_workflows/approval_workflows-list',
label: 'Approval workflows',
roles: sharedEntityRoles,
icon: optionalIcon('mdiSitemap'),
permissions: 'READ_APPROVAL_WORKFLOWS',
},
{
href: '/approval_steps/approval_steps-list',
label: 'Approval steps',
roles: sharedEntityRoles,
icon: optionalIcon('mdiStairs'),
permissions: 'READ_APPROVAL_STEPS',
},
{
href: '/approvals/approvals-list',
label: 'Approvals',
roles: sharedEntityRoles,
icon: optionalIcon('mdiChecklist'),
permissions: 'READ_APPROVALS',
},
{
href: '/notifications/notifications-list',
label: 'Notifications',
roles: sharedEntityRoles,
icon: optionalIcon('mdiBell'),
permissions: 'READ_NOTIFICATIONS',
},
{
href: '/audit_logs/audit_logs-list',
label: 'Audit logs',
roles: sharedEntityRoles,
icon: optionalIcon('mdiClipboardTextClock'),
permissions: 'READ_AUDIT_LOGS',
},
{
href: '/fiscal_years/fiscal_years-list',
label: 'Fiscal years',
roles: sharedEntityRoles,
icon: optionalIcon('mdiCalendarRange'),
permissions: 'READ_FISCAL_YEARS',
},
{
href: '/funding_sources/funding_sources-list',
label: 'Funding sources',
roles: sharedEntityRoles,
icon: optionalIcon('mdiCashMultiple'),
permissions: 'READ_FUNDING_SOURCES',
},
{
href: '/budget_programs/budget_programs-list',
label: 'Budget programs',
roles: sharedEntityRoles,
icon: optionalIcon('mdiChartDonut'),
permissions: 'READ_BUDGET_PROGRAMS',
},
{
href: '/budget_lines/budget_lines-list',
label: 'Budget lines',
roles: sharedEntityRoles,
icon: optionalIcon('mdiFormatListBulleted'),
permissions: 'READ_BUDGET_LINES',
},
{
href: '/allocations/allocations-list',
label: 'Allocations',
roles: sharedEntityRoles,
icon: optionalIcon('mdiDatabaseArrowRight'),
permissions: 'READ_ALLOCATIONS',
},
{
href: '/budget_reallocations/budget_reallocations-list',
label: 'Budget reallocations',
roles: sharedEntityRoles,
icon: optionalIcon('mdiSwapHorizontal'),
permissions: 'READ_BUDGET_REALLOCATIONS',
},
{
href: '/procurement_plans/procurement_plans-list',
label: 'Procurement plans',
roles: sharedEntityRoles,
icon: optionalIcon('mdiClipboardList'),
permissions: 'READ_PROCUREMENT_PLANS',
},
{
href: '/requisitions/requisitions-list',
label: 'Requisitions',
roles: sharedEntityRoles,
icon: optionalIcon('mdiFileDocumentEdit'),
permissions: 'READ_REQUISITIONS',
},
{
href: '/tenders/tenders-list',
label: 'Tenders',
roles: sharedEntityRoles,
icon: optionalIcon('mdiGavel'),
permissions: 'READ_TENDERS',
},
{
href: '/vendors/vendors-list',
label: 'Vendors',
roles: sharedEntityRoles,
icon: optionalIcon('mdiTruckFast'),
permissions: 'READ_VENDORS',
},
{
href: '/vendor_compliance_documents/vendor_compliance_documents-list',
label: 'Vendor compliance documents',
roles: sharedEntityRoles,
icon: optionalIcon('mdiFileCertificate'),
permissions: 'READ_VENDOR_COMPLIANCE_DOCUMENTS',
},
{
href: '/bids/bids-list',
label: 'Bids',
roles: sharedEntityRoles,
icon: optionalIcon('mdiFileSign'),
permissions: 'READ_BIDS',
},
{
href: '/bid_evaluations/bid_evaluations-list',
label: 'Bid evaluations',
roles: sharedEntityRoles,
icon: optionalIcon('mdiClipboardCheck'),
permissions: 'READ_BID_EVALUATIONS',
},
{
href: '/awards/awards-list',
label: 'Awards',
roles: sharedEntityRoles,
icon: optionalIcon('mdiTrophyAward'),
permissions: 'READ_AWARDS',
},
{
href: '/programs/programs-list',
label: 'Programs',
roles: sharedEntityRoles,
icon: optionalIcon('mdiViewGridOutline'),
permissions: 'READ_PROGRAMS',
},
{
href: '/projects/projects-list',
label: 'Projects',
roles: sharedEntityRoles,
icon: optionalIcon('mdiBriefcaseCheck'),
permissions: 'READ_PROJECTS',
},
{
href: '/project_milestones/project_milestones-list',
label: 'Project milestones',
roles: sharedEntityRoles,
icon: optionalIcon('mdiTimelineCheck'),
permissions: 'READ_PROJECT_MILESTONES',
},
{
href: '/risks/risks-list',
label: 'Risks',
roles: sharedEntityRoles,
icon: optionalIcon('mdiAlertOctagon'),
permissions: 'READ_RISKS',
},
{
href: '/issues/issues-list',
label: 'Issues',
roles: sharedEntityRoles,
icon: optionalIcon('mdiBug'),
permissions: 'READ_ISSUES',
},
{
href: '/field_verifications/field_verifications-list',
label: 'Field verifications',
roles: sharedEntityRoles,
icon: optionalIcon('mdiMapMarkerCheck'),
permissions: 'READ_FIELD_VERIFICATIONS',
},
{
href: '/contracts/contracts-list',
label: 'Contracts',
roles: sharedEntityRoles,
icon: optionalIcon('mdiFileDocumentOutline'),
permissions: 'READ_CONTRACTS',
},
{
href: '/contract_amendments/contract_amendments-list',
label: 'Contract amendments',
roles: sharedEntityRoles,
icon: optionalIcon('mdiFileReplaceOutline'),
permissions: 'READ_CONTRACT_AMENDMENTS',
},
{
href: '/contract_milestones/contract_milestones-list',
label: 'Contract milestones',
roles: sharedEntityRoles,
icon: optionalIcon('mdiTimelineCheck'),
permissions: 'READ_CONTRACT_MILESTONES',
},
{
href: '/grants/grants-list',
label: 'Grants',
roles: sharedEntityRoles,
icon: optionalIcon('mdiHandCoin'),
permissions: 'READ_GRANTS',
},
{
href: '/beneficiaries/beneficiaries-list',
label: 'Beneficiaries',
roles: sharedEntityRoles,
icon: optionalIcon('mdiAccountGroup'),
permissions: 'READ_BENEFICIARIES',
},
{
href: '/grant_applications/grant_applications-list',
label: 'Grant applications',
roles: sharedEntityRoles,
icon: optionalIcon('mdiFileDocumentMultiple'),
permissions: 'READ_GRANT_APPLICATIONS',
},
{
href: '/grant_evaluations/grant_evaluations-list',
label: 'Grant evaluations',
roles: sharedEntityRoles,
icon: optionalIcon('mdiStarCheck'),
permissions: 'READ_GRANT_EVALUATIONS',
},
{
href: '/grant_tranches/grant_tranches-list',
label: 'Grant tranches',
roles: sharedEntityRoles,
icon: optionalIcon('mdiCashCheck'),
permissions: 'READ_GRANT_TRANCHES',
},
{
href: '/expense_categories/expense_categories-list',
label: 'Expense categories',
roles: sharedEntityRoles,
icon: optionalIcon('mdiTagMultiple'),
permissions: 'READ_EXPENSE_CATEGORIES',
},
{
href: '/invoices/invoices-list',
label: 'Invoices',
roles: sharedEntityRoles,
icon: optionalIcon('mdiReceiptText'),
permissions: 'READ_INVOICES',
},
{
href: '/payment_requests/payment_requests-list',
label: 'Payment requests',
roles: sharedEntityRoles,
icon: optionalIcon('mdiCashFast'),
permissions: 'READ_PAYMENT_REQUESTS',
},
{
href: '/payment_batches/payment_batches-list',
label: 'Payment batches',
roles: sharedEntityRoles,
icon: optionalIcon('mdiPackageVariantClosed'),
permissions: 'READ_PAYMENT_BATCHES',
},
{
href: '/payments/payments-list',
label: 'Payments',
roles: sharedEntityRoles,
icon: optionalIcon('mdiBankTransfer'),
permissions: 'READ_PAYMENTS',
},
{
href: '/obligations/obligations-list',
label: 'Obligations',
roles: sharedEntityRoles,
icon: optionalIcon('mdiBookArrowDown'),
permissions: 'READ_OBLIGATIONS',
},
{
href: '/ledger_entries/ledger_entries-list',
label: 'Ledger entries',
roles: sharedEntityRoles,
icon: optionalIcon('mdiBookOpenPageVariant'),
permissions: 'READ_LEDGER_ENTRIES',
},
{
href: '/documents/documents-list',
label: 'Documents',
roles: sharedEntityRoles,
icon: optionalIcon('mdiFolderFile'),
permissions: 'READ_DOCUMENTS',
},
{
href: '/compliance_alerts/compliance_alerts-list',
label: 'Compliance alerts',
roles: sharedEntityRoles,
icon: optionalIcon('mdiShieldAlert'),
permissions: 'READ_COMPLIANCE_ALERTS',
},
]
const sharedEntityNavigation: MenuAsideItem[] = []
const menuAside: MenuAsideItem[] = [
{
@ -725,21 +454,7 @@ const menuAside: MenuAsideItem[] = [
},
roles: [WORKSPACE_ROLES.superAdmin, WORKSPACE_ROLES.administrator],
},
{
href: '/organizations/organizations-list',
label: 'Tenant Oversight',
icon: icon.mdiDomain,
permissions: 'READ_ORGANIZATIONS',
roles: [WORKSPACE_ROLES.superAdmin],
withDevider: true,
},
{
href: '/users/users-list',
label: 'Access Control',
icon: icon.mdiAccountGroup,
permissions: 'READ_USERS',
roles: [WORKSPACE_ROLES.superAdmin],
},
...superAdminGroupedNavigation,
...adminGroupedNavigation,
{
href: '/projects/projects-list',

View File

@ -19,6 +19,33 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const DASHBOARD_ENTITY_SCOPE_BY_ROLE: Record<string, string[]> = {
'Super Administrator': [
'users',
'roles',
'permissions',
'organizations',
'role_permissions',
'approval_workflows',
'approval_steps',
'notifications',
'audit_logs',
],
Administrator: [
'users',
'provinces',
'departments',
'role_permissions',
'approval_workflows',
'approval_steps',
'approvals',
'notifications',
'audit_logs',
'documents',
'compliance_alerts',
],
};
const Dashboard = () => {
const dispatch = useAppDispatch();
const router = useRouter();
@ -90,17 +117,19 @@ const Dashboard = () => {
const roleName = currentUser?.app_role?.name;
const dashboardAllowed = isDashboardRole(roleName);
const workspaceRoute = getWorkspaceRoute(roleName);
const allowedDashboardEntities = DASHBOARD_ENTITY_SCOPE_BY_ROLE[roleName || ''] || [];
const canShowDashboardEntity = (entity: string, permission: string) => allowedDashboardEntities.includes(entity) && hasPermission(currentUser, permission);
const dashboardCopy = roleName === 'Super Administrator'
? {
pageTitle: 'Platform Widgets',
eyebrow: 'Super Administrator widget surface',
description: 'Configure and review widgets for tenant governance, access control, and cross-organization oversight.',
description: 'Configure and review widgets for organizations, platform users, access models, workflow templates, notices, and audit visibility.',
widgetLabelFallback: 'Super Administrator',
}
: {
pageTitle: 'Operations Widgets',
eyebrow: 'Administrator operations surface',
description: 'Configure and review widgets for workflow readiness, operational notices, and day-to-day administrative follow-through.',
description: 'Configure and review widgets for users, master records, approval routing, notices, documents, and administrative control follow-through.',
widgetLabelFallback: 'Administrator',
};
@ -112,7 +141,7 @@ const Dashboard = () => {
const requests = entities.map((entity, index) => {
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
if (allowedDashboardEntities.includes(entity) && hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
return axios.get(`/${entity.toLowerCase()}/count`);
} else {
fns[index](null);
@ -233,7 +262,7 @@ const Dashboard = () => {
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
{canShowDashboardEntity('users', 'READ_USERS') && <Link href={'/users/users-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -261,7 +290,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
{canShowDashboardEntity('roles', 'READ_ROLES') && <Link href={'/roles/roles-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -289,7 +318,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
{canShowDashboardEntity('permissions', 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -317,7 +346,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_ORGANIZATIONS') && <Link href={'/organizations/organizations-list'}>
{canShowDashboardEntity('organizations', 'READ_ORGANIZATIONS') && <Link href={'/organizations/organizations-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -345,7 +374,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PROVINCES') && <Link href={'/provinces/provinces-list'}>
{canShowDashboardEntity('provinces', 'READ_PROVINCES') && <Link href={'/provinces/provinces-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -373,7 +402,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_DEPARTMENTS') && <Link href={'/departments/departments-list'}>
{canShowDashboardEntity('departments', 'READ_DEPARTMENTS') && <Link href={'/departments/departments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -401,7 +430,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_ROLE_PERMISSIONS') && <Link href={'/role_permissions/role_permissions-list'}>
{canShowDashboardEntity('role_permissions', 'READ_ROLE_PERMISSIONS') && <Link href={'/role_permissions/role_permissions-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -429,7 +458,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_APPROVAL_WORKFLOWS') && <Link href={'/approval_workflows/approval_workflows-list'}>
{canShowDashboardEntity('approval_workflows', 'READ_APPROVAL_WORKFLOWS') && <Link href={'/approval_workflows/approval_workflows-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -457,7 +486,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_APPROVAL_STEPS') && <Link href={'/approval_steps/approval_steps-list'}>
{canShowDashboardEntity('approval_steps', 'READ_APPROVAL_STEPS') && <Link href={'/approval_steps/approval_steps-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -485,7 +514,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_APPROVALS') && <Link href={'/approvals/approvals-list'}>
{canShowDashboardEntity('approvals', 'READ_APPROVALS') && <Link href={'/approvals/approvals-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -513,7 +542,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_NOTIFICATIONS') && <Link href={'/notifications/notifications-list'}>
{canShowDashboardEntity('notifications', 'READ_NOTIFICATIONS') && <Link href={'/notifications/notifications-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -541,7 +570,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_AUDIT_LOGS') && <Link href={'/audit_logs/audit_logs-list'}>
{canShowDashboardEntity('audit_logs', 'READ_AUDIT_LOGS') && <Link href={'/audit_logs/audit_logs-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -569,7 +598,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_FISCAL_YEARS') && <Link href={'/fiscal_years/fiscal_years-list'}>
{canShowDashboardEntity('fiscal_years', 'READ_FISCAL_YEARS') && <Link href={'/fiscal_years/fiscal_years-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -597,7 +626,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_FUNDING_SOURCES') && <Link href={'/funding_sources/funding_sources-list'}>
{canShowDashboardEntity('funding_sources', 'READ_FUNDING_SOURCES') && <Link href={'/funding_sources/funding_sources-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -625,7 +654,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_BUDGET_PROGRAMS') && <Link href={'/budget_programs/budget_programs-list'}>
{canShowDashboardEntity('budget_programs', 'READ_BUDGET_PROGRAMS') && <Link href={'/budget_programs/budget_programs-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -653,7 +682,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_BUDGET_LINES') && <Link href={'/budget_lines/budget_lines-list'}>
{canShowDashboardEntity('budget_lines', 'READ_BUDGET_LINES') && <Link href={'/budget_lines/budget_lines-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -681,7 +710,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_ALLOCATIONS') && <Link href={'/allocations/allocations-list'}>
{canShowDashboardEntity('allocations', 'READ_ALLOCATIONS') && <Link href={'/allocations/allocations-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -709,7 +738,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_BUDGET_REALLOCATIONS') && <Link href={'/budget_reallocations/budget_reallocations-list'}>
{canShowDashboardEntity('budget_reallocations', 'READ_BUDGET_REALLOCATIONS') && <Link href={'/budget_reallocations/budget_reallocations-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -737,7 +766,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PROCUREMENT_PLANS') && <Link href={'/procurement_plans/procurement_plans-list'}>
{canShowDashboardEntity('procurement_plans', 'READ_PROCUREMENT_PLANS') && <Link href={'/procurement_plans/procurement_plans-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -765,7 +794,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_REQUISITIONS') && <Link href={'/requisitions/requisitions-list'}>
{canShowDashboardEntity('requisitions', 'READ_REQUISITIONS') && <Link href={'/requisitions/requisitions-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -793,7 +822,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_TENDERS') && <Link href={'/tenders/tenders-list'}>
{canShowDashboardEntity('tenders', 'READ_TENDERS') && <Link href={'/tenders/tenders-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -821,7 +850,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_VENDORS') && <Link href={'/vendors/vendors-list'}>
{canShowDashboardEntity('vendors', 'READ_VENDORS') && <Link href={'/vendors/vendors-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -849,7 +878,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_VENDOR_COMPLIANCE_DOCUMENTS') && <Link href={'/vendor_compliance_documents/vendor_compliance_documents-list'}>
{canShowDashboardEntity('vendor_compliance_documents', 'READ_VENDOR_COMPLIANCE_DOCUMENTS') && <Link href={'/vendor_compliance_documents/vendor_compliance_documents-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -877,7 +906,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_BIDS') && <Link href={'/bids/bids-list'}>
{canShowDashboardEntity('bids', 'READ_BIDS') && <Link href={'/bids/bids-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -905,7 +934,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_BID_EVALUATIONS') && <Link href={'/bid_evaluations/bid_evaluations-list'}>
{canShowDashboardEntity('bid_evaluations', 'READ_BID_EVALUATIONS') && <Link href={'/bid_evaluations/bid_evaluations-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -933,7 +962,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_AWARDS') && <Link href={'/awards/awards-list'}>
{canShowDashboardEntity('awards', 'READ_AWARDS') && <Link href={'/awards/awards-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -961,7 +990,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PROGRAMS') && <Link href={'/programs/programs-list'}>
{canShowDashboardEntity('programs', 'READ_PROGRAMS') && <Link href={'/programs/programs-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -989,7 +1018,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PROJECTS') && <Link href={'/projects/projects-list'}>
{canShowDashboardEntity('projects', 'READ_PROJECTS') && <Link href={'/projects/projects-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1017,7 +1046,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PROJECT_MILESTONES') && <Link href={'/project_milestones/project_milestones-list'}>
{canShowDashboardEntity('project_milestones', 'READ_PROJECT_MILESTONES') && <Link href={'/project_milestones/project_milestones-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1045,7 +1074,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_RISKS') && <Link href={'/risks/risks-list'}>
{canShowDashboardEntity('risks', 'READ_RISKS') && <Link href={'/risks/risks-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1073,7 +1102,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_ISSUES') && <Link href={'/issues/issues-list'}>
{canShowDashboardEntity('issues', 'READ_ISSUES') && <Link href={'/issues/issues-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1101,7 +1130,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_FIELD_VERIFICATIONS') && <Link href={'/field_verifications/field_verifications-list'}>
{canShowDashboardEntity('field_verifications', 'READ_FIELD_VERIFICATIONS') && <Link href={'/field_verifications/field_verifications-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1129,7 +1158,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_CONTRACTS') && <Link href={'/contracts/contracts-list'}>
{canShowDashboardEntity('contracts', 'READ_CONTRACTS') && <Link href={'/contracts/contracts-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1157,7 +1186,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_CONTRACT_AMENDMENTS') && <Link href={'/contract_amendments/contract_amendments-list'}>
{canShowDashboardEntity('contract_amendments', 'READ_CONTRACT_AMENDMENTS') && <Link href={'/contract_amendments/contract_amendments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1185,7 +1214,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_CONTRACT_MILESTONES') && <Link href={'/contract_milestones/contract_milestones-list'}>
{canShowDashboardEntity('contract_milestones', 'READ_CONTRACT_MILESTONES') && <Link href={'/contract_milestones/contract_milestones-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1213,7 +1242,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_GRANTS') && <Link href={'/grants/grants-list'}>
{canShowDashboardEntity('grants', 'READ_GRANTS') && <Link href={'/grants/grants-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1241,7 +1270,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_BENEFICIARIES') && <Link href={'/beneficiaries/beneficiaries-list'}>
{canShowDashboardEntity('beneficiaries', 'READ_BENEFICIARIES') && <Link href={'/beneficiaries/beneficiaries-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1269,7 +1298,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_GRANT_APPLICATIONS') && <Link href={'/grant_applications/grant_applications-list'}>
{canShowDashboardEntity('grant_applications', 'READ_GRANT_APPLICATIONS') && <Link href={'/grant_applications/grant_applications-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1297,7 +1326,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_GRANT_EVALUATIONS') && <Link href={'/grant_evaluations/grant_evaluations-list'}>
{canShowDashboardEntity('grant_evaluations', 'READ_GRANT_EVALUATIONS') && <Link href={'/grant_evaluations/grant_evaluations-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1325,7 +1354,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_GRANT_TRANCHES') && <Link href={'/grant_tranches/grant_tranches-list'}>
{canShowDashboardEntity('grant_tranches', 'READ_GRANT_TRANCHES') && <Link href={'/grant_tranches/grant_tranches-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1353,7 +1382,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_EXPENSE_CATEGORIES') && <Link href={'/expense_categories/expense_categories-list'}>
{canShowDashboardEntity('expense_categories', 'READ_EXPENSE_CATEGORIES') && <Link href={'/expense_categories/expense_categories-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1381,7 +1410,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_INVOICES') && <Link href={'/invoices/invoices-list'}>
{canShowDashboardEntity('invoices', 'READ_INVOICES') && <Link href={'/invoices/invoices-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1409,7 +1438,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PAYMENT_REQUESTS') && <Link href={'/payment_requests/payment_requests-list'}>
{canShowDashboardEntity('payment_requests', 'READ_PAYMENT_REQUESTS') && <Link href={'/payment_requests/payment_requests-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1437,7 +1466,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PAYMENT_BATCHES') && <Link href={'/payment_batches/payment_batches-list'}>
{canShowDashboardEntity('payment_batches', 'READ_PAYMENT_BATCHES') && <Link href={'/payment_batches/payment_batches-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1465,7 +1494,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_PAYMENTS') && <Link href={'/payments/payments-list'}>
{canShowDashboardEntity('payments', 'READ_PAYMENTS') && <Link href={'/payments/payments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1493,7 +1522,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_OBLIGATIONS') && <Link href={'/obligations/obligations-list'}>
{canShowDashboardEntity('obligations', 'READ_OBLIGATIONS') && <Link href={'/obligations/obligations-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1521,7 +1550,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_LEDGER_ENTRIES') && <Link href={'/ledger_entries/ledger_entries-list'}>
{canShowDashboardEntity('ledger_entries', 'READ_LEDGER_ENTRIES') && <Link href={'/ledger_entries/ledger_entries-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1549,7 +1578,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_DOCUMENTS') && <Link href={'/documents/documents-list'}>
{canShowDashboardEntity('documents', 'READ_DOCUMENTS') && <Link href={'/documents/documents-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
@ -1577,7 +1606,7 @@ const Dashboard = () => {
</div>
</Link>}
{hasPermission(currentUser, 'READ_COMPLIANCE_ALERTS') && <Link href={'/compliance_alerts/compliance_alerts-list'}>
{canShowDashboardEntity('compliance_alerts', 'READ_COMPLIANCE_ALERTS') && <Link href={'/compliance_alerts/compliance_alerts-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>

View File

@ -11,18 +11,18 @@ export default function Error() {
return (
<>
<Head>
<title>{getPageTitle('Error')}</title>
<title>{getPageTitle('Access denied')}</title>
</Head>
<SectionFullScreen bg="pinkRed">
<CardBox
className="w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12 shadow-2xl"
footer={<BaseButton href="/dashboard" label="Done" color="danger" />}
footer={<BaseButton href="/" label="Return to app" color="danger" />}
>
<div className="space-y-3">
<h1 className="text-2xl">Unhandled exception</h1>
<h1 className="text-2xl">Access denied</h1>
<p>An Error Occurred</p>
<p>You do not have permission to open that page with your current role.</p>
</div>
</CardBox>
</SectionFullScreen>

View File

@ -51,6 +51,11 @@ interface Summary {
vendorComplianceAlerts: number;
openRiskAlerts: number;
unreadNotifications: number;
organizationCount: number;
platformUserCount: number;
roleCount: number;
permissionCount: number;
auditEventCount: number;
averageProjectProgress: number;
highRiskProjects: number;
}
@ -186,6 +191,11 @@ const defaultResponse: ExecutiveSummaryResponse = {
vendorComplianceAlerts: 0,
openRiskAlerts: 0,
unreadNotifications: 0,
organizationCount: 0,
platformUserCount: 0,
roleCount: 0,
permissionCount: 0,
auditEventCount: 0,
averageProjectProgress: 0,
highRiskProjects: 0,
},
@ -527,6 +537,36 @@ const ExecutiveSummaryPage = () => {
note: 'System notices, workflow events, and alerts awaiting attention.',
icon: mdiBellOutline,
},
organizationCount: {
title: 'Organizations',
value: `${data.summary.organizationCount}`,
note: 'Organizations currently governed on the platform.',
icon: mdiBankOutline,
},
platformUserCount: {
title: 'Platform users',
value: `${data.summary.platformUserCount}`,
note: 'User accounts currently under platform access governance.',
icon: mdiCheckDecagramOutline,
},
roleCount: {
title: 'Roles',
value: `${data.summary.roleCount}`,
note: 'Role definitions shaping responsibility and segregation of duties.',
icon: mdiClipboardListOutline,
},
permissionCount: {
title: 'Permissions',
value: `${data.summary.permissionCount}`,
note: 'Permission records defining the access-control surface.',
icon: mdiShieldAlertOutline,
},
auditEventCount: {
title: 'Audit events',
value: `${data.summary.auditEventCount}`,
note: 'Audit log entries available for review and investigation.',
icon: mdiBellOutline,
},
}),
[data.summary],
);
@ -666,7 +706,7 @@ const ExecutiveSummaryPage = () => {
</div>
) : (
<div className='mt-5 rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500 dark:text-slate-400 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400'>
No pending approvals are queued right now. Start a new requisition to test the workflow end to end.
No pending approvals are queued right now. New items will appear here when records reach the approval flow.
</div>
)}
</CardBox>
@ -933,9 +973,9 @@ const ExecutiveSummaryPage = () => {
title={workspaceConfig.sectionCopy.topContracts.title}
action={
<BaseButton
href='/vendors/vendors-list'
href='/contracts/contracts-list'
color='whiteDark'
label={workspaceConfig.sectionCopy.topContracts.actionLabel || 'Vendor master'}
label={workspaceConfig.sectionCopy.topContracts.actionLabel || 'Contract register'}
/>
}
/>

View File

@ -23,6 +23,16 @@ import {toast, ToastContainer} from "react-toastify";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
import { getPostLoginRoute } from '../helpers/workspace';
const roleAccounts = [
{ email: 'super_admin@flatlogic.com', password: '5e8f2960', roleLabel: 'Super Administrator' },
{ email: 'admin@flatlogic.com', password: '5e8f2960', roleLabel: 'Administrator' },
{ email: 'director.general@flatlogic.com', password: '242b5480541a', roleLabel: 'Director General' },
{ email: 'finance.director@flatlogic.com', password: '242b5480541a', roleLabel: 'Finance Director' },
{ email: 'procurement.lead@flatlogic.com', password: '242b5480541a', roleLabel: 'Procurement Lead' },
{ email: 'compliance.audit@flatlogic.com', password: '242b5480541a', roleLabel: 'Compliance and Audit Lead' },
{ email: 'project.delivery@flatlogic.com', password: '242b5480541a', roleLabel: 'Project Delivery Lead' },
];
export default function Login() {
const router = useRouter();
const dispatch = useAppDispatch();
@ -41,9 +51,11 @@ export default function Login() {
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
(state) => state.auth,
);
const [initialValues, setInitialValues] = React.useState({ email:'super_admin@flatlogic.com',
password: '5e8f2960',
remember: true })
const [initialValues, setInitialValues] = React.useState({
email: roleAccounts[0].email,
password: roleAccounts[0].password,
remember: true,
})
const title = 'FDSU ERP'
@ -170,30 +182,24 @@ export default function Login() {
<h2 className="text-4xl font-semibold my-4">{title}</h2>
<div className='flex flex-row text-gray-500 justify-between'>
<div>
<p className='mb-2'>Use{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="5e8f2960"
onClick={(e) => setLogin(e.target)}>super_admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>5e8f2960</code>{' / '}
to login as Super Admin</p>
<p className='mb-2'>Use{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="5e8f2960"
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>5e8f2960</code>{' / '}
to login as Admin</p>
<p>Use <code
<div className='flex flex-col gap-4 text-gray-500 md:flex-row md:justify-between'>
<div className='space-y-3'>
<p className='text-sm text-gray-500'>Role access accounts for this workspace. Click any email to autofill the form.</p>
{roleAccounts.map((account) => (
<p key={account.email}>
<span className='font-medium text-gray-700 dark:text-gray-200'>{account.roleLabel}:</span>{' '}
<code
className={`cursor-pointer ${textColor}`}
data-password="242b5480541a"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>242b5480541a</code>{' / '}
to login as User</p>
data-password={account.password}
onClick={(e) => setLogin(e.target as HTMLElement)}
>
{account.email}
</code>{' / '}
<code className={`${textColor}`}>{account.password}</code>
</p>
))}
</div>
<div>
<div className='flex justify-end md:block'>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
@ -214,7 +220,7 @@ export default function Login() {
<Form>
<FormField
label='Login'
help='Please enter your login'>
help='Please enter your email address'>
<Field name='email' />
</FormField>