Compare commits

...

6 Commits

Author SHA1 Message Date
Flatlogic Bot
18dbc6c578 Autosave: 20260404-180248 2026-04-04 18:02:49 +00:00
Flatlogic Bot
8ceeef1051 Autosave: 20260404-171444 2026-04-04 17:14:44 +00:00
Flatlogic Bot
d7c816c260 Autosave: 20260404-161532 2026-04-04 16:15:32 +00:00
Flatlogic Bot
4d4711eef8 Autosave: 20260404-052155 2026-04-04 05:21:56 +00:00
Flatlogic Bot
8d94ffcf46 Autosave: 20260404-043544 2026-04-04 04:35:44 +00:00
Flatlogic Bot
4f12eae1aa Autosave: 20260404-034952 2026-04-04 03:49:52 +00:00
134 changed files with 8244 additions and 53218 deletions

View File

@ -36,6 +36,8 @@ const Departments = db.departments;
const RolePermissions = db.role_permissions;
const Permissions = db.permissions;
const ApprovalWorkflows = db.approval_workflows;
const ApprovalSteps = db.approval_steps;

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

@ -121,6 +121,8 @@ const documentsRoutes = require('./routes/documents');
const compliance_alertsRoutes = require('./routes/compliance_alerts');
const executive_summaryRoutes = require('./routes/executive_summary');
const getBaseUrl = (url) => {
if (!url) return '';
@ -277,6 +279,8 @@ app.use('/api/documents', passport.authenticate('jwt', {session: false}), docume
app.use('/api/compliance_alerts', passport.authenticate('jwt', {session: false}), compliance_alertsRoutes);
app.use('/api/executive-summary', passport.authenticate('jwt', {session: false}), executive_summaryRoutes);
app.use(
'/api/openai',
passport.authenticate('jwt', { session: false }),

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

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ export default function AsideMenu({
<>
<AsideMenuLayer
menu={props.menu}
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
className={`${isAsideMobileExpanded ? 'left-0' : '-left-72 lg:left-0'} ${
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
}`}
onAsideLgCloseClick={props.onAsideLgClose}

View File

@ -1,18 +1,32 @@
import React, { useEffect, useState } from 'react'
import { mdiMinus, mdiPlus } from '@mdi/js'
import BaseIcon from './BaseIcon'
import { mdiChevronDown, mdiChevronUp } from '@mdi/js'
import Link from 'next/link'
import { useRouter } from 'next/router'
import BaseIcon from './BaseIcon'
import { getButtonColor } from '../colors'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
import { useRouter } from 'next/router'
type Props = {
item: MenuAsideItem
isDropdownList?: boolean
}
const getActiveView = (path: string) => new URL(path, location.href).pathname.split('/')[1]
const itemMatchesActiveView = (menuItem: MenuAsideItem, activeView: string): boolean => {
if (menuItem.href && getActiveView(menuItem.href) === activeView) {
return true
}
if (!menuItem.menu?.length) {
return false
}
return menuItem.menu.some((nestedItem) => itemMatchesActiveView(nestedItem, activeView))
}
const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const [isLinkActive, setIsLinkActive] = useState(false)
const [isDropdownActive, setIsDropdownActive] = useState(false)
@ -20,77 +34,78 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle)
const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle)
const asideMenuItemActiveStyle = useAppSelector((state) => state.style.asideMenuItemActiveStyle)
const borders = useAppSelector((state) => state.style.borders);
const activeLinkColor = useAppSelector(
(state) => state.style.activeLinkColor,
);
const borders = useAppSelector((state) => state.style.borders)
const activeLinkColor = useAppSelector((state) => state.style.activeLinkColor)
const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : ''
const isGroupTrigger = Boolean(item.menu && !item.href && !isDropdownList)
const { asPath, isReady } = useRouter()
useEffect(() => {
if (item.href && isReady) {
const linkPathName = new URL(item.href, location.href).pathname + '/';
const activePathname = new URL(asPath, location.href).pathname
const activeView = activePathname.split('/')[1];
const linkPathNameView = linkPathName.split('/')[1];
setIsLinkActive(linkPathNameView === activeView);
if (!isReady) {
return
}
}, [item.href, isReady, asPath])
const activeView = getActiveView(asPath)
if (item.href) {
setIsLinkActive(getActiveView(item.href) === activeView)
}
if (item.menu?.length) {
setIsDropdownActive(item.menu.some((nestedItem) => itemMatchesActiveView(nestedItem, activeView)))
}
}, [item.href, item.menu, isReady, asPath])
const asideMenuItemInnerContents = (
<>
{item.icon && (
<BaseIcon path={item.icon} className={`flex-none mx-3 ${activeClassAddon}`} size="18" />
<span className='flex h-9 w-9 flex-none items-center justify-center rounded-xl'>
<BaseIcon path={item.icon} className={`${activeClassAddon}`} size='18' />
</span>
)}
<span
className={`grow text-ellipsis line-clamp-1 ${
item.menu ? '' : 'pr-12'
} ${activeClassAddon}`}
className={`min-w-0 grow whitespace-normal break-words leading-5 line-clamp-2 ${item.menu ? '' : 'pr-3'} ${isGroupTrigger ? 'text-[0.93rem] tracking-[0.01em]' : ''} ${activeClassAddon}`}
>
{item.label}
</span>
{item.menu && (
<BaseIcon
path={isDropdownActive ? mdiMinus : mdiPlus}
path={isDropdownActive ? mdiChevronUp : mdiChevronDown}
className={`flex-none ${activeClassAddon}`}
w="w-12"
size='18'
/>
)}
</>
)
const componentClass = [
'flex cursor-pointer py-1.5 ',
isDropdownList ? 'px-6 text-sm' : '',
item.color
? getButtonColor(item.color, false, true)
: `${asideMenuItemStyle}`,
isLinkActive
? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800`
: '',
].join(' ');
'group flex w-full items-start gap-3 rounded-xl px-3 text-left transition-all duration-150',
isDropdownList ? 'py-2 text-sm' : 'py-2.5',
item.color ? getButtonColor(item.color, false, true) : `${asideMenuItemStyle}`,
isGroupTrigger ? 'font-semibold' : '',
isLinkActive ? `${activeLinkColor} shadow-sm` : '',
].join(' ')
return (
<li className={'px-3 py-1.5'}>
<li className={isDropdownList ? 'px-2 py-1' : 'px-2 py-1.5'}>
{item.withDevider && <hr className={`${borders} mb-3`} />}
{item.href && (
{item.href ? (
<Link href={item.href} target={item.target} className={componentClass}>
{asideMenuItemInnerContents}
</Link>
)}
{!item.href && (
<div className={componentClass} onClick={() => setIsDropdownActive(!isDropdownActive)}>
) : (
<button type='button' className={componentClass} onClick={() => setIsDropdownActive(!isDropdownActive)}>
{asideMenuItemInnerContents}
</div>
</button>
)}
{item.menu && (
<AsideMenuList
menu={item.menu}
className={`${asideMenuDropdownStyle} ${
isDropdownActive ? 'block dark:bg-slate-800/50' : 'hidden'
isDropdownActive
? 'mt-2 ml-3 block rounded-xl p-2'
: 'hidden'
}`}
isDropdownList
/>

View File

@ -1,15 +1,12 @@
import React from 'react'
import { mdiLogout, mdiClose } from '@mdi/js'
import Link from 'next/link'
import axios from 'axios'
import { mdiArrowTopRight, mdiClose, mdiOfficeBuildingOutline } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
import Link from 'next/link';
import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { getWorkspaceConfig, getWorkspaceRoute } from '../helpers/workspace'
type Props = {
menu: MenuAsideItem[]
@ -17,75 +14,102 @@ type Props = {
onAsideLgCloseClick: () => void
}
type OrganizationOption = {
id: string
name: string
}
export default function AsideMenuLayer({ menu, className = '', ...props }: Props) {
const corners = useAppSelector((state) => state.style.corners);
const corners = useAppSelector((state) => state.style.corners)
const asideStyle = useAppSelector((state) => state.style.asideStyle)
const asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle)
const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
const darkMode = useAppSelector((state) => state.style.darkMode)
const { currentUser } = useAppSelector((state) => state.auth)
const [organizations, setOrganizations] = React.useState<OrganizationOption[]>([])
const roleName = currentUser?.app_role?.name || ''
const workspaceConfig = getWorkspaceConfig(roleName)
const workspaceHref = getWorkspaceRoute(roleName)
const organizationId = currentUser?.organizations?.id
React.useEffect(() => {
let mounted = true
const fetchOrganizations = async () => {
try {
const response = await axios.get('/org-for-auth')
if (mounted) {
setOrganizations(Array.isArray(response.data) ? response.data : [])
}
} catch (error: any) {
console.error('Failed to load organizations for sidebar', error?.response || error)
}
}
fetchOrganizations()
return () => {
mounted = false
}
}, [])
const handleAsideLgCloseClick = (e: React.MouseEvent) => {
e.preventDefault()
props.onAsideLgCloseClick()
}
const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth);
const organizationsId = currentUser?.organizations?.id;
const [organizations, setOrganizations] = React.useState(null);
const organizationName = React.useMemo(() => {
const matchedOrganization = organizations.find((item) => item.id === organizationId)?.name
const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => {
try {
const response = await axios.get('/org-for-auth');
setOrganizations(response.data);
return response.data;
} catch (error) {
console.error(error.response);
throw error;
}
});
React.useEffect(() => {
dispatch(fetchOrganizations());
}, [dispatch]);
let organizationName = organizations?.find(item => item.id === organizationsId)?.name;
if(organizationName?.length > 25){
organizationName = organizationName?.substring(0, 25) + '...';
if (!matchedOrganization) {
return 'Organization workspace'
}
return matchedOrganization.length > 26 ? `${matchedOrganization.substring(0, 26)}...` : matchedOrganization
}, [organizationId, organizations])
return (
<aside
id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
className={`${className} fixed top-0 z-40 flex h-screen w-72 overflow-hidden transition-position lg:py-2 lg:pl-2`}
>
<div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
>
<div
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
>
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
<b className="font-black">FDSU ERP</b>
{organizationName && <p>{organizationName}</p>}
<div className={`flex flex-1 flex-col overflow-hidden border ${asideStyle} ${corners}`}>
<div className={`border-b px-4 py-4 ${asideBrandStyle}`}>
<div className='flex items-start justify-between gap-3'>
<div className='min-w-0 flex-1'>
<p className='text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-500 dark:text-slate-400'>FDSU ERP</p>
<h2 className='mt-2 line-clamp-2 text-lg font-semibold leading-6 text-slate-900 dark:text-white'>
{workspaceConfig.sidebarLabel}
</h2>
<div className='mt-3 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400'>
<BaseIcon path={mdiOfficeBuildingOutline} size={14} />
<span className='line-clamp-2 leading-5'>{organizationName}</span>
</div>
<button
className="hidden lg:inline-block xl:hidden p-3"
onClick={handleAsideLgCloseClick}
>
</div>
<button className='hidden rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 lg:inline-flex xl:hidden dark:hover:bg-slate-900 dark:hover:text-white' onClick={handleAsideLgCloseClick}>
<BaseIcon path={mdiClose} />
</button>
</div>
<div
className={`flex-1 overflow-y-auto overflow-x-hidden ${
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
}`}
>
<div className='mt-4 border-t border-slate-200 pt-3 dark:border-slate-800'>
<p className='truncate text-sm font-semibold text-slate-900 dark:text-white'>
{currentUser?.firstName || currentUser?.email || 'Authenticated user'}
</p>
<div className='mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500 dark:text-slate-400'>
<span>{roleName || 'Workspace access'}</span>
<span aria-hidden='true'></span>
<Link href={workspaceHref} className='inline-flex items-center gap-1 font-medium text-blue-700 transition hover:text-slate-950 dark:text-sky-400 dark:hover:text-white'>
Open workspace home
<BaseIcon path={mdiArrowTopRight} size={14} />
</Link>
</div>
</div>
</div>
<div className={`flex-1 overflow-y-auto overflow-x-hidden px-2 py-3 ${darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle}`}>
<AsideMenuList menu={menu} />
</div>
</div>

View File

@ -1,8 +1,9 @@
import React from 'react'
import { MenuAsideItem } from '../interfaces'
import AsideMenuItem from './AsideMenuItem'
import {useAppSelector} from "../stores/hooks";
import {hasPermission} from "../helpers/userPermissions";
import { useAppSelector } from '../stores/hooks';
import { hasPermission } from '../helpers/userPermissions';
import { getWorkspaceConfig, getWorkspaceRoute, itemVisibleForRole } from '../helpers/workspace';
type Props = {
menu: MenuAsideItem[]
@ -15,16 +16,29 @@ export default function AsideMenuList({ menu, isDropdownList = false, className
if (!currentUser) return null;
const roleName = currentUser?.app_role?.name;
const workspaceConfig = getWorkspaceConfig(roleName);
return (
<ul className={className}>
{menu.map((item, index) => {
if (!itemVisibleForRole(item.roles, roleName)) return null;
if (!hasPermission(currentUser, item.permissions)) return null;
const displayItem: MenuAsideItem = {
...item,
label: item.labelByRole?.[roleName] || item.label,
};
if (displayItem.label === 'Role Workspace') {
displayItem.label = workspaceConfig.sidebarLabel;
displayItem.href = getWorkspaceRoute(roleName);
}
return (
<div key={index}>
<AsideMenuItem
item={item}
item={displayItem}
isDropdownList={isDropdownList}
/>
</div>

View File

@ -0,0 +1,96 @@
import { mdiArrowLeft, mdiEyeOutline } from '@mdi/js';
import React from 'react';
import BaseButton from '../BaseButton';
import BaseButtons from '../BaseButtons';
import CardBox from '../CardBox';
import {
emptyValue,
formatPrimitiveValue,
getPrimaryTitle,
getRecordSubtitle,
getSummaryEntries,
humanizeLabel,
} from '../EntityPageUtils';
type Props = {
entityLabel: string;
pluralLabel: string;
listHref: string;
viewHref?: string;
record: any;
children: React.ReactNode;
};
const EnhancedEntityEditShell = ({ entityLabel, pluralLabel, listHref, viewHref, record, children }: Props) => {
const title = getPrimaryTitle(record, `Edit ${entityLabel}`);
const subtitle = getRecordSubtitle(record, `${entityLabel} record`);
const summaryEntries = getSummaryEntries(record, 7);
return (
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px] xl:items-start">
<div className="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm dark:border-dark-700 dark:bg-dark-900">
<div className="mb-6 rounded-xl border border-slate-200/80 bg-slate-50/80 p-5 dark:border-dark-700 dark:bg-dark-800/60">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400">Edit workspace</p>
<h3 className="mt-2 text-2xl font-semibold text-slate-900 dark:text-slate-100">{title}</h3>
<p className="mt-2 text-sm text-slate-600 dark:text-slate-300">{subtitle}. Update the fields below and keep the record details clear, complete, and ready for downstream users.</p>
</div>
<BaseButtons type="justify-start md:justify-end" className="w-full lg:w-auto" noWrap>
<BaseButton color="white" outline icon={mdiArrowLeft} label={`Back to ${pluralLabel.toLowerCase()}`} href={listHref} />
{viewHref ? <BaseButton color="white" outline icon={mdiEyeOutline} label={`View ${entityLabel.toLowerCase()}`} href={viewHref} /> : null}
</BaseButtons>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2 [&>div:not(:last-child)]:min-w-0 [&>div:not(:last-child)]:rounded-xl [&>div:not(:last-child)]:border [&>div:not(:last-child)]:border-slate-200/80 [&>div:not(:last-child)]:bg-slate-50/50 [&>div:not(:last-child)]:p-4 dark:[&>div:not(:last-child)]:border-dark-700 dark:[&>div:not(:last-child)]:bg-dark-800/40 [&>hr]:md:col-span-2 [&>div:last-child]:md:col-span-2">
{children}
</div>
</div>
<div className="space-y-6 xl:sticky xl:top-6">
<CardBox>
<div className="space-y-5">
<div className="flex items-center gap-3">
<div className="rounded-lg border border-slate-200 bg-slate-50 p-2 text-slate-500 dark:border-dark-700 dark:bg-dark-800 dark:text-slate-300">
<span className="inline-flex"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M4 19H20V21H4V19M17 7L15 5L17 3L19 5L17 7M6 17H4V15L14 5L16 7L6 17Z" /></svg></span>
</div>
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Editing summary</h3>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-300">Key values stay visible while the form is being updated.</p>
</div>
</div>
<div className="space-y-3">
{summaryEntries.length > 0 ? (
summaryEntries.map(([key, value]) => (
<div key={key} className="rounded-xl border border-slate-200/80 bg-slate-50/70 p-4 dark:border-dark-700 dark:bg-dark-800/60">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">{humanizeLabel(key)}</p>
<div className="mt-2 text-sm font-medium text-slate-900 dark:text-slate-100">{value instanceof Date || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? formatPrimitiveValue(key, value) : value || emptyValue}</div>
</div>
))
) : (
<div className="rounded-xl border border-dashed border-slate-200 bg-slate-50/70 px-4 py-6 text-sm text-slate-600 dark:border-dark-700 dark:bg-dark-800/50 dark:text-slate-300">
Summary fields will appear here as soon as this record has saved content.
</div>
)}
</div>
</div>
</CardBox>
<CardBox>
<div className="space-y-3">
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Editing guidance</h3>
<ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
<li> Keep naming and reference fields consistent with list and dashboard views.</li>
<li> Use notes and description fields to add decision-quality context, not placeholders.</li>
<li> Reset only if you want to discard unsaved changes in this form session.</li>
</ul>
</div>
</CardBox>
</div>
</div>
);
};
export default EnhancedEntityEditShell;

View File

@ -0,0 +1,282 @@
import { mdiArrowLeft, mdiChartTimelineVariant, mdiPencil } from '@mdi/js';
import Head from 'next/head';
import React, { useEffect } from 'react';
import BaseButton from '../BaseButton';
import BaseButtons from '../BaseButtons';
import BaseIcon from '../BaseIcon';
import CardBox from '../CardBox';
import SectionMain from '../SectionMain';
import SectionTitleLineWithButton from '../SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router';
import {
emptyValue,
formatPrimitiveValue,
getChipEntries,
getCollectionPreview,
getMetricCards,
getPrimaryTitle,
getRecordEntries,
getRecordSubtitle,
getRelationLabel,
getSummaryEntries,
humanizeLabel,
isRecordObject,
} from '../EntityPageUtils';
type Props = {
singularLabel: string;
pluralLabel: string;
stateKey: string;
recordKey: string;
fetchRecord: any;
listHref: string;
editHref: (id: string | string[] | undefined) => string;
};
const getBadgeClassName = (value?: string | null) => {
switch (String(value || '').toLowerCase()) {
case 'active':
case 'approved':
case 'completed':
case 'resolved':
case 'settled':
case 'paid':
case 'enabled':
case 'true':
return 'border-emerald-200 bg-emerald-50 text-emerald-700';
case 'submitted':
case 'under_review':
case 'in_progress':
case 'validated':
case 'partial':
case 'draft':
return 'border-blue-200 bg-blue-50 text-blue-700';
case 'on_hold':
case 'blocked':
case 'follow_up_required':
case 'disputed':
case 'pending':
return 'border-amber-200 bg-amber-50 text-amber-700';
case 'critical':
case 'high':
case 'terminated':
case 'cancelled':
case 'closed':
case 'disabled':
case 'false':
return 'border-red-200 bg-red-50 text-red-700';
default:
return 'border-slate-200 bg-slate-50 text-slate-700 dark:border-dark-700 dark:bg-dark-800 dark:text-slate-200';
}
};
const StatusBadge = ({ value }: { value: any }) => (
<span className={`rounded-full border px-2.5 py-1 text-xs font-semibold ${getBadgeClassName(value)}`}>{String(formatPrimitiveValue('status', value))}</span>
);
const DetailItem = ({ label, value }: { label: string; value: React.ReactNode }) => (
<div className="rounded-xl border border-slate-200/80 bg-slate-50/70 p-4 dark:border-dark-700 dark:bg-dark-800/60">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">{label}</p>
<div className="mt-2 text-sm leading-6 text-slate-900 dark:text-slate-100">{value || emptyValue}</div>
</div>
);
const MetricCard = ({ label, value, note }: { label: string; value: string; note: string }) => (
<div className="rounded-xl border border-slate-200/80 bg-slate-50/80 p-4 dark:border-dark-700 dark:bg-dark-800/60">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">{label}</p>
<p className="mt-3 text-2xl font-semibold text-slate-900 dark:text-slate-100">{value}</p>
<p className="mt-2 text-sm text-slate-600 dark:text-slate-300">{note}</p>
</div>
);
const EntityRecordViewPage = ({ singularLabel, pluralLabel, stateKey, recordKey, fetchRecord, listHref, editHref }: Props) => {
const router = useRouter();
const dispatch = useAppDispatch();
const { id } = router.query;
const sliceState = useAppSelector((state) => (state as any)[stateKey]);
const record = sliceState?.[recordKey];
useEffect(() => {
if (id) {
dispatch(fetchRecord({ id }));
}
}, [dispatch, fetchRecord, id]);
const recordTitle = getPrimaryTitle(record, `View ${singularLabel}`);
const recordSubtitle = getRecordSubtitle(record, `${singularLabel} details`);
const summaryEntries = getSummaryEntries(record, 8);
const metricCards = getMetricCards(record);
const chipEntries = getChipEntries(record);
const { scalarEntries, relationEntries, collectionEntries } = getRecordEntries(record);
const overviewEntries = summaryEntries;
const detailEntries = scalarEntries.filter(([key]) => !overviewEntries.some(([entryKey]) => entryKey === key));
return (
<>
<Head>
<title>{getPageTitle(recordTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={recordTitle} main>
<BaseButtons type="justify-start md:justify-end" className="w-full md:w-auto" noWrap>
<BaseButton color="white" outline icon={mdiArrowLeft} label={`Back to ${pluralLabel.toLowerCase()}`} onClick={() => router.push(listHref)} />
<BaseButton color="info" icon={mdiPencil} label={`Edit ${singularLabel.toLowerCase()}`} href={editHref(id)} />
</BaseButtons>
</SectionTitleLineWithButton>
<div className="space-y-6">
<CardBox>
<div className="space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="max-w-3xl">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400">{singularLabel} workspace</p>
<h2 className="mt-2 text-3xl font-semibold text-slate-900 dark:text-slate-100">{recordTitle}</h2>
<p className="mt-3 text-sm text-slate-600 dark:text-slate-300">{recordSubtitle}</p>
</div>
{chipEntries.length > 0 && (
<div className="flex flex-wrap gap-2">
{chipEntries.map(([key, value]) => (
<div key={key} className="flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 dark:border-dark-700 dark:bg-dark-900">
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">{humanizeLabel(key)}</span>
<StatusBadge value={value} />
</div>
))}
</div>
)}
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{metricCards.map((metric) => (
<MetricCard key={metric.label} label={metric.label} value={metric.value} note={metric.note} />
))}
</div>
</div>
</CardBox>
{!isRecordObject(record) ? (
<CardBox>
<div className="rounded-xl border border-dashed border-slate-200 bg-slate-50/70 px-4 py-8 text-sm text-slate-600 dark:border-dark-700 dark:bg-dark-800/50 dark:text-slate-300">
Loading {singularLabel.toLowerCase()} details or waiting for the record to become available.
</div>
</CardBox>
) : (
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
<div className="space-y-6">
<CardBox>
<div className="space-y-5">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Overview</h3>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-300">Key information is grouped here for faster review and cleaner scanning.</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{overviewEntries.map(([key, value]) => (
<DetailItem key={key} label={humanizeLabel(key)} value={formatPrimitiveValue(key, value)} />
))}
</div>
</div>
</CardBox>
{detailEntries.length > 0 && (
<CardBox>
<div className="space-y-5">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Extended details</h3>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-300">Additional record content is still available, but presented in a cleaner structured layout.</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{detailEntries.map(([key, value]) => (
<DetailItem key={key} label={humanizeLabel(key)} value={formatPrimitiveValue(key, value)} />
))}
</div>
</div>
</CardBox>
)}
{collectionEntries.map(([key, value]) => {
const items = Array.isArray(value) ? value : [];
const preview = getCollectionPreview(items);
return (
<CardBox key={key}>
<div className="space-y-5">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">{humanizeLabel(key)}</h3>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-300">Related records are presented as compact cards instead of a raw data dump.</p>
</div>
<span className="rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-slate-600 dark:border-dark-700 dark:bg-dark-800 dark:text-slate-300">
{items.length} records
</span>
</div>
{preview.length > 0 ? (
<div className="grid gap-4 md:grid-cols-2">
{preview.map((item) => (
<div key={item.key} className="rounded-xl border border-slate-200/80 bg-slate-50/70 p-4 dark:border-dark-700 dark:bg-dark-800/60">
<div className="flex items-start justify-between gap-3">
<p className="font-semibold text-slate-900 dark:text-slate-100">{item.title}</p>
<div className="rounded-lg border border-slate-200 bg-white p-2 text-slate-500 dark:border-dark-700 dark:bg-dark-900 dark:text-slate-300">
<BaseIcon path={mdiChartTimelineVariant} size={18} />
</div>
</div>
{item.details.length > 0 ? (
<dl className="mt-4 space-y-3">
{item.details.map((detail) => (
<div key={`${item.key}-${detail.label}`}>
<dt className="text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">{detail.label}</dt>
<dd className="mt-1 text-sm text-slate-700 dark:text-slate-200">{detail.value || emptyValue}</dd>
</div>
))}
</dl>
) : (
<p className="mt-4 text-sm text-slate-600 dark:text-slate-300">No additional preview fields are available for this related record.</p>
)}
</div>
))}
</div>
) : (
<div className="rounded-xl border border-dashed border-slate-200 bg-slate-50/70 px-4 py-6 text-sm text-slate-600 dark:border-dark-700 dark:bg-dark-800/50 dark:text-slate-300">
No related records have been attached here yet.
</div>
)}
</div>
</CardBox>
);
})}
</div>
<div className="space-y-6">
<CardBox>
<div className="space-y-5">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Connected records</h3>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-300">Direct linked records are summarized here so users can see context without parsing nested JSON-like sections.</p>
</div>
<div className="space-y-4">
{relationEntries.length > 0 ? (
relationEntries.map(([key, value]) => (
<div key={key} className="rounded-xl border border-slate-200/80 bg-slate-50/70 p-4 dark:border-dark-700 dark:bg-dark-800/60">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">{humanizeLabel(key)}</p>
<div className="mt-2 text-sm font-medium text-slate-900 dark:text-slate-100">{getRelationLabel(value)}</div>
</div>
))
) : (
<div className="rounded-xl border border-dashed border-slate-200 bg-slate-50/70 px-4 py-6 text-sm text-slate-600 dark:border-dark-700 dark:bg-dark-800/50 dark:text-slate-300">
No linked records are available on this item yet.
</div>
)}
</div>
</div>
</CardBox>
</div>
</div>
)}
</div>
</SectionMain>
</>
);
};
export default EntityRecordViewPage;

View File

@ -0,0 +1,317 @@
import React from 'react';
import dayjs from 'dayjs';
const EMPTY_VALUE = 'Not yet recorded';
const SYSTEM_FIELDS = new Set([
'id',
'createdAt',
'updatedAt',
'deletedAt',
'created_at',
'updated_at',
'deleted_at',
'password',
'passwordHash',
'salt',
]);
const PRIMARY_TITLE_FIELDS = [
'name',
'title',
'label',
'subject',
'project_name',
'program_name',
'contract_title',
'contract_number',
'code',
'project_code',
'email',
'firstName',
'lastName',
];
const SUMMARY_FIELD_PRIORITY = [
'name',
'title',
'label',
'code',
'project_code',
'contract_number',
'status',
'state',
'phase',
'priority',
'risk_level',
'type',
'category',
'organization',
'program',
'project',
'vendor',
'department',
'assigned_to',
'requested_by',
'requested_at',
'due_date',
'start_date',
'end_date',
'amount',
'budget_amount',
'contract_value',
'currency',
];
const CHIP_FIELDS = ['status', 'state', 'phase', 'priority', 'risk_level', 'type', 'category'];
const DATE_SUFFIXES = ['_at', '_date'];
const DATE_KEYWORDS = ['date', 'time', 'deadline'];
const AMOUNT_KEYWORDS = ['amount', 'value', 'cost', 'budget', 'total', 'price', 'percent'];
export const emptyValue = EMPTY_VALUE;
export const titleCase = (value?: string | null) => {
if (!value) {
return '';
}
return value
.replace(/_/g, ' ')
.replace(/\b\w/g, (match) => match.toUpperCase());
};
export const singularizeLabel = (value: string) => {
if (value.endsWith('ies')) {
return `${value.slice(0, -3)}y`;
}
if (value.endsWith('ches') || value.endsWith('shes') || value.endsWith('xes') || value.endsWith('zes')) {
return value.slice(0, -2);
}
if (value.endsWith('ses') && !value.endsWith('issues') && !value.endsWith('buses')) {
return value.slice(0, -1);
}
if (value.endsWith('s')) {
return value.slice(0, -1);
}
return value;
};
export const humanizeLabel = (key: string) => {
return titleCase(
key
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/_/g, ' ')
.replace(/\bid\b/i, 'ID')
.trim(),
);
};
export const isRecordObject = (value: any) => {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date);
};
export const isDateLikeKey = (key: string) => {
return DATE_SUFFIXES.some((suffix) => key.endsWith(suffix)) || DATE_KEYWORDS.some((part) => key.includes(part));
};
export const isAmountLikeKey = (key: string) => {
return AMOUNT_KEYWORDS.some((part) => key.includes(part));
};
export const stripHtml = (value: string) => value.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
export const formatPrimitiveValue = (key: string, value: any): React.ReactNode => {
if (value === null || value === undefined || value === '') {
return EMPTY_VALUE;
}
if (typeof value === 'boolean') {
return value ? 'Yes' : 'No';
}
if (typeof value === 'number') {
if (key.toLowerCase().includes('percent')) {
return `${value}%`;
}
return value.toLocaleString();
}
if (value instanceof Date || (typeof value === 'string' && isDateLikeKey(key) && dayjs(value).isValid())) {
return dayjs(value).format('DD MMM YYYY HH:mm');
}
if (typeof value === 'string') {
if (value.trim().startsWith('<') && value.includes('>')) {
const stripped = stripHtml(value);
return stripped || EMPTY_VALUE;
}
return value;
}
return String(value);
};
export const getRelationLabel = (value: any) => {
if (!isRecordObject(value)) {
return EMPTY_VALUE;
}
const candidates = ['name', 'title', 'label', 'code', 'project_code', 'contract_number', 'email', 'id'];
for (const candidate of candidates) {
if (value[candidate]) {
return formatPrimitiveValue(candidate, value[candidate]);
}
}
const visibleEntry = Object.entries(value).find(([entryKey, entryValue]) => !SYSTEM_FIELDS.has(entryKey) && !isRecordObject(entryValue) && !Array.isArray(entryValue));
return visibleEntry ? formatPrimitiveValue(visibleEntry[0], visibleEntry[1]) : EMPTY_VALUE;
};
export const getPrimaryTitle = (record: any, fallback: string) => {
if (!isRecordObject(record)) {
return fallback;
}
const firstName = record.firstName || record.first_name;
const lastName = record.lastName || record.last_name;
if (firstName || lastName) {
return [firstName, lastName].filter(Boolean).join(' ');
}
for (const field of PRIMARY_TITLE_FIELDS) {
if (record[field]) {
const value = formatPrimitiveValue(field, record[field]);
return typeof value === 'string' ? value : fallback;
}
}
return fallback;
};
export const getRecordSubtitle = (record: any, fallback: string) => {
if (!isRecordObject(record)) {
return fallback;
}
const referenceFields = ['code', 'project_code', 'contract_number', 'email', 'id'];
const reference = referenceFields.find((field) => record[field]);
if (!reference) {
return fallback;
}
return `${humanizeLabel(reference)}: ${formatPrimitiveValue(reference, record[reference])}`;
};
export const getRecordEntries = (record: any) => {
if (!isRecordObject(record)) {
return { scalarEntries: [], relationEntries: [], collectionEntries: [] };
}
const scalarEntries = Object.entries(record).filter(([key, value]) => {
return !SYSTEM_FIELDS.has(key) && !Array.isArray(value) && !isRecordObject(value);
});
const relationEntries = Object.entries(record).filter(([key, value]) => {
return !SYSTEM_FIELDS.has(key) && isRecordObject(value);
});
const collectionEntries = Object.entries(record).filter(([key, value]) => {
return !SYSTEM_FIELDS.has(key) && Array.isArray(value);
});
return { scalarEntries, relationEntries, collectionEntries };
};
export const sortEntries = (entries: Array<[string, any]>) => {
return [...entries].sort((left, right) => {
const leftIndex = SUMMARY_FIELD_PRIORITY.indexOf(left[0]);
const rightIndex = SUMMARY_FIELD_PRIORITY.indexOf(right[0]);
if (leftIndex !== -1 || rightIndex !== -1) {
return (leftIndex === -1 ? 999 : leftIndex) - (rightIndex === -1 ? 999 : rightIndex);
}
return humanizeLabel(left[0]).localeCompare(humanizeLabel(right[0]));
});
};
export const getSummaryEntries = (record: any, limit = 8) => {
const { scalarEntries, relationEntries } = getRecordEntries(record);
const scalar = sortEntries(scalarEntries);
const relation = sortEntries(relationEntries)
.filter(([key]) => !CHIP_FIELDS.includes(key))
.map(([key, value]) => [key, getRelationLabel(value)] as [string, React.ReactNode]);
return [...scalar, ...relation].slice(0, limit);
};
export const getChipEntries = (record: any) => {
if (!isRecordObject(record)) {
return [] as Array<[string, any]>;
}
return CHIP_FIELDS.filter((field) => record[field]).map((field) => [field, record[field]] as [string, any]);
};
export const getMetricCards = (record: any) => {
const { collectionEntries } = getRecordEntries(record);
const metrics = [
{
label: 'Linked sections',
value: collectionEntries.length.toString(),
note: collectionEntries.length ? 'Related collections are shown below.' : 'No related collections are attached yet.',
},
];
const importantEntries = [
['status', record?.status],
['start_date', record?.start_date],
['end_date', record?.end_date],
['budget_amount', record?.budget_amount ?? record?.amount ?? record?.contract_value],
].filter(([, value]) => value !== undefined && value !== null && value !== '');
for (const [key, value] of importantEntries.slice(0, 3)) {
metrics.push({
label: humanizeLabel(key),
value: String(formatPrimitiveValue(key, value)),
note: key === 'budget_amount' ? 'Tracked financial figure for this record.' : 'Key operating signal for this record.',
});
}
return metrics.slice(0, 4);
};
export const getCollectionPreview = (items: any[]) => {
return items.slice(0, 6).map((item, index) => {
if (!isRecordObject(item)) {
return {
key: String(index),
title: formatPrimitiveValue('value', item),
details: [],
};
}
const title = getPrimaryTitle(item, `Record ${index + 1}`);
const details = sortEntries(
Object.entries(item).filter(([key, value]) => !SYSTEM_FIELDS.has(key) && !Array.isArray(value) && !isRecordObject(value)),
)
.filter(([key]) => !PRIMARY_TITLE_FIELDS.includes(key))
.slice(0, 4)
.map(([key, value]) => ({
label: humanizeLabel(key),
value: formatPrimitiveValue(key, value),
}));
return {
key: String(item.id || index),
title,
details,
};
});
};

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'

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

@ -3,7 +3,7 @@ html {
}
body {
@apply pt-14 xl:pl-60 h-full;
@apply pt-14 xl:pl-72 h-full;
}
#app {

View File

@ -0,0 +1,820 @@
export const WORKSPACE_ROLES = {
superAdmin: 'Super Administrator',
administrator: 'Administrator',
directorGeneral: 'Director General',
financeDirector: 'Finance Director',
procurementLead: 'Procurement Lead',
complianceAuditLead: 'Compliance and Audit Lead',
projectDeliveryLead: 'Project Delivery Lead',
public: 'Public',
} as const;
export const WORKSPACE_ROUTES = {
[WORKSPACE_ROLES.superAdmin]: '/platform-command',
[WORKSPACE_ROLES.administrator]: '/operations-command',
[WORKSPACE_ROLES.directorGeneral]: '/executive-command',
[WORKSPACE_ROLES.financeDirector]: '/financial-control',
[WORKSPACE_ROLES.procurementLead]: '/procurement-desk',
[WORKSPACE_ROLES.complianceAuditLead]: '/compliance-desk',
[WORKSPACE_ROLES.projectDeliveryLead]: '/delivery-command',
} as const;
const ADMIN_DASHBOARD_ROLES = [WORKSPACE_ROLES.superAdmin, WORKSPACE_ROLES.administrator] as const;
export type WorkspaceMetricKey =
| 'approvedBudget'
| 'committedBudget'
| 'disbursedBudget'
| 'pendingApprovals'
| 'contractsNearingExpiry'
| 'vendorComplianceAlerts'
| 'procurementPipeline'
| 'openRiskAlerts'
| 'averageProjectProgress'
| 'highRiskProjects'
| 'overduePayments'
| 'activeProjects'
| 'unreadNotifications'
| 'organizationCount'
| 'platformUserCount'
| 'roleCount'
| 'permissionCount'
| 'auditEventCount';
export type WorkspaceDetailBlockKey =
| 'focus'
| 'summary'
| 'watchlist'
| 'approvalRisk'
| 'operations'
| 'delivery'
| 'actions';
export type WorkspaceSectionKey =
| 'approvalQueue'
| 'riskPanel'
| 'procurementQueue'
| 'contractWatchlist'
| 'recentNotifications'
| 'provinceRollout'
| 'topContracts'
| 'quickActions';
export type WorkspaceQuickLinkIconKey =
| 'organizations'
| 'users'
| 'approvals'
| 'notifications'
| 'projects'
| 'contracts'
| 'payments'
| 'allocations'
| 'requisitions'
| 'tenders'
| 'compliance'
| 'audit'
| 'milestones'
| 'vendors';
export interface WorkspaceAction {
href: string;
label: string;
}
export interface WorkspaceQuickLink {
href: string;
label: string;
description: string;
icon: WorkspaceQuickLinkIconKey;
}
export interface WorkspaceSectionCopy {
eyebrow: string;
title: string;
actionLabel?: string;
}
export interface WorkspaceBriefingCard {
title: string;
items: string[];
}
export interface WorkspaceConfig {
sidebarLabel: string;
pageTitle: string;
eyebrow: string;
heroTitle: string;
heroDescription: string;
primaryAction: WorkspaceAction;
secondaryAction: WorkspaceAction;
briefingCards: WorkspaceBriefingCard[];
highlightedMetricKeys: WorkspaceMetricKey[];
heroMetricKeys: WorkspaceMetricKey[];
blockOrder: WorkspaceDetailBlockKey[];
sectionCopy: Record<WorkspaceSectionKey, WorkspaceSectionCopy>;
quickLinks: WorkspaceQuickLink[];
}
type WorkspaceConfigInput = Omit<WorkspaceConfig, 'sectionCopy' | 'heroMetricKeys' | 'blockOrder' | 'quickLinks' | 'briefingCards'> & {
heroMetricKeys?: WorkspaceMetricKey[];
blockOrder?: WorkspaceDetailBlockKey[];
sectionCopy?: Partial<Record<WorkspaceSectionKey, WorkspaceSectionCopy>>;
quickLinks?: WorkspaceQuickLink[];
briefingCards?: WorkspaceBriefingCard[];
};
const defaultBlockOrder: WorkspaceDetailBlockKey[] = [
'focus',
'summary',
'watchlist',
'approvalRisk',
'operations',
'delivery',
'actions',
];
const defaultSectionCopy: Record<WorkspaceSectionKey, WorkspaceSectionCopy> = {
approvalQueue: {
eyebrow: 'Approval inbox',
title: 'Pending institutional decisions',
actionLabel: 'Open approvals',
},
riskPanel: {
eyebrow: 'Risk and red-flag panel',
title: 'Compliance, audit, and payment exposure',
},
procurementQueue: {
eyebrow: 'Procurement workflow',
title: 'Live procurement pipeline',
actionLabel: 'All requisitions',
},
contractWatchlist: {
eyebrow: 'Contract watchlist',
title: 'Expiring commitments',
actionLabel: 'Contract register',
},
recentNotifications: {
eyebrow: 'Notifications',
title: 'Recent operational signals',
actionLabel: 'Notification center',
},
provinceRollout: {
eyebrow: 'Geographic rollout',
title: 'Projects by province',
actionLabel: 'Projects register',
},
topContracts: {
eyebrow: 'Top-value contracts',
title: 'Largest commitments currently tracked',
actionLabel: 'Vendor master',
},
quickActions: {
eyebrow: 'Action launcher',
title: 'Move from oversight to execution',
},
};
const defaultQuickLinks: WorkspaceQuickLink[] = [
{
href: '/requisitions/requisitions-new',
label: 'Create requisition',
description: 'Start the procurement workflow.',
icon: 'requisitions',
},
{
href: '/approvals/approvals-list',
label: 'Approval inbox',
description: 'Review pending decisions and escalations.',
icon: 'approvals',
},
{
href: '/contracts/contracts-list',
label: 'Contract register',
description: 'Inspect value, milestones, and expiry status.',
icon: 'contracts',
},
{
href: '/audit_logs/audit_logs-list',
label: 'Audit center',
description: 'Trace actions, timestamps, and record history.',
icon: 'audit',
},
];
const createWorkspaceConfig = ({
heroMetricKeys,
blockOrder,
sectionCopy,
quickLinks,
briefingCards,
...config
}: WorkspaceConfigInput): WorkspaceConfig => ({
...config,
heroMetricKeys: heroMetricKeys || config.highlightedMetricKeys.slice(0, 4),
blockOrder: blockOrder || defaultBlockOrder,
sectionCopy: {
...defaultSectionCopy,
...sectionCopy,
},
quickLinks: quickLinks || defaultQuickLinks,
briefingCards: briefingCards || [],
});
const workspaceConfigs: Record<string, WorkspaceConfig> = {
[WORKSPACE_ROLES.superAdmin]: createWorkspaceConfig({
sidebarLabel: 'Platform Administration',
pageTitle: 'Platform Administration',
eyebrow: 'FDSU ERP · Platform Administration',
heroTitle: 'Govern tenants, access structure, and audit visibility across the platform.',
heroDescription:
'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: '/permissions/permissions-list', label: 'Review permissions' },
briefingCards: [
{
title: 'Should see',
items: ['Organizations using the platform', 'Platform users, roles, permissions, and access assignments', 'Audit activity and cross-tenant control exceptions'],
},
{
title: 'Should do',
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', 'Audit findings and platform support issues'],
},
{
title: 'Hands off to',
items: ['Administrators for organization execution', 'Functional leads for business workflows', 'Director General or tenant owners when decisions become operational'],
},
],
highlightedMetricKeys: ['organizationCount', 'platformUserCount', 'roleCount', 'permissionCount', 'auditEventCount', 'unreadNotifications'],
heroMetricKeys: ['organizationCount', 'platformUserCount', 'roleCount', 'auditEventCount'],
blockOrder: ['summary', 'focus', 'actions'],
sectionCopy: {
quickActions: {
eyebrow: 'Platform administration shortcuts',
title: 'Go directly to organizations, access models, permissions, audit logs, and notices',
},
},
quickLinks: [
{
href: '/organizations/organizations-list',
label: 'Organizations',
description: 'Review tenants, owners, and platform coverage.',
icon: 'organizations',
},
{
href: '/users/users-list',
label: 'Platform users',
description: 'Inspect who has access across the platform.',
icon: 'users',
},
{
href: '/roles/roles-list',
label: 'Roles',
description: 'Maintain responsibility boundaries and access structure.',
icon: 'users',
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
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',
},
],
}),
[WORKSPACE_ROLES.administrator]: createWorkspaceConfig({
sidebarLabel: 'Organization Administration',
pageTitle: 'Organization Administration',
eyebrow: 'FDSU ERP · Organization Administration',
heroTitle: 'Keep the organization ready to operate: users, routing, notices, and master data.',
heroDescription:
'This workspace is for the Administrator. It owns user setup, departments, provinces, approval routing, notifications, and day-to-day system readiness. It should not read like a finance, procurement, or project command center.',
primaryAction: { href: '/approval_workflows/approval_workflows-list', label: 'Review workflow setup' },
secondaryAction: { href: '/notifications/notifications-list', label: 'Review notifications' },
briefingCards: [
{
title: 'Should see',
items: ['Workflow readiness and routing issues', 'Users, departments, provinces, and notices', 'Configuration gaps slowing daily operations'],
},
{
title: 'Should do',
items: ['Onboard and support users', 'Maintain approval steps, delegations, and notifications', 'Keep administrative records clean and usable'],
},
{
title: 'Receives from',
items: ['Super Admin policy and access structure', 'Support requests from staff', 'Broken-routing or setup escalations from functional leads'],
},
{
title: 'Hands off to',
items: ['Operational staff for daily execution', 'Functional leads for domain decisions', 'Director General when administrative blockages affect the institution'],
},
],
highlightedMetricKeys: ['pendingApprovals', 'unreadNotifications', 'openRiskAlerts', 'vendorComplianceAlerts'],
heroMetricKeys: ['pendingApprovals', 'unreadNotifications', 'openRiskAlerts', 'vendorComplianceAlerts'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Workflow routing queue',
title: 'Approvals waiting because routing, delegation, or setup needs attention',
actionLabel: 'Open approval queue',
},
riskPanel: {
eyebrow: 'Administrative control signals',
title: 'Alerts and exceptions that often point to setup, delegation, or record issues',
},
recentNotifications: {
eyebrow: 'Administrator notices',
title: 'Support and system notices requiring administrator follow-through',
actionLabel: 'Open notices',
},
quickActions: {
eyebrow: 'Administration shortcuts',
title: 'Go straight to users, workflow setup, notifications, and organization records',
},
},
quickLinks: [
{
href: '/users/users-list',
label: 'Users',
description: 'Manage onboarding, account support, and assignments.',
icon: 'users',
},
{
href: '/approval_workflows/approval_workflows-list',
label: 'Approval workflows',
description: 'Maintain routes, steps, and approval readiness.',
icon: 'approvals',
},
{
href: '/notifications/notifications-list',
label: 'Notifications',
description: 'Review notices, workflow signals, and support follow-up.',
icon: 'notifications',
},
{
href: '/departments/departments-list',
label: 'Departments',
description: 'Keep organization structure and master data aligned.',
icon: 'users',
},
],
}),
[WORKSPACE_ROLES.directorGeneral]: createWorkspaceConfig({
sidebarLabel: 'Executive Oversight',
pageTitle: 'Executive Oversight',
eyebrow: 'FDSU ERP · Executive Oversight',
heroTitle: 'See the institution across finance, procurement, compliance, and delivery, then make executive decisions.',
heroDescription:
'This workspace is for the Director General. It brings together budget posture, delivery progress, risk concentration, and high-impact approvals so executive attention stays on direction, escalation, and accountability.',
primaryAction: { href: '/approvals/approvals-list', label: 'Review escalations' },
secondaryAction: { href: '/projects/projects-list', label: 'Review project portfolio' },
briefingCards: [
{
title: 'Should see',
items: ['Strategic budget posture', 'Delivery momentum and risk concentration', 'Major approvals and contracts needing executive direction'],
},
{
title: 'Should do',
items: ['Approve high-impact decisions', 'Set priorities across functions', 'Resolve escalations that cross departmental boundaries'],
},
{
title: 'Receives from',
items: ['Finance, procurement, compliance, and delivery leads', 'Administrator escalations with institution-wide impact'],
},
{
title: 'Hands off to',
items: ['Functional leads for execution', 'Administrator for coordination follow-through', 'Operational teams through approved decisions'],
},
],
highlightedMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects', 'contractsNearingExpiry'],
heroMetricKeys: ['approvedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects'],
blockOrder: ['summary', 'focus', 'watchlist', 'delivery', 'approvalRisk', 'actions'],
sectionCopy: {
riskPanel: {
eyebrow: 'Executive risk watch',
title: 'Strategic risks, bottlenecks, and control exposure needing executive attention',
},
provinceRollout: {
eyebrow: 'Delivery coverage',
title: 'Project rollout and execution pace across provinces',
actionLabel: 'Open projects',
},
topContracts: {
eyebrow: 'Major commitments',
title: 'Largest contracts shaping institutional exposure and delivery',
actionLabel: 'Open contracts',
},
quickActions: {
eyebrow: 'Executive shortcuts',
title: 'Open the records most relevant to executive review and escalation',
},
},
quickLinks: [
{
href: '/approvals/approvals-list',
label: 'Escalations',
description: 'Review approvals and issues that need executive decision.',
icon: 'approvals',
},
{
href: '/projects/projects-list',
label: 'Projects',
description: 'Review delivery status and accountability.',
icon: 'projects',
},
{
href: '/contracts/contracts-list',
label: 'Contracts',
description: 'Inspect major commitments and expiry exposure.',
icon: 'contracts',
},
{
href: '/notifications/notifications-list',
label: 'Executive notices',
description: 'See the latest escalations and control signals.',
icon: 'notifications',
},
],
}),
[WORKSPACE_ROLES.financeDirector]: createWorkspaceConfig({
sidebarLabel: 'Financial Management',
pageTitle: 'Financial Management',
eyebrow: 'FDSU ERP · Financial Management',
heroTitle: 'Control budget, commitments, disbursements, and payment pressure.',
heroDescription:
'This workspace is for the Finance Director. It focuses on allocations, commitments, disbursements, aging payment requests, and finance approvals while staying tied to the contracts and spending decisions that drive exposure.',
primaryAction: { href: '/payment_requests/payment_requests-list', label: 'Review payment requests' },
secondaryAction: { href: '/allocations/allocations-list', label: 'Review allocations' },
briefingCards: [
{
title: 'Should see',
items: ['Approved versus committed budget', 'Payment backlog and disbursement risk', 'Allocation pressure and finance approvals'],
},
{
title: 'Should do',
items: ['Validate funding coverage', 'Clear finance approvals and payment bottlenecks', 'Escalate shortfalls and fiscal-control issues'],
},
{
title: 'Receives from',
items: ['Operational finance staff', 'Procurement commitments and award decisions', 'Administrator-routed approvals'],
},
{
title: 'Hands off to',
items: ['Director General for major funding decisions', 'Payment and ledger staff for execution', 'Procurement and delivery leads on affordability constraints'],
},
],
highlightedMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments', 'pendingApprovals', 'unreadNotifications'],
heroMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Finance approval queue',
title: 'Approvals affecting commitments, disbursements, and payment timing',
actionLabel: 'Open finance approvals',
},
riskPanel: {
eyebrow: 'Fiscal exposure',
title: 'Budget pressure, aging payments, and control exceptions affecting finance',
},
quickActions: {
eyebrow: 'Finance shortcuts',
title: 'Go directly to allocations, payment requests, commitments, and approvals',
},
},
quickLinks: [
{
href: '/payment_requests/payment_requests-list',
label: 'Payment requests',
description: 'Review pending and aging disbursement requests.',
icon: 'payments',
},
{
href: '/allocations/allocations-list',
label: 'Allocations',
description: 'Check available headroom and funding coverage.',
icon: 'allocations',
},
{
href: '/contracts/contracts-list',
label: 'Commitments',
description: 'Inspect the contracts driving current spend exposure.',
icon: 'contracts',
},
{
href: '/approvals/approvals-list',
label: 'Finance approvals',
description: 'Track bottlenecks holding back finance action.',
icon: 'approvals',
},
],
}),
[WORKSPACE_ROLES.procurementLead]: createWorkspaceConfig({
sidebarLabel: 'Procurement Management',
pageTitle: 'Procurement Management',
eyebrow: 'FDSU ERP · Procurement Management',
heroTitle: 'Own the demand-to-award chain: requisitions, tenders, vendors, awards, and contract readiness.',
heroDescription:
'This workspace is for the Procurement Lead. It keeps sourcing leadership focused on requisition inflow, tender progression, vendor readiness, contract deadlines, and approvals that are slowing the procurement chain.',
primaryAction: { href: '/requisitions/requisitions-list', label: 'Review requisitions' },
secondaryAction: { href: '/tenders/tenders-list', label: 'Review tenders' },
briefingCards: [
{
title: 'Should see',
items: ['Requisition inflow and tender progress', 'Expiring contracts and supplier readiness', 'Approvals slowing sourcing and award decisions'],
},
{
title: 'Should do',
items: ['Move demand into sourcing', 'Review awards and vendor readiness', 'Escalate blockers in the procurement chain'],
},
{
title: 'Receives from',
items: ['Operational requesters and departments', 'Project teams needing sourcing support', 'Administrator workflow routing and setup support'],
},
{
title: 'Hands off to',
items: ['Finance on commitments and funding coverage', 'Compliance on control concerns', 'Director General on high-value or escalated decisions'],
},
],
highlightedMetricKeys: ['procurementPipeline', 'pendingApprovals', 'contractsNearingExpiry', 'vendorComplianceAlerts', 'openRiskAlerts', 'activeProjects'],
heroMetricKeys: ['procurementPipeline', 'pendingApprovals', 'contractsNearingExpiry', 'vendorComplianceAlerts'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'operations', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Procurement approval queue',
title: 'Decisions blocking sourcing, evaluation, award, or contract signature',
actionLabel: 'Open approval backlog',
},
procurementQueue: {
eyebrow: 'Sourcing pipeline',
title: 'Requisitions and tenders currently needing procurement action',
actionLabel: 'Open sourcing queue',
},
contractWatchlist: {
eyebrow: 'Contract renewals',
title: 'Supplier commitments nearing expiry or needing follow-through',
actionLabel: 'Open contracts',
},
quickActions: {
eyebrow: 'Procurement shortcuts',
title: 'Go directly to requisitions, tenders, vendors, and contract follow-up',
},
},
quickLinks: [
{
href: '/requisitions/requisitions-list',
label: 'Requisitions',
description: 'Review new demand and sourcing readiness.',
icon: 'requisitions',
},
{
href: '/tenders/tenders-list',
label: 'Tenders',
description: 'Follow active tenders and consultations.',
icon: 'tenders',
},
{
href: '/contracts/contracts-list',
label: 'Contracts',
description: 'Track expiring commitments and follow-up actions.',
icon: 'contracts',
},
{
href: '/vendors/vendors-list',
label: 'Vendors',
description: 'Inspect supplier readiness and history.',
icon: 'vendors',
},
],
}),
[WORKSPACE_ROLES.complianceAuditLead]: createWorkspaceConfig({
sidebarLabel: 'Compliance & Audit',
pageTitle: 'Compliance & Audit',
eyebrow: 'FDSU ERP · Compliance & Audit',
heroTitle: 'Watch control breaches, evidence gaps, expiring obligations, and approvals that need independent review.',
heroDescription:
'This workspace is for the Compliance and Audit Lead. It focuses on red flags, evidence quality, audit trails, expiring obligations, and approvals where controls must be checked before risk becomes failure.',
primaryAction: { href: '/compliance_alerts/compliance_alerts-list', label: 'Review compliance alerts' },
secondaryAction: { href: '/audit_logs/audit_logs-list', label: 'Review audit logs' },
briefingCards: [
{
title: 'Should see',
items: ['Compliance alerts and evidence gaps', 'Expiring obligations and aging items', 'Approvals requiring control review'],
},
{
title: 'Should do',
items: ['Investigate red flags', 'Trace actions in audit logs', 'Push corrective action and evidence follow-up'],
},
{
title: 'Receives from',
items: ['System alerts and audit trails', 'Functional leads reporting control exceptions', 'Administrator notices and escalations'],
},
{
title: 'Hands off to',
items: ['Director General on serious exposure', 'Process owners for remediation', 'Staff for evidence completion and follow-up'],
},
],
highlightedMetricKeys: ['openRiskAlerts', 'vendorComplianceAlerts', 'contractsNearingExpiry', 'pendingApprovals', 'overduePayments', 'unreadNotifications'],
heroMetricKeys: ['openRiskAlerts', 'vendorComplianceAlerts', 'contractsNearingExpiry', 'pendingApprovals'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Control review queue',
title: 'Approvals and cases requiring audit or compliance follow-through',
actionLabel: 'Open control queue',
},
riskPanel: {
eyebrow: 'Compliance watch',
title: 'Exceptions, evidence gaps, overdue items, and red flags needing review',
},
recentNotifications: {
eyebrow: 'Control notices',
title: 'Recent signals that may indicate policy, evidence, or routing problems',
actionLabel: 'Open notices',
},
quickActions: {
eyebrow: 'Compliance shortcuts',
title: 'Go directly to alerts, logs, approvals, and obligations needing review',
},
},
quickLinks: [
{
href: '/compliance_alerts/compliance_alerts-list',
label: 'Compliance alerts',
description: 'Review open red flags and follow-up status.',
icon: 'compliance',
},
{
href: '/audit_logs/audit_logs-list',
label: 'Audit logs',
description: 'Trace evidence across users and records.',
icon: 'audit',
},
{
href: '/approvals/approvals-list',
label: 'Control queue',
description: 'Inspect approvals needing governance review.',
icon: 'approvals',
},
{
href: '/contracts/contracts-list',
label: 'Obligations',
description: 'Check commitments nearing expiry or breach.',
icon: 'contracts',
},
],
}),
[WORKSPACE_ROLES.projectDeliveryLead]: createWorkspaceConfig({
sidebarLabel: 'Project Delivery',
pageTitle: 'Project Delivery',
eyebrow: 'FDSU ERP · Project Delivery',
heroTitle: 'Keep projects moving by watching milestones, execution risk, approvals, and payment delays.',
heroDescription:
'This workspace is for the Project Delivery Lead. It centers on implementation progress, field issues, delivery blockers, and the finance or procurement dependencies that can delay execution.',
primaryAction: { href: '/projects/projects-list', label: 'Review projects' },
secondaryAction: { href: '/project_milestones/project_milestones-list', label: 'Review milestones' },
briefingCards: [
{
title: 'Should see',
items: ['Project status and milestone slippage', 'Field issues and execution risks', 'Approvals or payments delaying delivery'],
},
{
title: 'Should do',
items: ['Review project progress', 'Clear delivery blockers with other leads', 'Escalate risks before timelines fail'],
},
{
title: 'Receives from',
items: ['Project staff and field verification teams', 'Procurement and finance status updates', 'Administrator workflow escalations'],
},
{
title: 'Hands off to',
items: ['Director General for major decisions', 'Procurement and finance leads on blockers', 'Operational project teams for execution'],
},
],
highlightedMetricKeys: ['activeProjects', 'averageProjectProgress', 'highRiskProjects', 'pendingApprovals', 'overduePayments', 'unreadNotifications'],
heroMetricKeys: ['activeProjects', 'averageProjectProgress', 'highRiskProjects', 'pendingApprovals'],
blockOrder: ['summary', 'focus', 'watchlist', 'delivery', 'approvalRisk', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Delivery blocker queue',
title: 'Approvals holding back project execution',
actionLabel: 'Open delivery blockers',
},
provinceRollout: {
eyebrow: 'Delivery footprint',
title: 'Project progress and execution pace by province',
actionLabel: 'Open project register',
},
topContracts: {
eyebrow: 'Delivery contracts',
title: 'Largest contracts underpinning execution and milestone delivery',
actionLabel: 'Open contracts',
},
quickActions: {
eyebrow: 'Delivery shortcuts',
title: 'Go directly to projects, milestones, blockers, and supporting contracts',
},
},
quickLinks: [
{
href: '/projects/projects-list',
label: 'Projects',
description: 'Review implementation status and ownership.',
icon: 'projects',
},
{
href: '/project_milestones/project_milestones-list',
label: 'Milestones',
description: 'Track critical delivery checkpoints.',
icon: 'milestones',
},
{
href: '/approvals/approvals-list',
label: 'Delivery blockers',
description: 'See approvals slowing field execution.',
icon: 'approvals',
},
{
href: '/contracts/contracts-list',
label: 'Supporting contracts',
description: 'Inspect contracts affecting delivery scope.',
icon: 'contracts',
},
],
}),
};
export function getWorkspaceConfig(roleName?: string | null): WorkspaceConfig {
if (!roleName) {
return workspaceConfigs[WORKSPACE_ROLES.projectDeliveryLead];
}
return workspaceConfigs[roleName] || workspaceConfigs[WORKSPACE_ROLES.projectDeliveryLead];
}
export function getWorkspaceRoute(roleName?: string | null) {
if (!roleName) {
return '/executive-summary';
}
return WORKSPACE_ROUTES[roleName] || '/executive-summary';
}
export function normalizeInternalRedirectTarget(target?: string | string[] | null) {
const redirectTarget = Array.isArray(target) ? target[0] : target;
if (!redirectTarget || typeof redirectTarget !== 'string') {
return null;
}
if (!redirectTarget.startsWith('/') || redirectTarget.startsWith('//')) {
return null;
}
if (redirectTarget.startsWith('/login')) {
return null;
}
return redirectTarget;
}
export function getPostLoginRoute(roleName?: string | null, redirectTarget?: string | string[] | null) {
return normalizeInternalRedirectTarget(redirectTarget) || getWorkspaceRoute(roleName);
}
export function getLoginRoute(redirectTarget?: string | string[] | null) {
const safeRedirectTarget = normalizeInternalRedirectTarget(redirectTarget);
if (!safeRedirectTarget) {
return '/login';
}
return `/login?redirect=${encodeURIComponent(safeRedirectTarget)}`;
}
export function isDashboardRole(roleName?: string | null) {
if (!roleName) {
return false;
}
return ADMIN_DASHBOARD_ROLES.includes(roleName as (typeof ADMIN_DASHBOARD_ROLES)[number]);
}
export function itemVisibleForRole(itemRoles?: string[], roleName?: string | null) {
if (!itemRoles?.length) {
return true;
}
if (!roleName) {
return false;
}
return itemRoles.includes(roleName);
}

View File

@ -14,6 +14,8 @@ export type MenuAsideItem = {
withDevider?: boolean;
menu?: MenuAsideItem[]
permissions?: string | string[]
roles?: string[]
labelByRole?: Record<string, string>
}
export type MenuNavBarItem = {

View File

@ -1,7 +1,7 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken'
import { mdiBackburger, mdiForwardburger, mdiMenu } from '@mdi/js'
import { useRouter } from 'next/router'
import menuAside from '../menuAside'
import menuNavBar from '../menuNavBar'
import BaseIcon from '../components/BaseIcon'
@ -9,67 +9,61 @@ import NavBar from '../components/NavBar'
import NavBarItemPlain from '../components/NavBarItemPlain'
import AsideMenu from '../components/AsideMenu'
import FooterBar from '../components/FooterBar'
import Search from '../components/Search'
import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Search from '../components/Search';
import { useRouter } from 'next/router'
import {findMe, logoutUser} from "../stores/authSlice";
import {hasPermission} from "../helpers/userPermissions";
import { findMe, logoutUser } from '../stores/authSlice'
import { hasPermission } from '../helpers/userPermissions'
import { getLoginRoute } from '../helpers/workspace'
type Props = {
children: ReactNode
permission?: string
}
export default function LayoutAuthenticated({
children,
permission
}: Props) {
export default function LayoutAuthenticated({ children, permission }: Props) {
const dispatch = useAppDispatch()
const router = useRouter()
const { token, currentUser } = useAppSelector((state) => state.auth)
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
let localToken
if (typeof window !== 'undefined') {
// Perform localStorage action
localToken = localStorage.getItem('token')
}
const isTokenValid = () => {
const token = localStorage.getItem('token');
if (!token) return;
const date = new Date().getTime() / 1000;
const data = jwt.decode(token);
if (!data) return;
return date < data.exp;
};
useEffect(() => {
dispatch(findMe());
if (!isTokenValid()) {
dispatch(logoutUser());
router.push('/login');
}
}, [token, localToken]);
useEffect(() => {
if (!permission || !currentUser) return;
if (!hasPermission(currentUser, permission)) router.push('/error');
}, [currentUser, permission]);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor)
const darkMode = useAppSelector((state) => state.style.darkMode)
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
let localToken
if (typeof window !== 'undefined') {
localToken = localStorage.getItem('token')
}
const isTokenValid = () => {
const storedToken = localStorage.getItem('token')
if (!storedToken) return
const date = new Date().getTime() / 1000
const data: any = jwt.decode(storedToken)
if (!data) return
return date < data.exp
}
useEffect(() => {
if (!isTokenValid()) {
dispatch(logoutUser())
router.replace(getLoginRoute(router.asPath))
return
}
dispatch(findMe())
}, [dispatch, localToken, router, token])
useEffect(() => {
if (!permission || !currentUser) return
if (!hasPermission(currentUser, permission)) router.push('/error')
}, [currentUser, permission, router])
useEffect(() => {
const handleRouteChangeStart = () => {
setIsAsideMobileExpanded(false)
@ -78,51 +72,51 @@ export default function LayoutAuthenticated({
router.events.on('routeChangeStart', handleRouteChangeStart)
// If the component is unmounted, unsubscribe
// from the event with the `off` method:
return () => {
router.events.off('routeChangeStart', handleRouteChangeStart)
}
}, [router.events, dispatch])
}, [router.events])
const layoutAsidePadding = 'xl:pl-60'
const layoutAsidePadding = 'xl:pl-72'
return (
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
<div
className={`${layoutAsidePadding} ${
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
} min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
>
<NavBar
menu={menuNavBar}
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''}`}
>
<NavBarItemPlain
display="flex lg:hidden"
display='flex lg:hidden'
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
>
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size='24' />
</NavBarItemPlain>
<NavBarItemPlain
display="hidden lg:flex xl:hidden"
onClick={() => setIsAsideLgActive(true)}
>
<BaseIcon path={mdiMenu} size="24" />
<NavBarItemPlain display='hidden lg:flex xl:hidden' onClick={() => setIsAsideLgActive(true)}>
<BaseIcon path={mdiMenu} size='24' />
</NavBarItemPlain>
<NavBarItemPlain useMargin>
<Search />
</NavBarItemPlain>
</NavBar>
<AsideMenu
isAsideMobileExpanded={isAsideMobileExpanded}
isAsideLgActive={isAsideLgActive}
menu={menuAside}
onAsideLgClose={() => setIsAsideLgActive(false)}
/>
<div className='pt-14'>
<div className='min-h-[calc(100vh-3.5rem)]'>
{children}
<FooterBar>Hand-crafted & Made with </FooterBar>
</div>
<FooterBar />
</div>
</div>
</div>
)

View File

@ -1,418 +1,295 @@
import * as icon from '@mdi/js';
import * as icon from '@mdi/js'
import { MenuAsideItem } from './interfaces'
import { WORKSPACE_ROLES } from './helpers/workspace'
const menuAside: MenuAsideItem[] = [
const optionalIcon = (name: string, fallback: string = icon.mdiTable): string => {
const iconSet = icon as Record<string, string>
return iconSet[name] || fallback
}
const superAdminWorkspaceRoles = [WORKSPACE_ROLES.superAdmin]
const adminWorkspaceRoles = [WORKSPACE_ROLES.administrator]
const superAdminGroupedNavigation: MenuAsideItem[] = [
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
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',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable,
permissions: 'READ_USERS'
icon: icon.mdiAccountGroup,
permissions: 'READ_USERS',
},
{
href: '/roles/roles-list',
label: 'Roles',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
permissions: 'READ_ROLES'
icon: optionalIcon('mdiShieldAccountVariantOutline'),
permissions: 'READ_ROLES',
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS'
},
{
href: '/organizations/organizations-list',
label: 'Organizations',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_ORGANIZATIONS'
},
{
href: '/provinces/provinces-list',
label: 'Provinces',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PROVINCES'
},
{
href: '/departments/departments-list',
label: 'Departments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiOfficeBuilding' in icon ? icon['mdiOfficeBuilding' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_DEPARTMENTS'
icon: optionalIcon('mdiShieldAccountOutline'),
permissions: 'READ_PERMISSIONS',
},
{
href: '/role_permissions/role_permissions-list',
label: 'Role permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_ROLE_PERMISSIONS'
icon: optionalIcon('mdiLinkVariant'),
permissions: 'READ_ROLE_PERMISSIONS',
},
],
},
{
label: 'Workflow & Oversight',
icon: icon.mdiSitemap,
roles: superAdminWorkspaceRoles,
menu: [
{
href: '/approval_workflows/approval_workflows-list',
label: 'Approval workflows',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiSitemap' in icon ? icon['mdiSitemap' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_APPROVAL_WORKFLOWS'
icon: optionalIcon('mdiSitemap'),
permissions: 'READ_APPROVAL_WORKFLOWS',
},
{
href: '/approval_steps/approval_steps-list',
label: 'Approval steps',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiStairs' in icon ? icon['mdiStairs' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_APPROVAL_STEPS'
},
{
href: '/approvals/approvals-list',
label: 'Approvals',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiChecklist' in icon ? icon['mdiChecklist' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_APPROVALS'
icon: optionalIcon('mdiStairs'),
permissions: 'READ_APPROVAL_STEPS',
},
{
href: '/notifications/notifications-list',
label: 'Notifications',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiBell' in icon ? icon['mdiBell' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_NOTIFICATIONS'
icon: optionalIcon('mdiBell'),
permissions: 'READ_NOTIFICATIONS',
},
{
href: '/audit_logs/audit_logs-list',
label: 'Audit logs',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiClipboardTextClock' in icon ? icon['mdiClipboardTextClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_AUDIT_LOGS'
icon: optionalIcon('mdiClipboardTextClock'),
permissions: 'READ_AUDIT_LOGS',
},
],
},
]
const adminGroupedNavigation: MenuAsideItem[] = [
{
label: 'Organization Setup',
icon: icon.mdiAccountGroup,
roles: adminWorkspaceRoles,
withDevider: true,
menu: [
{
href: '/users/users-list',
label: 'Users',
icon: icon.mdiAccountGroup,
permissions: 'READ_USERS',
},
{
href: '/fiscal_years/fiscal_years-list',
label: 'Fiscal years',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCalendarRange' in icon ? icon['mdiCalendarRange' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_FISCAL_YEARS'
href: '/provinces/provinces-list',
label: 'Provinces',
icon: optionalIcon('mdiMapMarker'),
permissions: 'READ_PROVINCES',
},
{
href: '/funding_sources/funding_sources-list',
label: 'Funding sources',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_FUNDING_SOURCES'
},
{
href: '/budget_programs/budget_programs-list',
label: 'Budget programs',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiChartDonut' in icon ? icon['mdiChartDonut' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_BUDGET_PROGRAMS'
},
{
href: '/budget_lines/budget_lines-list',
label: 'Budget lines',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_BUDGET_LINES'
},
{
href: '/allocations/allocations-list',
label: 'Allocations',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiDatabaseArrowRight' in icon ? icon['mdiDatabaseArrowRight' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_ALLOCATIONS'
},
{
href: '/budget_reallocations/budget_reallocations-list',
label: 'Budget reallocations',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_BUDGET_REALLOCATIONS'
},
{
href: '/procurement_plans/procurement_plans-list',
label: 'Procurement plans',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiClipboardList' in icon ? icon['mdiClipboardList' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PROCUREMENT_PLANS'
},
{
href: '/requisitions/requisitions-list',
label: 'Requisitions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFileDocumentEdit' in icon ? icon['mdiFileDocumentEdit' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_REQUISITIONS'
},
{
href: '/tenders/tenders-list',
label: 'Tenders',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiGavel' in icon ? icon['mdiGavel' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_TENDERS'
},
{
href: '/vendors/vendors-list',
label: 'Vendors',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiTruckFast' in icon ? icon['mdiTruckFast' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_VENDORS'
},
{
href: '/vendor_compliance_documents/vendor_compliance_documents-list',
label: 'Vendor compliance documents',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFileCertificate' in icon ? icon['mdiFileCertificate' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_VENDOR_COMPLIANCE_DOCUMENTS'
},
{
href: '/bids/bids-list',
label: 'Bids',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFileSign' in icon ? icon['mdiFileSign' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_BIDS'
},
{
href: '/bid_evaluations/bid_evaluations-list',
label: 'Bid evaluations',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiClipboardCheck' in icon ? icon['mdiClipboardCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_BID_EVALUATIONS'
},
{
href: '/awards/awards-list',
label: 'Awards',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiTrophyAward' in icon ? icon['mdiTrophyAward' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_AWARDS'
},
{
href: '/programs/programs-list',
label: 'Programs',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiViewGridOutline' in icon ? icon['mdiViewGridOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PROGRAMS'
},
{
href: '/projects/projects-list',
label: 'Projects',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiBriefcaseCheck' in icon ? icon['mdiBriefcaseCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PROJECTS'
},
{
href: '/project_milestones/project_milestones-list',
label: 'Project milestones',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFlagCheckered' in icon ? icon['mdiFlagCheckered' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PROJECT_MILESTONES'
},
{
href: '/risks/risks-list',
label: 'Risks',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiAlertOctagon' in icon ? icon['mdiAlertOctagon' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_RISKS'
},
{
href: '/issues/issues-list',
label: 'Issues',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiBug' in icon ? icon['mdiBug' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_ISSUES'
},
{
href: '/field_verifications/field_verifications-list',
label: 'Field verifications',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiMapMarkerCheck' in icon ? icon['mdiMapMarkerCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_FIELD_VERIFICATIONS'
},
{
href: '/contracts/contracts-list',
label: 'Contracts',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFileDocumentOutline' in icon ? icon['mdiFileDocumentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_CONTRACTS'
},
{
href: '/contract_amendments/contract_amendments-list',
label: 'Contract amendments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFileReplaceOutline' in icon ? icon['mdiFileReplaceOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_CONTRACT_AMENDMENTS'
},
{
href: '/contract_milestones/contract_milestones-list',
label: 'Contract milestones',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiTimelineCheck' in icon ? icon['mdiTimelineCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_CONTRACT_MILESTONES'
},
{
href: '/grants/grants-list',
label: 'Grants',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiHandCoin' in icon ? icon['mdiHandCoin' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_GRANTS'
},
{
href: '/beneficiaries/beneficiaries-list',
label: 'Beneficiaries',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_BENEFICIARIES'
},
{
href: '/grant_applications/grant_applications-list',
label: 'Grant applications',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFileDocumentMultiple' in icon ? icon['mdiFileDocumentMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_GRANT_APPLICATIONS'
},
{
href: '/grant_evaluations/grant_evaluations-list',
label: 'Grant evaluations',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiStarCheck' in icon ? icon['mdiStarCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_GRANT_EVALUATIONS'
},
{
href: '/grant_tranches/grant_tranches-list',
label: 'Grant tranches',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCashCheck' in icon ? icon['mdiCashCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_GRANT_TRANCHES'
},
{
href: '/expense_categories/expense_categories-list',
label: 'Expense categories',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiTagMultiple' in icon ? icon['mdiTagMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_EXPENSE_CATEGORIES'
},
{
href: '/invoices/invoices-list',
label: 'Invoices',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiReceiptText' in icon ? icon['mdiReceiptText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_INVOICES'
},
{
href: '/payment_requests/payment_requests-list',
label: 'Payment requests',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCashFast' in icon ? icon['mdiCashFast' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PAYMENT_REQUESTS'
},
{
href: '/payment_batches/payment_batches-list',
label: 'Payment batches',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiPackageVariantClosed' in icon ? icon['mdiPackageVariantClosed' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PAYMENT_BATCHES'
},
{
href: '/payments/payments-list',
label: 'Payments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiBankTransfer' in icon ? icon['mdiBankTransfer' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PAYMENTS'
},
{
href: '/obligations/obligations-list',
label: 'Obligations',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiBookArrowDown' in icon ? icon['mdiBookArrowDown' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_OBLIGATIONS'
},
{
href: '/ledger_entries/ledger_entries-list',
label: 'Ledger entries',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiBookOpenPageVariant' in icon ? icon['mdiBookOpenPageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_LEDGER_ENTRIES'
href: '/departments/departments-list',
label: 'Departments',
icon: optionalIcon('mdiOfficeBuilding'),
permissions: 'READ_DEPARTMENTS',
},
{
href: '/documents/documents-list',
label: 'Documents',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFolderFile' in icon ? icon['mdiFolderFile' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_DOCUMENTS'
icon: optionalIcon('mdiFolderFile'),
permissions: 'READ_DOCUMENTS',
},
],
},
{
label: 'Workflow Control',
icon: icon.mdiSitemap,
roles: adminWorkspaceRoles,
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: '/approvals/approvals-list',
label: 'Approvals',
icon: optionalIcon('mdiChecklist'),
permissions: 'READ_APPROVALS',
},
{
href: '/role_permissions/role_permissions-list',
label: 'Role access',
icon: optionalIcon('mdiLinkVariant'),
permissions: 'READ_ROLE_PERMISSIONS',
},
],
},
{
label: 'Assurance & Notices',
icon: icon.mdiBellOutline,
roles: adminWorkspaceRoles,
menu: [
{
href: '/notifications/notifications-list',
label: 'Notifications',
icon: optionalIcon('mdiBell'),
permissions: 'READ_NOTIFICATIONS',
},
{
href: '/compliance_alerts/compliance_alerts-list',
label: 'Compliance alerts',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiShieldAlert' in icon ? icon['mdiShieldAlert' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_COMPLIANCE_ALERTS'
icon: optionalIcon('mdiShieldAlert'),
permissions: 'READ_COMPLIANCE_ALERTS',
},
{
href: '/audit_logs/audit_logs-list',
label: 'Audit logs',
icon: optionalIcon('mdiClipboardTextClock'),
permissions: 'READ_AUDIT_LOGS',
},
],
},
]
const sharedEntityNavigation: MenuAsideItem[] = []
const menuAside: MenuAsideItem[] = [
{
href: '/executive-summary',
icon: icon.mdiBankOutline,
label: 'Role Workspace',
labelByRole: {
[WORKSPACE_ROLES.superAdmin]: 'Platform Administration',
[WORKSPACE_ROLES.administrator]: 'Organization Administration',
[WORKSPACE_ROLES.directorGeneral]: 'Executive Oversight',
[WORKSPACE_ROLES.financeDirector]: 'Financial Management',
[WORKSPACE_ROLES.procurementLead]: 'Procurement Management',
[WORKSPACE_ROLES.complianceAuditLead]: 'Compliance & Audit',
[WORKSPACE_ROLES.projectDeliveryLead]: 'Project Delivery',
},
},
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Role Widgets',
labelByRole: {
[WORKSPACE_ROLES.superAdmin]: 'Platform Widgets',
[WORKSPACE_ROLES.administrator]: 'Operations Widgets',
},
roles: [WORKSPACE_ROLES.superAdmin, WORKSPACE_ROLES.administrator],
},
...superAdminGroupedNavigation,
...adminGroupedNavigation,
{
href: '/projects/projects-list',
label: 'Strategic Portfolio',
icon: icon.mdiChartTimelineVariant,
permissions: 'READ_PROJECTS',
roles: [WORKSPACE_ROLES.directorGeneral],
withDevider: true,
},
{
href: '/contracts/contracts-list',
label: 'Contract Exposure',
icon: icon.mdiFileDocumentOutline,
permissions: 'READ_CONTRACTS',
roles: [WORKSPACE_ROLES.directorGeneral],
},
{
href: '/payment_requests/payment_requests-list',
label: 'Payment Control',
icon: icon.mdiCashCheck,
permissions: 'READ_PAYMENT_REQUESTS',
roles: [WORKSPACE_ROLES.financeDirector],
withDevider: true,
},
{
href: '/allocations/allocations-list',
label: 'Allocation Register',
icon: icon.mdiWalletOutline,
permissions: 'READ_ALLOCATIONS',
roles: [WORKSPACE_ROLES.financeDirector],
},
{
href: '/requisitions/requisitions-list',
label: 'Requisition Queue',
icon: icon.mdiClipboardListOutline,
permissions: 'READ_REQUISITIONS',
roles: [WORKSPACE_ROLES.procurementLead],
withDevider: true,
},
{
href: '/tenders/tenders-list',
label: 'Tender Pipeline',
icon: icon.mdiGavel,
permissions: 'READ_TENDERS',
roles: [WORKSPACE_ROLES.procurementLead],
},
{
href: '/compliance_alerts/compliance_alerts-list',
label: 'Control Exceptions',
icon: icon.mdiShieldAlertOutline,
permissions: 'READ_COMPLIANCE_ALERTS',
roles: [WORKSPACE_ROLES.complianceAuditLead],
withDevider: true,
},
{
href: '/audit_logs/audit_logs-list',
label: 'Audit Trail',
icon: icon.mdiClipboardTextClock,
permissions: 'READ_AUDIT_LOGS',
roles: [WORKSPACE_ROLES.complianceAuditLead],
},
{
href: '/projects/projects-list',
label: 'Delivery Portfolio',
icon: icon.mdiChartTimelineVariant,
permissions: 'READ_PROJECTS',
roles: [WORKSPACE_ROLES.projectDeliveryLead],
withDevider: true,
},
{
href: '/project_milestones/project_milestones-list',
label: 'Milestone Tracker',
icon: icon.mdiFlagCheckered,
permissions: 'READ_PROJECT_MILESTONES',
roles: [WORKSPACE_ROLES.projectDeliveryLead],
},
...sharedEntityNavigation,
{
href: '/profile',
label: 'Profile',
icon: icon.mdiAccountCircle,
},
{
href: '/api-docs',
target: '_blank',
label: 'Swagger API',
icon: icon.mdiFileCode,
permissions: 'READ_API_DOCS'
permissions: 'READ_API_DOCS',
},
]

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,29 +344,14 @@ const EditAllocationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='BudgetLine' labelFor='budget_line'>
<EnhancedEntityEditShell
entityLabel="Allocation"
pluralLabel="Allocations"
listHref="/allocations/allocations-list"
viewHref={`/allocations/allocations-view/?id=${id}`}
record={initialValues}
>
<FormField label='BudgetLine' labelFor='budget_line'>
<Field
name='budget_line'
id='budget_line'
@ -1115,6 +1101,7 @@ const EditAllocationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/allocations/allocations-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,841 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/allocations/allocationsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/allocations/allocationsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const AllocationsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { allocations } = useAppSelector((state) => state.allocations)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View allocations')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View allocations')} main>
<BaseButton
color='info'
label='Edit'
href={`/allocations/allocations-edit/?id=${id}`}
const AllocationsView = () => (
<EntityRecordViewPage
singularLabel="Allocation"
pluralLabel="Allocations"
stateKey="allocations"
recordKey="allocations"
fetchRecord={fetch}
listHref="/allocations/allocations-list"
editHref={(id) => `/allocations/allocations-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>BudgetLine</p>
<p>{allocations?.budget_line?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Province</p>
<p>{allocations?.province?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Department</p>
<p>{allocations?.department?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AllocationAmount</p>
<p>{allocations?.amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{allocations?.currency ?? 'No data'}</p>
</div>
<FormField label='AllocatedAt'>
{allocations.allocated_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={allocations.allocated_at ?
new Date(
dayjs(allocations.allocated_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No AllocatedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{allocations?.status ?? 'No data'}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={allocations?.notes} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{allocations?.organizations?.name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/allocations/allocations-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
AllocationsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_ALLOCATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_ALLOCATIONS'}>{page}</LayoutAuthenticated>;
};
export default AllocationsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,29 +344,14 @@ const EditApproval_stepsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Workflow' labelFor='workflow'>
<EnhancedEntityEditShell
entityLabel="Approval Step"
pluralLabel="Approval Steps"
listHref="/approval_steps/approval_steps-list"
viewHref={`/approval_steps/approval_steps-view/?id=${id}`}
record={initialValues}
>
<FormField label='Workflow' labelFor='workflow'>
<Field
name='workflow'
id='workflow'
@ -1107,6 +1093,7 @@ const EditApproval_stepsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/approval_steps/approval_steps-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,954 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/approval_steps/approval_stepsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/approval_steps/approval_stepsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Approval_stepsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { approval_steps } = useAppSelector((state) => state.approval_steps)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View approval_steps')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View approval_steps')} main>
<BaseButton
color='info'
label='Edit'
href={`/approval_steps/approval_steps-edit/?id=${id}`}
const ApprovalStepsView = () => (
<EntityRecordViewPage
singularLabel="Approval Step"
pluralLabel="Approval Steps"
stateKey="approval_steps"
recordKey="approval_steps"
fetchRecord={fetch}
listHref="/approval_steps/approval_steps-list"
editHref={(id) => `/approval_steps/approval_steps-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Workflow</p>
<p>{approval_steps?.workflow?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>StepOrder</p>
<p>{approval_steps?.step_order || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>StepName</p>
<p>{approval_steps?.name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ApproverRole</p>
<p>{approval_steps?.approver_role?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ApproverDepartment</p>
<p>{approval_steps?.approver_department?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>MinimumAmount</p>
<p>{approval_steps?.min_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>MaximumAmount</p>
<p>{approval_steps?.max_amount || 'No data'}</p>
</div>
<FormField label='RequiresComment'>
<SwitchField
field={{name: 'requires_comment', value: approval_steps?.requires_comment}}
form={{setFieldValue: () => null}}
disabled
/>
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{approval_steps?.organizations?.name ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Approvals CurrentStep</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>RecordType</th>
<th>RecordKey</th>
<th>Status</th>
<th>RequestedAt</th>
<th>DecidedAt</th>
<th>DecisionComment</th>
<th>RejectionReason</th>
</tr>
</thead>
<tbody>
{approval_steps.approvals_step && Array.isArray(approval_steps.approvals_step) &&
approval_steps.approvals_step.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/approvals/approvals-view/?id=${item.id}`)}>
<td data-label="record_type">
{ item.record_type }
</td>
<td data-label="record_key">
{ item.record_key }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="requested_at">
{ dataFormatter.dateTimeFormatter(item.requested_at) }
</td>
<td data-label="decided_at">
{ dataFormatter.dateTimeFormatter(item.decided_at) }
</td>
<td data-label="decision_comment">
{ item.decision_comment }
</td>
<td data-label="rejection_reason">
{ item.rejection_reason }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!approval_steps?.approvals_step?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/approval_steps/approval_steps-list')}
/>
</CardBox>
</SectionMain>
</>
);
ApprovalStepsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_APPROVAL_STEPS'}>{page}</LayoutAuthenticated>;
};
Approval_stepsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_APPROVAL_STEPS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Approval_stepsView;
export default ApprovalStepsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -231,28 +232,14 @@ const EditApproval_workflowsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Approval Workflow"
pluralLabel="Approval Workflows"
listHref="/approval_workflows/approval_workflows-list"
viewHref={`/approval_workflows/approval_workflows-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -563,6 +550,7 @@ const EditApproval_workflowsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/approval_workflows/approval_workflows-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,606 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/approval_workflows/approval_workflowsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/approval_workflows/approval_workflowsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Approval_workflowsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { approval_workflows } = useAppSelector((state) => state.approval_workflows)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View approval_workflows')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View approval_workflows')} main>
<BaseButton
color='info'
label='Edit'
href={`/approval_workflows/approval_workflows-edit/?id=${id}`}
const ApprovalWorkflowsView = () => (
<EntityRecordViewPage
singularLabel="Approval Workflow"
pluralLabel="Approval Workflows"
stateKey="approval_workflows"
recordKey="approval_workflows"
fetchRecord={fetch}
listHref="/approval_workflows/approval_workflows-list"
editHref={(id) => `/approval_workflows/approval_workflows-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{approval_workflows?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>WorkflowName</p>
<p>{approval_workflows?.name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Module</p>
<p>{approval_workflows?.module ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordType</p>
<p>{approval_workflows?.record_type ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{approval_workflows?.status ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Approval_steps Workflow</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>StepOrder</th>
<th>StepName</th>
<th>MinimumAmount</th>
<th>MaximumAmount</th>
<th>RequiresComment</th>
</tr>
</thead>
<tbody>
{approval_workflows.approval_steps_workflow && Array.isArray(approval_workflows.approval_steps_workflow) &&
approval_workflows.approval_steps_workflow.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/approval_steps/approval_steps-view/?id=${item.id}`)}>
<td data-label="step_order">
{ item.step_order }
</td>
<td data-label="name">
{ item.name }
</td>
<td data-label="min_amount">
{ item.min_amount }
</td>
<td data-label="max_amount">
{ item.max_amount }
</td>
<td data-label="requires_comment">
{ dataFormatter.booleanFormatter(item.requires_comment) }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!approval_workflows?.approval_steps_workflow?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Approvals Workflow</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>RecordType</th>
<th>RecordKey</th>
<th>Status</th>
<th>RequestedAt</th>
<th>DecidedAt</th>
<th>DecisionComment</th>
<th>RejectionReason</th>
</tr>
</thead>
<tbody>
{approval_workflows.approvals_workflow && Array.isArray(approval_workflows.approvals_workflow) &&
approval_workflows.approvals_workflow.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/approvals/approvals-view/?id=${item.id}`)}>
<td data-label="record_type">
{ item.record_type }
</td>
<td data-label="record_key">
{ item.record_key }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="requested_at">
{ dataFormatter.dateTimeFormatter(item.requested_at) }
</td>
<td data-label="decided_at">
{ dataFormatter.dateTimeFormatter(item.decided_at) }
</td>
<td data-label="decision_comment">
{ item.decision_comment }
</td>
<td data-label="rejection_reason">
{ item.rejection_reason }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!approval_workflows?.approvals_workflow?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/approval_workflows/approval_workflows-list')}
/>
</CardBox>
</SectionMain>
</>
);
ApprovalWorkflowsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_APPROVAL_WORKFLOWS'}>{page}</LayoutAuthenticated>;
};
Approval_workflowsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_APPROVAL_WORKFLOWS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Approval_workflowsView;
export default ApprovalWorkflowsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -427,28 +428,14 @@ const EditApprovalsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Approval"
pluralLabel="Approvals"
listHref="/approvals/approvals-list"
viewHref={`/approvals/approvals-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1412,6 +1399,7 @@ const EditApprovalsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/approvals/approvals-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,28 +344,14 @@ const EditAudit_logsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Audit Log"
pluralLabel="Audit Logs"
listHref="/audit_logs/audit_logs-list"
viewHref={`/audit_logs/audit_logs-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -891,6 +878,7 @@ const EditAudit_logsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/audit_logs/audit_logs-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,636 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/audit_logs/audit_logsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/audit_logs/audit_logsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Audit_logsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { audit_logs } = useAppSelector((state) => state.audit_logs)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View audit_logs')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View audit_logs')} main>
<BaseButton
color='info'
label='Edit'
href={`/audit_logs/audit_logs-edit/?id=${id}`}
const AuditLogsView = () => (
<EntityRecordViewPage
singularLabel="Audit Log"
pluralLabel="Audit Logs"
stateKey="audit_logs"
recordKey="audit_logs"
fetchRecord={fetch}
listHref="/audit_logs/audit_logs-list"
editHref={(id) => `/audit_logs/audit_logs-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{audit_logs?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ActorUser</p>
<p>{audit_logs?.actor_user?.firstName ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Action</p>
<p>{audit_logs?.action}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>EntityName</p>
<p>{audit_logs?.entity_name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordKey</p>
<p>{audit_logs?.record_key}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={audit_logs?.summary} />
</FormField>
<FormField label='OccurredAt'>
{audit_logs.occurred_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={audit_logs.occurred_at ?
new Date(
dayjs(audit_logs.occurred_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No OccurredAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>IPAddress</p>
<p>{audit_logs?.ip_address}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={audit_logs?.user_agent} />
</FormField>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/audit_logs/audit_logs-list')}
/>
</CardBox>
</SectionMain>
</>
);
AuditLogsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_AUDIT_LOGS'}>{page}</LayoutAuthenticated>;
};
Audit_logsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_AUDIT_LOGS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Audit_logsView;
export default AuditLogsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,28 +344,14 @@ const EditAwardsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Award"
pluralLabel="Awards"
listHref="/awards/awards-list"
viewHref={`/awards/awards-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1016,6 +1003,7 @@ const EditAwardsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/awards/awards-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,900 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/awards/awardsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/awards/awardsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const AwardsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { awards } = useAppSelector((state) => state.awards)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View awards')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View awards')} main>
<BaseButton
color='info'
label='Edit'
href={`/awards/awards-edit/?id=${id}`}
const AwardsView = () => (
<EntityRecordViewPage
singularLabel="Award"
pluralLabel="Awards"
stateKey="awards"
recordKey="awards"
fetchRecord={fetch}
listHref="/awards/awards-list"
editHref={(id) => `/awards/awards-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{awards?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Tender</p>
<p>{awards?.tender?.tender_number ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>WinningBid</p>
<p>{awards?.winning_bid?.bid_reference ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AwardNumber</p>
<p>{awards?.award_number}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AwardAmount</p>
<p>{awards?.award_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{awards?.currency ?? 'No data'}</p>
</div>
<FormField label='DecisionDate'>
{awards.decision_date ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={awards.decision_date ?
new Date(
dayjs(awards.decision_date).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No DecisionDate</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{awards?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AwardMemo</p>
{awards.award_memo
? <p dangerouslySetInnerHTML={{__html: awards.award_memo}}/>
: <p>No data</p>
}
</div>
<>
<p className={'block font-bold mb-2'}>Contracts Award</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>ContractNumber</th>
<th>ContractTitle</th>
<th>ContractType</th>
<th>ContractValue</th>
<th>Currency</th>
<th>StartDate</th>
<th>EndDate</th>
<th>Status</th>
<th>RetentionPercent</th>
<th>PenaltyRatePercent</th>
<th>SignedAt</th>
</tr>
</thead>
<tbody>
{awards.contracts_award && Array.isArray(awards.contracts_award) &&
awards.contracts_award.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/contracts/contracts-view/?id=${item.id}`)}>
<td data-label="contract_number">
{ item.contract_number }
</td>
<td data-label="title">
{ item.title }
</td>
<td data-label="contract_type">
{ item.contract_type }
</td>
<td data-label="contract_value">
{ item.contract_value }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="start_date">
{ dataFormatter.dateTimeFormatter(item.start_date) }
</td>
<td data-label="end_date">
{ dataFormatter.dateTimeFormatter(item.end_date) }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="retention_percent">
{ item.retention_percent }
</td>
<td data-label="penalty_rate_percent">
{ item.penalty_rate_percent }
</td>
<td data-label="signed_at">
{ dataFormatter.dateTimeFormatter(item.signed_at) }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!awards?.contracts_award?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/awards/awards-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
AwardsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_AWARDS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_AWARDS'}>{page}</LayoutAuthenticated>;
};
export default AwardsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -427,29 +428,14 @@ const EditBeneficiariesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Grant' labelFor='grant'>
<EnhancedEntityEditShell
entityLabel="Beneficiary"
pluralLabel="Beneficiaries"
listHref="/beneficiaries/beneficiaries-list"
viewHref={`/beneficiaries/beneficiaries-view/?id=${id}`}
record={initialValues}
>
<FormField label='Grant' labelFor='grant'>
<Field
name='grant'
id='grant'
@ -1225,6 +1211,7 @@ const EditBeneficiariesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/beneficiaries/beneficiaries-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,925 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/beneficiaries/beneficiariesSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/beneficiaries/beneficiariesSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const BeneficiariesView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { beneficiaries } = useAppSelector((state) => state.beneficiaries)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View beneficiaries')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View beneficiaries')} main>
<BaseButton
color='info'
label='Edit'
href={`/beneficiaries/beneficiaries-edit/?id=${id}`}
const BeneficiariesView = () => (
<EntityRecordViewPage
singularLabel="Beneficiary"
pluralLabel="Beneficiaries"
stateKey="beneficiaries"
recordKey="beneficiaries"
fetchRecord={fetch}
listHref="/beneficiaries/beneficiaries-list"
editHref={(id) => `/beneficiaries/beneficiaries-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Grant</p>
<p>{beneficiaries?.grant?.call_reference ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Province</p>
<p>{beneficiaries?.province?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>BeneficiaryName</p>
<p>{beneficiaries?.name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>BeneficiaryType</p>
<p>{beneficiaries?.beneficiary_type ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RegistrationNumber</p>
<p>{beneficiaries?.registration_number}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ContactEmail</p>
<p>{beneficiaries?.contact_email}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ContactPhone</p>
<p>{beneficiaries?.contact_phone}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ApprovedAmount</p>
<p>{beneficiaries?.approved_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{beneficiaries?.currency ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{beneficiaries?.status ?? 'No data'}</p>
</div>
<FormField label='ApprovedAt'>
{beneficiaries.approved_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={beneficiaries.approved_at ?
new Date(
dayjs(beneficiaries.approved_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No ApprovedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{beneficiaries?.organizations?.name ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Grant_applications Beneficiary</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>ApplicationReference</th>
<th>SubmittedAt</th>
<th>RequestedAmount</th>
<th>Currency</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{beneficiaries.grant_applications_beneficiary && Array.isArray(beneficiaries.grant_applications_beneficiary) &&
beneficiaries.grant_applications_beneficiary.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/grant_applications/grant_applications-view/?id=${item.id}`)}>
<td data-label="application_reference">
{ item.application_reference }
</td>
<td data-label="submitted_at">
{ dataFormatter.dateTimeFormatter(item.submitted_at) }
</td>
<td data-label="requested_amount">
{ item.requested_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="status">
{ item.status }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!beneficiaries?.grant_applications_beneficiary?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/beneficiaries/beneficiaries-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
BeneficiariesView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_BENEFICIARIES'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_BENEFICIARIES'}>{page}</LayoutAuthenticated>;
};
export default BeneficiariesView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,29 +344,14 @@ const EditBid_evaluationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Bid' labelFor='bid'>
<EnhancedEntityEditShell
entityLabel="Bid Evaluation"
pluralLabel="Bid Evaluations"
listHref="/bid_evaluations/bid_evaluations-list"
viewHref={`/bid_evaluations/bid_evaluations-view/?id=${id}`}
record={initialValues}
>
<FormField label='Bid' labelFor='bid'>
<Field
name='bid'
id='bid'
@ -1007,6 +993,7 @@ const EditBid_evaluationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/bid_evaluations/bid_evaluations-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,739 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/bid_evaluations/bid_evaluationsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/bid_evaluations/bid_evaluationsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Bid_evaluationsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { bid_evaluations } = useAppSelector((state) => state.bid_evaluations)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View bid_evaluations')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View bid_evaluations')} main>
<BaseButton
color='info'
label='Edit'
href={`/bid_evaluations/bid_evaluations-edit/?id=${id}`}
const BidEvaluationsView = () => (
<EntityRecordViewPage
singularLabel="Bid Evaluation"
pluralLabel="Bid Evaluations"
stateKey="bid_evaluations"
recordKey="bid_evaluations"
fetchRecord={fetch}
listHref="/bid_evaluations/bid_evaluations-list"
editHref={(id) => `/bid_evaluations/bid_evaluations-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Bid</p>
<p>{bid_evaluations?.bid?.bid_reference ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>LeadEvaluator</p>
<p>{bid_evaluations?.lead_evaluator_user?.firstName ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>TechnicalScore</p>
<p>{bid_evaluations?.technical_score || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FinancialScore</p>
<p>{bid_evaluations?.financial_score || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>TotalScore</p>
<p>{bid_evaluations?.total_score || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Recommendation</p>
<p>{bid_evaluations?.recommendation ?? 'No data'}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={bid_evaluations?.justification} />
</FormField>
<FormField label='EvaluatedAt'>
{bid_evaluations.evaluated_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={bid_evaluations.evaluated_at ?
new Date(
dayjs(bid_evaluations.evaluated_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No EvaluatedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{bid_evaluations?.organizations?.name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/bid_evaluations/bid_evaluations-list')}
/>
</CardBox>
</SectionMain>
</>
);
BidEvaluationsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_BID_EVALUATIONS'}>{page}</LayoutAuthenticated>;
};
Bid_evaluationsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_BID_EVALUATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Bid_evaluationsView;
export default BidEvaluationsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,29 +344,14 @@ const EditBidsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Tender' labelFor='tender'>
<EnhancedEntityEditShell
entityLabel="Bid"
pluralLabel="Bids"
listHref="/bids/bids-list"
viewHref={`/bids/bids-view/?id=${id}`}
record={initialValues}
>
<FormField label='Tender' labelFor='tender'>
<Field
name='tender'
id='tender'
@ -1014,6 +1000,7 @@ const EditBidsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/bids/bids-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,927 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/bids/bidsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/bids/bidsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const BidsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { bids } = useAppSelector((state) => state.bids)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View bids')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View bids')} main>
<BaseButton
color='info'
label='Edit'
href={`/bids/bids-edit/?id=${id}`}
const BidsView = () => (
<EntityRecordViewPage
singularLabel="Bid"
pluralLabel="Bids"
stateKey="bids"
recordKey="bids"
fetchRecord={fetch}
listHref="/bids/bids-list"
editHref={(id) => `/bids/bids-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Tender</p>
<p>{bids?.tender?.tender_number ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Vendor</p>
<p>{bids?.vendor?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>BidReference</p>
<p>{bids?.bid_reference}</p>
</div>
<FormField label='SubmittedAt'>
{bids.submitted_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={bids.submitted_at ?
new Date(
dayjs(bids.submitted_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No SubmittedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>BidAmount</p>
<p>{bids?.bid_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{bids?.currency ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{bids?.status ?? 'No data'}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={bids?.notes} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{bids?.organizations?.name ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Bid_evaluations Bid</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>TechnicalScore</th>
<th>FinancialScore</th>
<th>TotalScore</th>
<th>Recommendation</th>
<th>Justification</th>
<th>EvaluatedAt</th>
</tr>
</thead>
<tbody>
{bids.bid_evaluations_bid && Array.isArray(bids.bid_evaluations_bid) &&
bids.bid_evaluations_bid.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/bid_evaluations/bid_evaluations-view/?id=${item.id}`)}>
<td data-label="technical_score">
{ item.technical_score }
</td>
<td data-label="financial_score">
{ item.financial_score }
</td>
<td data-label="total_score">
{ item.total_score }
</td>
<td data-label="recommendation">
{ item.recommendation }
</td>
<td data-label="justification">
{ item.justification }
</td>
<td data-label="evaluated_at">
{ dataFormatter.dateTimeFormatter(item.evaluated_at) }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!bids?.bid_evaluations_bid?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Awards WinningBid</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>AwardNumber</th>
<th>AwardAmount</th>
<th>Currency</th>
<th>DecisionDate</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{bids.awards_winning_bid && Array.isArray(bids.awards_winning_bid) &&
bids.awards_winning_bid.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/awards/awards-view/?id=${item.id}`)}>
<td data-label="award_number">
{ item.award_number }
</td>
<td data-label="award_amount">
{ item.award_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="decision_date">
{ dataFormatter.dateTimeFormatter(item.decision_date) }
</td>
<td data-label="status">
{ item.status }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!bids?.awards_winning_bid?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/bids/bids-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
BidsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_BIDS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_BIDS'}>{page}</LayoutAuthenticated>;
};
export default BidsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -399,29 +400,14 @@ const EditBudget_linesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='BudgetProgram' labelFor='budget_program'>
<EnhancedEntityEditShell
entityLabel="Budget Line"
pluralLabel="Budget Lines"
listHref="/budget_lines/budget_lines-list"
viewHref={`/budget_lines/budget_lines-view/?id=${id}`}
record={initialValues}
>
<FormField label='BudgetProgram' labelFor='budget_program'>
<Field
name='budget_program'
id='budget_program'
@ -1038,6 +1024,7 @@ const EditBudget_linesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/budget_lines/budget_lines-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,28 +344,14 @@ const EditBudget_programsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Budget Program"
pluralLabel="Budget Programs"
listHref="/budget_programs/budget_programs-list"
viewHref={`/budget_programs/budget_programs-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1004,6 +991,7 @@ const EditBudget_programsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/budget_programs/budget_programs-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,852 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/budget_programs/budget_programsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/budget_programs/budget_programsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Budget_programsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { budget_programs } = useAppSelector((state) => state.budget_programs)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View budget_programs')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View budget_programs')} main>
<BaseButton
color='info'
label='Edit'
href={`/budget_programs/budget_programs-edit/?id=${id}`}
const BudgetProgramsView = () => (
<EntityRecordViewPage
singularLabel="Budget Program"
pluralLabel="Budget Programs"
stateKey="budget_programs"
recordKey="budget_programs"
fetchRecord={fetch}
listHref="/budget_programs/budget_programs-list"
editHref={(id) => `/budget_programs/budget_programs-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{budget_programs?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FiscalYear</p>
<p>{budget_programs?.fiscal_year?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FundingSource</p>
<p>{budget_programs?.funding_source?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ProgramName</p>
<p>{budget_programs?.name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ProgramCode</p>
<p>{budget_programs?.code}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={budget_programs?.objective} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{budget_programs?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ApprovedAmount</p>
<p>{budget_programs?.approved_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{budget_programs?.currency ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Budget_lines BudgetProgram</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>BudgetLineName</th>
<th>BudgetLineCode</th>
<th>Description</th>
<th>Category</th>
<th>ApprovedAmount</th>
<th>CommittedAmount</th>
<th>DisbursedAmount</th>
<th>Currency</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{budget_programs.budget_lines_budget_program && Array.isArray(budget_programs.budget_lines_budget_program) &&
budget_programs.budget_lines_budget_program.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/budget_lines/budget_lines-view/?id=${item.id}`)}>
<td data-label="name">
{ item.name }
</td>
<td data-label="code">
{ item.code }
</td>
<td data-label="description">
{ item.description }
</td>
<td data-label="category">
{ item.category }
</td>
<td data-label="approved_amount">
{ item.approved_amount }
</td>
<td data-label="committed_amount">
{ item.committed_amount }
</td>
<td data-label="disbursed_amount">
{ item.disbursed_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="status">
{ item.status }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!budget_programs?.budget_lines_budget_program?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/budget_programs/budget_programs-list')}
/>
</CardBox>
</SectionMain>
</>
);
BudgetProgramsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_BUDGET_PROGRAMS'}>{page}</LayoutAuthenticated>;
};
Budget_programsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_BUDGET_PROGRAMS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Budget_programsView;
export default BudgetProgramsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -371,28 +372,14 @@ const EditBudget_reallocationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Budget Reallocation"
pluralLabel="Budget Reallocations"
listHref="/budget_reallocations/budget_reallocations-list"
viewHref={`/budget_reallocations/budget_reallocations-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1285,6 +1272,7 @@ const EditBudget_reallocationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/budget_reallocations/budget_reallocations-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,975 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/budget_reallocations/budget_reallocationsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/budget_reallocations/budget_reallocationsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Budget_reallocationsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { budget_reallocations } = useAppSelector((state) => state.budget_reallocations)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View budget_reallocations')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View budget_reallocations')} main>
<BaseButton
color='info'
label='Edit'
href={`/budget_reallocations/budget_reallocations-edit/?id=${id}`}
const BudgetReallocationsView = () => (
<EntityRecordViewPage
singularLabel="Budget Reallocation"
pluralLabel="Budget Reallocations"
stateKey="budget_reallocations"
recordKey="budget_reallocations"
fetchRecord={fetch}
listHref="/budget_reallocations/budget_reallocations-list"
editHref={(id) => `/budget_reallocations/budget_reallocations-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{budget_reallocations?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FiscalYear</p>
<p>{budget_reallocations?.fiscal_year?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FromBudgetLine</p>
<p>{budget_reallocations?.from_budget_line?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ToBudgetLine</p>
<p>{budget_reallocations?.to_budget_line?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Amount</p>
<p>{budget_reallocations?.amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{budget_reallocations?.currency ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{budget_reallocations?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RequestedBy</p>
<p>{budget_reallocations?.requested_by_user?.firstName ?? 'No data'}</p>
</div>
<FormField label='RequestedAt'>
{budget_reallocations.requested_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={budget_reallocations.requested_at ?
new Date(
dayjs(budget_reallocations.requested_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No RequestedAt</p>}
</FormField>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={budget_reallocations?.justification} />
</FormField>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/budget_reallocations/budget_reallocations-list')}
/>
</CardBox>
</SectionMain>
</>
);
BudgetReallocationsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_BUDGET_REALLOCATIONS'}>{page}</LayoutAuthenticated>;
};
Budget_reallocationsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_BUDGET_REALLOCATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Budget_reallocationsView;
export default BudgetReallocationsView;

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const ComplianceDeskPage: any = ExecutiveSummaryPage;
ComplianceDeskPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default ComplianceDeskPage;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -399,28 +400,14 @@ const EditCompliance_alertsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Compliance Alert"
pluralLabel="Compliance Alerts"
listHref="/compliance_alerts/compliance_alerts-list"
viewHref={`/compliance_alerts/compliance_alerts-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1057,6 +1044,7 @@ const EditCompliance_alertsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/compliance_alerts/compliance_alerts-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,713 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/compliance_alerts/compliance_alertsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/compliance_alerts/compliance_alertsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Compliance_alertsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { compliance_alerts } = useAppSelector((state) => state.compliance_alerts)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View compliance_alerts')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View compliance_alerts')} main>
<BaseButton
color='info'
label='Edit'
href={`/compliance_alerts/compliance_alerts-edit/?id=${id}`}
const ComplianceAlertsView = () => (
<EntityRecordViewPage
singularLabel="Compliance Alert"
pluralLabel="Compliance Alerts"
stateKey="compliance_alerts"
recordKey="compliance_alerts"
fetchRecord={fetch}
listHref="/compliance_alerts/compliance_alerts-list"
editHref={(id) => `/compliance_alerts/compliance_alerts-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{compliance_alerts?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AlertType</p>
<p>{compliance_alerts?.alert_type ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Severity</p>
<p>{compliance_alerts?.severity ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Title</p>
<p>{compliance_alerts?.title}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Details</p>
{compliance_alerts.details
? <p dangerouslySetInnerHTML={{__html: compliance_alerts.details}}/>
: <p>No data</p>
}
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordType</p>
<p>{compliance_alerts?.record_type}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordKey</p>
<p>{compliance_alerts?.record_key}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{compliance_alerts?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AssignedTo</p>
<p>{compliance_alerts?.assigned_to_user?.firstName ?? 'No data'}</p>
</div>
<FormField label='DetectedAt'>
{compliance_alerts.detected_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={compliance_alerts.detected_at ?
new Date(
dayjs(compliance_alerts.detected_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No DetectedAt</p>}
</FormField>
<FormField label='DueAt'>
{compliance_alerts.due_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={compliance_alerts.due_at ?
new Date(
dayjs(compliance_alerts.due_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No DueAt</p>}
</FormField>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/compliance_alerts/compliance_alerts-list')}
/>
</CardBox>
</SectionMain>
</>
);
ComplianceAlertsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_COMPLIANCE_ALERTS'}>{page}</LayoutAuthenticated>;
};
Compliance_alertsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_COMPLIANCE_ALERTS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Compliance_alertsView;
export default ComplianceAlertsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,29 +344,14 @@ const EditContract_amendmentsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Contract' labelFor='contract'>
<EnhancedEntityEditShell
entityLabel="Contract Amendment"
pluralLabel="Contract Amendments"
listHref="/contract_amendments/contract_amendments-list"
viewHref={`/contract_amendments/contract_amendments-view/?id=${id}`}
record={initialValues}
>
<FormField label='Contract' labelFor='contract'>
<Field
name='contract'
id='contract'
@ -916,6 +902,7 @@ const EditContract_amendmentsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/contract_amendments/contract_amendments-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,641 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/contract_amendments/contract_amendmentsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/contract_amendments/contract_amendmentsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Contract_amendmentsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { contract_amendments } = useAppSelector((state) => state.contract_amendments)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View contract_amendments')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View contract_amendments')} main>
<BaseButton
color='info'
label='Edit'
href={`/contract_amendments/contract_amendments-edit/?id=${id}`}
const ContractAmendmentsView = () => (
<EntityRecordViewPage
singularLabel="Contract Amendment"
pluralLabel="Contract Amendments"
stateKey="contract_amendments"
recordKey="contract_amendments"
fetchRecord={fetch}
listHref="/contract_amendments/contract_amendments-list"
editHref={(id) => `/contract_amendments/contract_amendments-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Contract</p>
<p>{contract_amendments?.contract?.contract_number ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AmendmentNumber</p>
<p>{contract_amendments?.amendment_number}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AmendmentType</p>
<p>{contract_amendments?.amendment_type ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ValueChangeAmount</p>
<p>{contract_amendments?.value_change_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>DaysExtension</p>
<p>{contract_amendments?.days_extension || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Description</p>
{contract_amendments.description
? <p dangerouslySetInnerHTML={{__html: contract_amendments.description}}/>
: <p>No data</p>
}
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{contract_amendments?.status ?? 'No data'}</p>
</div>
<FormField label='EffectiveDate'>
{contract_amendments.effective_date ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={contract_amendments.effective_date ?
new Date(
dayjs(contract_amendments.effective_date).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No EffectiveDate</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{contract_amendments?.organizations?.name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/contract_amendments/contract_amendments-list')}
/>
</CardBox>
</SectionMain>
</>
);
ContractAmendmentsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_CONTRACT_AMENDMENTS'}>{page}</LayoutAuthenticated>;
};
Contract_amendmentsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_CONTRACT_AMENDMENTS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Contract_amendmentsView;
export default ContractAmendmentsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -315,29 +316,14 @@ const EditContract_milestonesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Contract' labelFor='contract'>
<EnhancedEntityEditShell
entityLabel="Contract Milestone"
pluralLabel="Contract Milestones"
listHref="/contract_milestones/contract_milestones-list"
viewHref={`/contract_milestones/contract_milestones-view/?id=${id}`}
record={initialValues}
>
<FormField label='Contract' labelFor='contract'>
<Field
name='contract'
id='contract'
@ -851,6 +837,7 @@ const EditContract_milestonesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/contract_milestones/contract_milestones-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,617 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/contract_milestones/contract_milestonesSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/contract_milestones/contract_milestonesSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Contract_milestonesView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { contract_milestones } = useAppSelector((state) => state.contract_milestones)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View contract_milestones')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View contract_milestones')} main>
<BaseButton
color='info'
label='Edit'
href={`/contract_milestones/contract_milestones-edit/?id=${id}`}
const ContractMilestonesView = () => (
<EntityRecordViewPage
singularLabel="Contract Milestone"
pluralLabel="Contract Milestones"
stateKey="contract_milestones"
recordKey="contract_milestones"
fetchRecord={fetch}
listHref="/contract_milestones/contract_milestones-list"
editHref={(id) => `/contract_milestones/contract_milestones-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Contract</p>
<p>{contract_milestones?.contract?.contract_number ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>MilestoneName</p>
<p>{contract_milestones?.name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>DeliverableDescription</p>
{contract_milestones.deliverable_description
? <p dangerouslySetInnerHTML={{__html: contract_milestones.deliverable_description}}/>
: <p>No data</p>
}
</div>
<FormField label='DueDate'>
{contract_milestones.due_date ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={contract_milestones.due_date ?
new Date(
dayjs(contract_milestones.due_date).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No DueDate</p>}
</FormField>
<FormField label='AcceptedAt'>
{contract_milestones.accepted_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={contract_milestones.accepted_at ?
new Date(
dayjs(contract_milestones.accepted_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No AcceptedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{contract_milestones?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>PaymentPercent</p>
<p>{contract_milestones?.payment_percent || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{contract_milestones?.organizations?.name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/contract_milestones/contract_milestones-list')}
/>
</CardBox>
</SectionMain>
</>
);
ContractMilestonesView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_CONTRACT_MILESTONES'}>{page}</LayoutAuthenticated>;
};
Contract_milestonesView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_CONTRACT_MILESTONES'}
>
{page}
</LayoutAuthenticated>
)
}
export default Contract_milestonesView;
export default ContractMilestonesView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -539,28 +540,14 @@ const EditContractsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Contract"
pluralLabel="Contracts"
listHref="/contracts/contracts-list"
viewHref={`/contracts/contracts-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1707,6 +1694,7 @@ const EditContractsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/contracts/contracts-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const DeliveryCommandPage: any = ExecutiveSummaryPage;
DeliveryCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default DeliveryCommandPage;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -231,28 +232,14 @@ const EditDepartmentsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Department"
pluralLabel="Departments"
listHref="/departments/departments-list"
viewHref={`/departments/departments-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -636,6 +623,7 @@ const EditDepartmentsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/departments/departments-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,924 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/departments/departmentsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/departments/departmentsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const DepartmentsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { departments } = useAppSelector((state) => state.departments)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View departments')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View departments')} main>
<BaseButton
color='info'
label='Edit'
href={`/departments/departments-edit/?id=${id}`}
const DepartmentsView = () => (
<EntityRecordViewPage
singularLabel="Department"
pluralLabel="Departments"
stateKey="departments"
recordKey="departments"
fetchRecord={fetch}
listHref="/departments/departments-list"
editHref={(id) => `/departments/departments-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{departments?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>DepartmentName</p>
<p>{departments?.name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>DepartmentCode</p>
<p>{departments?.code}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ParentDepartment</p>
<p>{departments?.parent_department?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{departments?.status ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Approval_steps ApproverDepartment</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>StepOrder</th>
<th>StepName</th>
<th>MinimumAmount</th>
<th>MaximumAmount</th>
<th>RequiresComment</th>
</tr>
</thead>
<tbody>
{departments.approval_steps_approver_department && Array.isArray(departments.approval_steps_approver_department) &&
departments.approval_steps_approver_department.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/approval_steps/approval_steps-view/?id=${item.id}`)}>
<td data-label="step_order">
{ item.step_order }
</td>
<td data-label="name">
{ item.name }
</td>
<td data-label="min_amount">
{ item.min_amount }
</td>
<td data-label="max_amount">
{ item.max_amount }
</td>
<td data-label="requires_comment">
{ dataFormatter.booleanFormatter(item.requires_comment) }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!departments?.approval_steps_approver_department?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Allocations Department</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>AllocationAmount</th>
<th>Currency</th>
<th>AllocatedAt</th>
<th>Status</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{departments.allocations_department && Array.isArray(departments.allocations_department) &&
departments.allocations_department.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/allocations/allocations-view/?id=${item.id}`)}>
<td data-label="amount">
{ item.amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="allocated_at">
{ dataFormatter.dateTimeFormatter(item.allocated_at) }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="notes">
{ item.notes }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!departments?.allocations_department?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Procurement_plans Department</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>PlanName</th>
<th>Status</th>
<th>EstimatedTotalAmount</th>
<th>Currency</th>
<th>SubmittedAt</th>
<th>ApprovedAt</th>
</tr>
</thead>
<tbody>
{departments.procurement_plans_department && Array.isArray(departments.procurement_plans_department) &&
departments.procurement_plans_department.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/procurement_plans/procurement_plans-view/?id=${item.id}`)}>
<td data-label="name">
{ item.name }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="estimated_total_amount">
{ item.estimated_total_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="submitted_at">
{ dataFormatter.dateTimeFormatter(item.submitted_at) }
</td>
<td data-label="approved_at">
{ dataFormatter.dateTimeFormatter(item.approved_at) }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!departments?.procurement_plans_department?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Requisitions RequestingDepartment</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>RequisitionNumber</th>
<th>Title</th>
<th>ProcurementMethod</th>
<th>EstimatedAmount</th>
<th>Currency</th>
<th>NeededByDate</th>
<th>Status</th>
<th>SubmittedAt</th>
<th>RejectionReason</th>
</tr>
</thead>
<tbody>
{departments.requisitions_requesting_department && Array.isArray(departments.requisitions_requesting_department) &&
departments.requisitions_requesting_department.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/requisitions/requisitions-view/?id=${item.id}`)}>
<td data-label="requisition_number">
{ item.requisition_number }
</td>
<td data-label="title">
{ item.title }
</td>
<td data-label="procurement_method">
{ item.procurement_method }
</td>
<td data-label="estimated_amount">
{ item.estimated_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="needed_by_date">
{ dataFormatter.dateTimeFormatter(item.needed_by_date) }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="submitted_at">
{ dataFormatter.dateTimeFormatter(item.submitted_at) }
</td>
<td data-label="rejection_reason">
{ item.rejection_reason }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!departments?.requisitions_requesting_department?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/departments/departments-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
DepartmentsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_DEPARTMENTS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_DEPARTMENTS'}>{page}</LayoutAuthenticated>;
};
export default DepartmentsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -399,28 +400,14 @@ const EditDocumentsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Document"
pluralLabel="Documents"
listHref="/documents/documents-list"
viewHref={`/documents/documents-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1054,6 +1041,7 @@ const EditDocumentsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/documents/documents-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,710 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/documents/documentsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/documents/documentsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const DocumentsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { documents } = useAppSelector((state) => state.documents)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View documents')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View documents')} main>
<BaseButton
color='info'
label='Edit'
href={`/documents/documents-edit/?id=${id}`}
const DocumentsView = () => (
<EntityRecordViewPage
singularLabel="Document"
pluralLabel="Documents"
stateKey="documents"
recordKey="documents"
fetchRecord={fetch}
listHref="/documents/documents-list"
editHref={(id) => `/documents/documents-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{documents?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Category</p>
<p>{documents?.category ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Title</p>
<p>{documents?.title}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={documents?.description} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordType</p>
<p>{documents?.record_type}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordKey</p>
<p>{documents?.record_key}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>File</p>
{documents?.file?.length
? dataFormatter.filesFormatter(documents.file).map(link => (
<button
key={link.publicUrl}
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
>
{link.name}
</button>
)) : <p>No File</p>
}
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>VersionNumber</p>
<p>{documents?.version_number || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AccessLevel</p>
<p>{documents?.access_level ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>UploadedBy</p>
<p>{documents?.uploaded_by_user?.firstName ?? 'No data'}</p>
</div>
<FormField label='UploadedAt'>
{documents.uploaded_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={documents.uploaded_at ?
new Date(
dayjs(documents.uploaded_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No UploadedAt</p>}
</FormField>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/documents/documents-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
DocumentsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_DOCUMENTS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_DOCUMENTS'}>{page}</LayoutAuthenticated>;
};
export default DocumentsView;

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

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const ExecutiveCommandPage: any = ExecutiveSummaryPage;
ExecutiveCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default ExecutiveCommandPage;

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -231,28 +232,14 @@ const EditExpense_categoriesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Expense Category"
pluralLabel="Expense Categories"
listHref="/expense_categories/expense_categories-list"
viewHref={`/expense_categories/expense_categories-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -526,6 +513,7 @@ const EditExpense_categoriesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/expense_categories/expense_categories-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,546 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/expense_categories/expense_categoriesSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/expense_categories/expense_categoriesSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Expense_categoriesView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { expense_categories } = useAppSelector((state) => state.expense_categories)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View expense_categories')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View expense_categories')} main>
<BaseButton
color='info'
label='Edit'
href={`/expense_categories/expense_categories-edit/?id=${id}`}
const ExpenseCategoriesView = () => (
<EntityRecordViewPage
singularLabel="Expense Category"
pluralLabel="Expense Categories"
stateKey="expense_categories"
recordKey="expense_categories"
fetchRecord={fetch}
listHref="/expense_categories/expense_categories-list"
editHref={(id) => `/expense_categories/expense_categories-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{expense_categories?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ExpenseCategory</p>
<p>{expense_categories?.name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Code</p>
<p>{expense_categories?.code}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={expense_categories?.description} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{expense_categories?.status ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Invoices ExpenseCategory</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>InvoiceNumber</th>
<th>InvoiceDate</th>
<th>ReceivedAt</th>
<th>DueDate</th>
<th>SubtotalAmount</th>
<th>TaxAmount</th>
<th>TotalAmount</th>
<th>Currency</th>
<th>Status</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{expense_categories.invoices_expense_category && Array.isArray(expense_categories.invoices_expense_category) &&
expense_categories.invoices_expense_category.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/invoices/invoices-view/?id=${item.id}`)}>
<td data-label="invoice_number">
{ item.invoice_number }
</td>
<td data-label="invoice_date">
{ dataFormatter.dateTimeFormatter(item.invoice_date) }
</td>
<td data-label="received_at">
{ dataFormatter.dateTimeFormatter(item.received_at) }
</td>
<td data-label="due_date">
{ dataFormatter.dateTimeFormatter(item.due_date) }
</td>
<td data-label="subtotal_amount">
{ item.subtotal_amount }
</td>
<td data-label="tax_amount">
{ item.tax_amount }
</td>
<td data-label="total_amount">
{ item.total_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="notes">
{ item.notes }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!expense_categories?.invoices_expense_category?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/expense_categories/expense_categories-list')}
/>
</CardBox>
</SectionMain>
</>
);
ExpenseCategoriesView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_EXPENSE_CATEGORIES'}>{page}</LayoutAuthenticated>;
};
Expense_categoriesView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_EXPENSE_CATEGORIES'}
>
{page}
</LayoutAuthenticated>
)
}
export default Expense_categoriesView;
export default ExpenseCategoriesView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -371,29 +372,14 @@ const EditField_verificationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Project' labelFor='project'>
<EnhancedEntityEditShell
entityLabel="Field Verification"
pluralLabel="Field Verifications"
listHref="/field_verifications/field_verifications-list"
viewHref={`/field_verifications/field_verifications-view/?id=${id}`}
record={initialValues}
>
<FormField label='Project' labelFor='project'>
<Field
name='project'
id='project'
@ -1088,6 +1074,7 @@ const EditField_verificationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/field_verifications/field_verifications-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,790 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/field_verifications/field_verificationsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/field_verifications/field_verificationsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Field_verificationsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { field_verifications } = useAppSelector((state) => state.field_verifications)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View field_verifications')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View field_verifications')} main>
<BaseButton
color='info'
label='Edit'
href={`/field_verifications/field_verifications-edit/?id=${id}`}
const FieldVerificationsView = () => (
<EntityRecordViewPage
singularLabel="Field Verification"
pluralLabel="Field Verifications"
stateKey="field_verifications"
recordKey="field_verifications"
fetchRecord={fetch}
listHref="/field_verifications/field_verifications-list"
editHref={(id) => `/field_verifications/field_verifications-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Project</p>
<p>{field_verifications?.project?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>VerifiedBy</p>
<p>{field_verifications?.verified_by_user?.firstName ?? 'No data'}</p>
</div>
<FormField label='VisitDate'>
{field_verifications.visit_date ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={field_verifications.visit_date ?
new Date(
dayjs(field_verifications.visit_date).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No VisitDate</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>VerificationType</p>
<p>{field_verifications?.verification_type ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Outcome</p>
<p>{field_verifications?.outcome ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Findings</p>
{field_verifications.findings
? <p dangerouslySetInnerHTML={{__html: field_verifications.findings}}/>
: <p>No data</p>
}
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={field_verifications?.actions_required} />
</FormField>
<FormField label='NextFollow-upAt'>
{field_verifications.next_followup_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={field_verifications.next_followup_at ?
new Date(
dayjs(field_verifications.next_followup_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No NextFollow-upAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Photos</p>
{field_verifications?.photos?.length
? (
<ImageField
name={'photos'}
image={field_verifications?.photos}
className='w-20 h-20'
/>
) : <p>No Photos</p>
}
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{field_verifications?.organizations?.name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/field_verifications/field_verifications-list')}
/>
</CardBox>
</SectionMain>
</>
);
FieldVerificationsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_FIELD_VERIFICATIONS'}>{page}</LayoutAuthenticated>;
};
Field_verificationsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_FIELD_VERIFICATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Field_verificationsView;
export default FieldVerificationsView;

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const FinancialControlPage: any = ExecutiveSummaryPage;
FinancialControlPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default FinancialControlPage;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -259,28 +260,14 @@ const EditFiscal_yearsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Fiscal Year"
pluralLabel="Fiscal Years"
listHref="/fiscal_years/fiscal_years-list"
viewHref={`/fiscal_years/fiscal_years-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -609,6 +596,7 @@ const EditFiscal_yearsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/fiscal_years/fiscal_years-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,964 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/fiscal_years/fiscal_yearsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/fiscal_years/fiscal_yearsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Fiscal_yearsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { fiscal_years } = useAppSelector((state) => state.fiscal_years)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View fiscal_years')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View fiscal_years')} main>
<BaseButton
color='info'
label='Edit'
href={`/fiscal_years/fiscal_years-edit/?id=${id}`}
const FiscalYearsView = () => (
<EntityRecordViewPage
singularLabel="Fiscal Year"
pluralLabel="Fiscal Years"
stateKey="fiscal_years"
recordKey="fiscal_years"
fetchRecord={fetch}
listHref="/fiscal_years/fiscal_years-list"
editHref={(id) => `/fiscal_years/fiscal_years-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{fiscal_years?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FiscalYear</p>
<p>{fiscal_years?.name}</p>
</div>
<FormField label='StartDate'>
{fiscal_years.start_date ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={fiscal_years.start_date ?
new Date(
dayjs(fiscal_years.start_date).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No StartDate</p>}
</FormField>
<FormField label='EndDate'>
{fiscal_years.end_date ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={fiscal_years.end_date ?
new Date(
dayjs(fiscal_years.end_date).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No EndDate</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{fiscal_years?.status ?? 'No data'}</p>
</div>
<FormField label='IsCurrent'>
<SwitchField
field={{name: 'is_current', value: fiscal_years?.is_current}}
form={{setFieldValue: () => null}}
disabled
/>
</FormField>
<>
<p className={'block font-bold mb-2'}>Budget_programs FiscalYear</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>ProgramName</th>
<th>ProgramCode</th>
<th>Objective</th>
<th>Status</th>
<th>ApprovedAmount</th>
<th>Currency</th>
</tr>
</thead>
<tbody>
{fiscal_years.budget_programs_fiscal_year && Array.isArray(fiscal_years.budget_programs_fiscal_year) &&
fiscal_years.budget_programs_fiscal_year.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/budget_programs/budget_programs-view/?id=${item.id}`)}>
<td data-label="name">
{ item.name }
</td>
<td data-label="code">
{ item.code }
</td>
<td data-label="objective">
{ item.objective }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="approved_amount">
{ item.approved_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!fiscal_years?.budget_programs_fiscal_year?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Budget_reallocations FiscalYear</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>Amount</th>
<th>Currency</th>
<th>Status</th>
<th>RequestedAt</th>
<th>Justification</th>
</tr>
</thead>
<tbody>
{fiscal_years.budget_reallocations_fiscal_year && Array.isArray(fiscal_years.budget_reallocations_fiscal_year) &&
fiscal_years.budget_reallocations_fiscal_year.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/budget_reallocations/budget_reallocations-view/?id=${item.id}`)}>
<td data-label="amount">
{ item.amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="requested_at">
{ dataFormatter.dateTimeFormatter(item.requested_at) }
</td>
<td data-label="justification">
{ item.justification }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!fiscal_years?.budget_reallocations_fiscal_year?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Procurement_plans FiscalYear</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>PlanName</th>
<th>Status</th>
<th>EstimatedTotalAmount</th>
<th>Currency</th>
<th>SubmittedAt</th>
<th>ApprovedAt</th>
</tr>
</thead>
<tbody>
{fiscal_years.procurement_plans_fiscal_year && Array.isArray(fiscal_years.procurement_plans_fiscal_year) &&
fiscal_years.procurement_plans_fiscal_year.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/procurement_plans/procurement_plans-view/?id=${item.id}`)}>
<td data-label="name">
{ item.name }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="estimated_total_amount">
{ item.estimated_total_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="submitted_at">
{ dataFormatter.dateTimeFormatter(item.submitted_at) }
</td>
<td data-label="approved_at">
{ dataFormatter.dateTimeFormatter(item.approved_at) }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!fiscal_years?.procurement_plans_fiscal_year?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Programs FiscalYear</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>ProgramName</th>
<th>ProgramCode</th>
<th>Status</th>
<th>BudgetAmount</th>
<th>Currency</th>
</tr>
</thead>
<tbody>
{fiscal_years.programs_fiscal_year && Array.isArray(fiscal_years.programs_fiscal_year) &&
fiscal_years.programs_fiscal_year.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/programs/programs-view/?id=${item.id}`)}>
<td data-label="name">
{ item.name }
</td>
<td data-label="code">
{ item.code }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="budget_amount">
{ item.budget_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!fiscal_years?.programs_fiscal_year?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Ledger_entries FiscalYear</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>EntryReference</th>
<th>EntryDate</th>
<th>EntryType</th>
<th>Currency</th>
<th>DebitAmount</th>
<th>CreditAmount</th>
<th>Description</th>
<th>RecordType</th>
<th>RecordKey</th>
</tr>
</thead>
<tbody>
{fiscal_years.ledger_entries_fiscal_year && Array.isArray(fiscal_years.ledger_entries_fiscal_year) &&
fiscal_years.ledger_entries_fiscal_year.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/ledger_entries/ledger_entries-view/?id=${item.id}`)}>
<td data-label="entry_reference">
{ item.entry_reference }
</td>
<td data-label="entry_date">
{ dataFormatter.dateTimeFormatter(item.entry_date) }
</td>
<td data-label="entry_type">
{ item.entry_type }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="debit_amount">
{ item.debit_amount }
</td>
<td data-label="credit_amount">
{ item.credit_amount }
</td>
<td data-label="description">
{ item.description }
</td>
<td data-label="record_type">
{ item.record_type }
</td>
<td data-label="record_key">
{ item.record_key }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!fiscal_years?.ledger_entries_fiscal_year?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/fiscal_years/fiscal_years-list')}
/>
</CardBox>
</SectionMain>
</>
);
FiscalYearsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_FISCAL_YEARS'}>{page}</LayoutAuthenticated>;
};
Fiscal_yearsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_FISCAL_YEARS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Fiscal_yearsView;
export default FiscalYearsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -315,28 +316,14 @@ const EditFunding_sourcesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Funding Source"
pluralLabel="Funding Sources"
listHref="/funding_sources/funding_sources-list"
viewHref={`/funding_sources/funding_sources-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -741,6 +728,7 @@ const EditFunding_sourcesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/funding_sources/funding_sources-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,696 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/funding_sources/funding_sourcesSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/funding_sources/funding_sourcesSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Funding_sourcesView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { funding_sources } = useAppSelector((state) => state.funding_sources)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View funding_sources')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View funding_sources')} main>
<BaseButton
color='info'
label='Edit'
href={`/funding_sources/funding_sources-edit/?id=${id}`}
const FundingSourcesView = () => (
<EntityRecordViewPage
singularLabel="Funding Source"
pluralLabel="Funding Sources"
stateKey="funding_sources"
recordKey="funding_sources"
fetchRecord={fetch}
listHref="/funding_sources/funding_sources-list"
editHref={(id) => `/funding_sources/funding_sources-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{funding_sources?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FundingSourceName</p>
<p>{funding_sources?.name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>SourceType</p>
<p>{funding_sources?.source_type ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ReferenceCode</p>
<p>{funding_sources?.reference_code}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{funding_sources?.currency ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>TotalCommittedAmount</p>
<p>{funding_sources?.total_committed_amount || 'No data'}</p>
</div>
<FormField label='EffectiveDate'>
{funding_sources.effective_date ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={funding_sources.effective_date ?
new Date(
dayjs(funding_sources.effective_date).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No EffectiveDate</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{funding_sources?.status ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Budget_programs FundingSource</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>ProgramName</th>
<th>ProgramCode</th>
<th>Objective</th>
<th>Status</th>
<th>ApprovedAmount</th>
<th>Currency</th>
</tr>
</thead>
<tbody>
{funding_sources.budget_programs_funding_source && Array.isArray(funding_sources.budget_programs_funding_source) &&
funding_sources.budget_programs_funding_source.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/budget_programs/budget_programs-view/?id=${item.id}`)}>
<td data-label="name">
{ item.name }
</td>
<td data-label="code">
{ item.code }
</td>
<td data-label="objective">
{ item.objective }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="approved_amount">
{ item.approved_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!funding_sources?.budget_programs_funding_source?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Programs FundingSource</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>ProgramName</th>
<th>ProgramCode</th>
<th>Status</th>
<th>BudgetAmount</th>
<th>Currency</th>
</tr>
</thead>
<tbody>
{funding_sources.programs_funding_source && Array.isArray(funding_sources.programs_funding_source) &&
funding_sources.programs_funding_source.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/programs/programs-view/?id=${item.id}`)}>
<td data-label="name">
{ item.name }
</td>
<td data-label="code">
{ item.code }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="budget_amount">
{ item.budget_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!funding_sources?.programs_funding_source?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/funding_sources/funding_sources-list')}
/>
</CardBox>
</SectionMain>
</>
);
FundingSourcesView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_FUNDING_SOURCES'}>{page}</LayoutAuthenticated>;
};
Funding_sourcesView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_FUNDING_SOURCES'}
>
{page}
</LayoutAuthenticated>
)
}
export default Funding_sourcesView;
export default FundingSourcesView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,29 +344,14 @@ const EditGrant_applicationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Grant' labelFor='grant'>
<EnhancedEntityEditShell
entityLabel="Grant Application"
pluralLabel="Grant Applications"
listHref="/grant_applications/grant_applications-list"
viewHref={`/grant_applications/grant_applications-view/?id=${id}`}
record={initialValues}
>
<FormField label='Grant' labelFor='grant'>
<Field
name='grant'
id='grant'
@ -1020,6 +1006,7 @@ const EditGrant_applicationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/grant_applications/grant_applications-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,919 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/grant_applications/grant_applicationsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/grant_applications/grant_applicationsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Grant_applicationsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { grant_applications } = useAppSelector((state) => state.grant_applications)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View grant_applications')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View grant_applications')} main>
<BaseButton
color='info'
label='Edit'
href={`/grant_applications/grant_applications-edit/?id=${id}`}
const GrantApplicationsView = () => (
<EntityRecordViewPage
singularLabel="Grant Application"
pluralLabel="Grant Applications"
stateKey="grant_applications"
recordKey="grant_applications"
fetchRecord={fetch}
listHref="/grant_applications/grant_applications-list"
editHref={(id) => `/grant_applications/grant_applications-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Grant</p>
<p>{grant_applications?.grant?.call_reference ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Beneficiary</p>
<p>{grant_applications?.beneficiary?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ApplicationReference</p>
<p>{grant_applications?.application_reference}</p>
</div>
<FormField label='SubmittedAt'>
{grant_applications.submitted_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={grant_applications.submitted_at ?
new Date(
dayjs(grant_applications.submitted_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No SubmittedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RequestedAmount</p>
<p>{grant_applications?.requested_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{grant_applications?.currency ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ProposalSummary</p>
{grant_applications.proposal_summary
? <p dangerouslySetInnerHTML={{__html: grant_applications.proposal_summary}}/>
: <p>No data</p>
}
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{grant_applications?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{grant_applications?.organizations?.name ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Grant_evaluations Application</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>Score</th>
<th>Comments</th>
<th>Recommendation</th>
<th>EvaluatedAt</th>
</tr>
</thead>
<tbody>
{grant_applications.grant_evaluations_application && Array.isArray(grant_applications.grant_evaluations_application) &&
grant_applications.grant_evaluations_application.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/grant_evaluations/grant_evaluations-view/?id=${item.id}`)}>
<td data-label="score">
{ item.score }
</td>
<td data-label="comments">
{ item.comments }
</td>
<td data-label="recommendation">
{ item.recommendation }
</td>
<td data-label="evaluated_at">
{ dataFormatter.dateTimeFormatter(item.evaluated_at) }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!grant_applications?.grant_evaluations_application?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Grant_tranches Application</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>TrancheNumber</th>
<th>Amount</th>
<th>Currency</th>
<th>PlannedDisbursementAt</th>
<th>DisbursedAt</th>
<th>Status</th>
<th>Conditions</th>
</tr>
</thead>
<tbody>
{grant_applications.grant_tranches_application && Array.isArray(grant_applications.grant_tranches_application) &&
grant_applications.grant_tranches_application.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/grant_tranches/grant_tranches-view/?id=${item.id}`)}>
<td data-label="tranche_number">
{ item.tranche_number }
</td>
<td data-label="amount">
{ item.amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="planned_disbursement_at">
{ dataFormatter.dateTimeFormatter(item.planned_disbursement_at) }
</td>
<td data-label="disbursed_at">
{ dataFormatter.dateTimeFormatter(item.disbursed_at) }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="conditions">
{ item.conditions }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!grant_applications?.grant_tranches_application?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/grant_applications/grant_applications-list')}
/>
</CardBox>
</SectionMain>
</>
);
GrantApplicationsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_GRANT_APPLICATIONS'}>{page}</LayoutAuthenticated>;
};
Grant_applicationsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_GRANT_APPLICATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Grant_applicationsView;
export default GrantApplicationsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -287,29 +288,14 @@ const EditGrant_evaluationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Application' labelFor='application'>
<EnhancedEntityEditShell
entityLabel="Grant Evaluation"
pluralLabel="Grant Evaluations"
listHref="/grant_evaluations/grant_evaluations-list"
viewHref={`/grant_evaluations/grant_evaluations-view/?id=${id}`}
record={initialValues}
>
<FormField label='Application' labelFor='application'>
<Field
name='application'
id='application'
@ -875,6 +861,7 @@ const EditGrant_evaluationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/grant_evaluations/grant_evaluations-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,675 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/grant_evaluations/grant_evaluationsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/grant_evaluations/grant_evaluationsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Grant_evaluationsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { grant_evaluations } = useAppSelector((state) => state.grant_evaluations)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View grant_evaluations')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View grant_evaluations')} main>
<BaseButton
color='info'
label='Edit'
href={`/grant_evaluations/grant_evaluations-edit/?id=${id}`}
const GrantEvaluationsView = () => (
<EntityRecordViewPage
singularLabel="Grant Evaluation"
pluralLabel="Grant Evaluations"
stateKey="grant_evaluations"
recordKey="grant_evaluations"
fetchRecord={fetch}
listHref="/grant_evaluations/grant_evaluations-list"
editHref={(id) => `/grant_evaluations/grant_evaluations-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Application</p>
<p>{grant_evaluations?.application?.application_reference ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Evaluator</p>
<p>{grant_evaluations?.evaluator_user?.firstName ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Score</p>
<p>{grant_evaluations?.score || 'No data'}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={grant_evaluations?.comments} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Recommendation</p>
<p>{grant_evaluations?.recommendation ?? 'No data'}</p>
</div>
<FormField label='EvaluatedAt'>
{grant_evaluations.evaluated_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={grant_evaluations.evaluated_at ?
new Date(
dayjs(grant_evaluations.evaluated_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No EvaluatedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{grant_evaluations?.organizations?.name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/grant_evaluations/grant_evaluations-list')}
/>
</CardBox>
</SectionMain>
</>
);
GrantEvaluationsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_GRANT_EVALUATIONS'}>{page}</LayoutAuthenticated>;
};
Grant_evaluationsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_GRANT_EVALUATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Grant_evaluationsView;
export default GrantEvaluationsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,29 +344,14 @@ const EditGrant_tranchesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Application' labelFor='application'>
<EnhancedEntityEditShell
entityLabel="Grant Tranche"
pluralLabel="Grant Tranches"
listHref="/grant_tranches/grant_tranches-list"
viewHref={`/grant_tranches/grant_tranches-view/?id=${id}`}
record={initialValues}
>
<FormField label='Application' labelFor='application'>
<Field
name='application'
id='application'
@ -914,6 +900,7 @@ const EditGrant_tranchesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/grant_tranches/grant_tranches-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,645 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/grant_tranches/grant_tranchesSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/grant_tranches/grant_tranchesSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Grant_tranchesView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { grant_tranches } = useAppSelector((state) => state.grant_tranches)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View grant_tranches')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View grant_tranches')} main>
<BaseButton
color='info'
label='Edit'
href={`/grant_tranches/grant_tranches-edit/?id=${id}`}
const GrantTranchesView = () => (
<EntityRecordViewPage
singularLabel="Grant Tranche"
pluralLabel="Grant Tranches"
stateKey="grant_tranches"
recordKey="grant_tranches"
fetchRecord={fetch}
listHref="/grant_tranches/grant_tranches-list"
editHref={(id) => `/grant_tranches/grant_tranches-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Application</p>
<p>{grant_tranches?.application?.application_reference ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>TrancheNumber</p>
<p>{grant_tranches?.tranche_number || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Amount</p>
<p>{grant_tranches?.amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{grant_tranches?.currency ?? 'No data'}</p>
</div>
<FormField label='PlannedDisbursementAt'>
{grant_tranches.planned_disbursement_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={grant_tranches.planned_disbursement_at ?
new Date(
dayjs(grant_tranches.planned_disbursement_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No PlannedDisbursementAt</p>}
</FormField>
<FormField label='DisbursedAt'>
{grant_tranches.disbursed_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={grant_tranches.disbursed_at ?
new Date(
dayjs(grant_tranches.disbursed_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No DisbursedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{grant_tranches?.status ?? 'No data'}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={grant_tranches?.conditions} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{grant_tranches?.organizations?.name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/grant_tranches/grant_tranches-list')}
/>
</CardBox>
</SectionMain>
</>
);
GrantTranchesView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_GRANT_TRANCHES'}>{page}</LayoutAuthenticated>;
};
Grant_tranchesView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_GRANT_TRANCHES'}
>
{page}
</LayoutAuthenticated>
)
}
export default Grant_tranchesView;
export default GrantTranchesView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -399,28 +400,14 @@ const EditGrantsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Grant"
pluralLabel="Grants"
listHref="/grants/grants-list"
viewHref={`/grants/grants-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1046,6 +1033,7 @@ const EditGrantsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/grants/grants-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,926 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/grants/grantsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/grants/grantsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const GrantsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { grants } = useAppSelector((state) => state.grants)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View grants')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View grants')} main>
<BaseButton
color='info'
label='Edit'
href={`/grants/grants-edit/?id=${id}`}
const GrantsView = () => (
<EntityRecordViewPage
singularLabel="Grant"
pluralLabel="Grants"
stateKey="grants"
recordKey="grants"
fetchRecord={fetch}
listHref="/grants/grants-list"
editHref={(id) => `/grants/grants-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{grants?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Program</p>
<p>{grants?.program?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FundingWindowName</p>
<p>{grants?.funding_window_name}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>CallReference</p>
<p>{grants?.call_reference}</p>
</div>
<FormField label='CallOpenAt'>
{grants.call_open_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={grants.call_open_at ?
new Date(
dayjs(grants.call_open_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No CallOpenAt</p>}
</FormField>
<FormField label='CallCloseAt'>
{grants.call_close_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={grants.call_close_at ?
new Date(
dayjs(grants.call_close_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No CallCloseAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{grants?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>EligibilityRules</p>
{grants.eligibility_rules
? <p dangerouslySetInnerHTML={{__html: grants.eligibility_rules}}/>
: <p>No data</p>
}
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>TotalEnvelopeAmount</p>
<p>{grants?.total_envelope_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{grants?.currency ?? 'No data'}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={grants?.notes} />
</FormField>
<>
<p className={'block font-bold mb-2'}>Beneficiaries Grant</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>BeneficiaryName</th>
<th>BeneficiaryType</th>
<th>RegistrationNumber</th>
<th>ContactEmail</th>
<th>ContactPhone</th>
<th>ApprovedAmount</th>
<th>Currency</th>
<th>Status</th>
<th>ApprovedAt</th>
</tr>
</thead>
<tbody>
{grants.beneficiaries_grant && Array.isArray(grants.beneficiaries_grant) &&
grants.beneficiaries_grant.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/beneficiaries/beneficiaries-view/?id=${item.id}`)}>
<td data-label="name">
{ item.name }
</td>
<td data-label="beneficiary_type">
{ item.beneficiary_type }
</td>
<td data-label="registration_number">
{ item.registration_number }
</td>
<td data-label="contact_email">
{ item.contact_email }
</td>
<td data-label="contact_phone">
{ item.contact_phone }
</td>
<td data-label="approved_amount">
{ item.approved_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="approved_at">
{ dataFormatter.dateTimeFormatter(item.approved_at) }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!grants?.beneficiaries_grant?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Grant_applications Grant</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>ApplicationReference</th>
<th>SubmittedAt</th>
<th>RequestedAmount</th>
<th>Currency</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{grants.grant_applications_grant && Array.isArray(grants.grant_applications_grant) &&
grants.grant_applications_grant.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/grant_applications/grant_applications-view/?id=${item.id}`)}>
<td data-label="application_reference">
{ item.application_reference }
</td>
<td data-label="submitted_at">
{ dataFormatter.dateTimeFormatter(item.submitted_at) }
</td>
<td data-label="requested_amount">
{ item.requested_amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="status">
{ item.status }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!grants?.grant_applications_grant?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/grants/grants-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
GrantsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_GRANTS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_GRANTS'}>{page}</LayoutAuthenticated>;
};
export default GrantsView;

View File

@ -1,166 +1,222 @@
import React, { useEffect, useState } from 'react';
import {
mdiBankOutline,
mdiCheckDecagramOutline,
mdiClipboardClockOutline,
mdiFileDocumentOutline,
mdiLogin,
mdiMapMarkerRadiusOutline,
mdiOfficeBuildingCogOutline,
mdiShieldCheckOutline,
mdiWalletOutline,
} from '@mdi/js';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useMemo } from 'react';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import { getLoginRoute, getWorkspaceRoute } from '../helpers/workspace';
import LayoutGuest from '../layouts/Guest';
import { findMe } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const moduleCards = [
{
title: 'Budgeting and fund allocation',
description: 'Approved allocations, commitment exposure, reallocations, and fiscal-year control across funding programs.',
icon: mdiBankOutline,
},
{
title: 'Procurement and approvals',
description: 'Requisition-to-award workflow with internal approvals, tender visibility, and contract handoff.',
icon: mdiClipboardClockOutline,
},
{
title: 'Contracts and vendors',
description: 'Vendor qualification, compliance review, contract expiry monitoring, and payment readiness.',
icon: mdiFileDocumentOutline,
},
{
title: 'Projects and rollout oversight',
description: 'Province-level implementation tracking for school broadband, digital centers, rural connectivity, and ICT programs.',
icon: mdiMapMarkerRadiusOutline,
},
];
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('background');
const textColor = useAppSelector((state) => state.style.linkColor);
const controlPoints = [
'Approval-aware procurement workflow from requisition to payment',
'Vendor compliance, document expiry, and audit-ready record trails',
'Executive summary with budget, contract, project, and risk visibility',
'Operational links into the existing admin interface and CRUD work areas',
];
const title = 'FDSU ERP'
export default function HomePage() {
const router = useRouter();
const dispatch = useAppDispatch();
const { currentUser, token } = useAppSelector((state) => state.auth);
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
const workspaceRoute = useMemo(
() => getWorkspaceRoute(currentUser?.app_role?.name),
[currentUser?.app_role?.name],
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
useEffect(() => {
const existingToken = token || (typeof window !== 'undefined' ? localStorage.getItem('token') : '');
if (existingToken && !currentUser?.id) {
dispatch(findMe());
}
};
}, [currentUser?.id, dispatch, token]);
useEffect(() => {
if (currentUser?.id) {
router.replace(workspaceRoute);
}
}, [currentUser?.id, router, workspaceRoute]);
const getProtectedHref = (href: string) => (currentUser?.id ? href : getLoginRoute(href));
const primaryWorkspaceHref = currentUser?.id ? workspaceRoute : getLoginRoute('/executive-summary');
const dashboardHref = currentUser?.id ? '/dashboard' : getLoginRoute('/dashboard');
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('FDSU ERP')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your FDSU ERP app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<div className='min-h-screen bg-slate-100 text-slate-900'>
<header className='border-b border-slate-200 bg-white'>
<div className='mx-auto flex max-w-7xl items-center justify-between gap-4 px-6 py-4'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-500'>National public fund authority platform</p>
<h1 className='mt-1 text-2xl font-semibold tracking-tight text-slate-950'>FDSU ERP</h1>
</div>
<div className='flex flex-wrap items-center gap-3'>
<BaseButton href={currentUser?.id ? workspaceRoute : '/login'} color='whiteDark' icon={mdiLogin} label={currentUser?.id ? 'My workspace' : 'Login'} />
<BaseButton href={dashboardHref} color='info' icon={mdiOfficeBuildingCogOutline} label='Admin interface' />
</div>
</div>
</header>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
<main className='mx-auto max-w-7xl px-6 py-8'>
<section className='grid gap-6 xl:grid-cols-[1.55fr,1fr]'>
<CardBox className='border border-slate-200 bg-slate-950 text-slate-100'>
<div className='max-w-4xl'>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-400'>Institutional ERP foundation</p>
<h2 className='mt-4 text-4xl font-semibold tracking-tight text-white'>
Operational control for budgeting, procurement, contract management, compliance, and digital inclusion rollout.
</h2>
<p className='mt-5 max-w-3xl text-base leading-7 text-slate-300'>
FDSU ERP is designed as a serious working environment for finance, procurement, project delivery, internal control, audit, and executive leadership.
It centralizes recordkeeping, approvals, payment oversight, vendor qualification, contract obligations, and province-level implementation in one platform.
</p>
</BaseButtons>
<div className='mt-8 flex flex-wrap gap-3'>
<BaseButton href={primaryWorkspaceHref} color='info' label={currentUser?.id ? 'Open my workspace' : 'Open executive summary'} />
<BaseButton href={getProtectedHref('/requisitions/requisitions-new')} color='whiteDark' label='Create requisition' />
<BaseButton href={getProtectedHref('/approvals/approvals-list')} color='whiteDark' label='Open approval inbox' />
</div>
</div>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<CardBox className='border border-slate-200 bg-white'>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-500'>What the first delivery includes</p>
<div className='mt-5 space-y-3'>
{controlPoints.map((item) => (
<div key={item} className='flex items-start gap-3 rounded-md border border-slate-200 bg-slate-50 px-4 py-3'>
<span className='mt-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-slate-900 text-white'>
<BaseIcon path={mdiShieldCheckOutline} size={14} />
</span>
<p className='text-sm leading-6 text-slate-700'>{item}</p>
</div>
))}
</div>
<BaseDivider />
<div className='grid grid-cols-2 gap-3'>
<div className='rounded-md border border-slate-200 p-4'>
<p className='text-xs uppercase tracking-[0.18em] text-slate-400'>Interface style</p>
<p className='mt-2 text-sm font-medium text-slate-900'>Dense, audit-conscious, line-of-business layout</p>
</div>
<div className='rounded-md border border-slate-200 p-4'>
<p className='text-xs uppercase tracking-[0.18em] text-slate-400'>Access</p>
<p className='mt-2 text-sm font-medium text-slate-900'>Public landing with role-aware sign-in routing into the secured workspace</p>
</div>
</div>
</CardBox>
</section>
<section className='mt-8 grid gap-4 lg:grid-cols-2 xl:grid-cols-4'>
{moduleCards.map((card) => (
<CardBox key={card.title} className='h-full border border-slate-200 bg-white'>
<div className='flex items-start justify-between gap-3'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>ERP module</p>
<h3 className='mt-3 text-lg font-semibold text-slate-900'>{card.title}</h3>
<p className='mt-3 text-sm leading-6 text-slate-600'>{card.description}</p>
</div>
<div className='flex h-11 w-11 items-center justify-center rounded-md border border-slate-200 bg-slate-50'>
<BaseIcon path={card.icon} size={20} className='text-slate-700' />
</div>
</div>
</CardBox>
))}
</section>
<section className='mt-8 grid gap-6 xl:grid-cols-[1.2fr,1fr]'>
<CardBox className='border border-slate-200 bg-white'>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-500'>Operational workflow</p>
<h3 className='mt-2 text-2xl font-semibold text-slate-900'>Thin-slice journey now wired into the app</h3>
<div className='mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
{[
['1', 'Create requisition', 'Capture procurement demand and route it into the formal approval chain.'],
['2', 'Review approvals', 'Use the new executive summary and approval inbox to identify pending actions.'],
['3', 'Inspect contracts', 'Track contracts nearing expiry and top-value commitments in one place.'],
['4', 'Monitor rollout', 'Review project concentration by province and outstanding operational risk.'],
].map(([step, title, text]) => (
<div key={step} className='rounded-md border border-slate-200 bg-slate-50 p-4'>
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-slate-900 text-sm font-semibold text-white'>{step}</div>
<p className='mt-4 font-medium text-slate-900'>{title}</p>
<p className='mt-2 text-sm leading-6 text-slate-600'>{text}</p>
</div>
))}
</div>
</CardBox>
<CardBox className='border border-slate-200 bg-white'>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-500'>Quick access</p>
<div className='mt-5 grid gap-3'>
{[
{ href: currentUser?.id ? workspaceRoute : getLoginRoute('/executive-summary'), icon: mdiWalletOutline, title: 'Executive summary', text: 'Operational overview, approval queue, risk panel, and rollout indicators.' },
{ href: getProtectedHref('/requisitions/requisitions-list'), icon: mdiClipboardClockOutline, title: 'Requisitions', text: 'Create, list, and review procurement requests.' },
{ href: getProtectedHref('/contracts/contracts-list'), icon: mdiFileDocumentOutline, title: 'Contracts', text: 'Open the contract register and milestone detail views.' },
{ href: getProtectedHref('/vendors/vendors-list'), icon: mdiCheckDecagramOutline, title: 'Vendor master', text: 'Access qualification, banking data, compliance, and related history.' },
].map((item) => (
<Link key={item.href} href={item.href} className='rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50'>
<div className='flex items-start gap-3'>
<div className='mt-0.5 flex h-10 w-10 items-center justify-center rounded-md border border-slate-200 bg-slate-50'>
<BaseIcon path={item.icon} size={18} className='text-slate-700' />
</div>
<div>
<p className='font-medium text-slate-900'>{item.title}</p>
<p className='mt-1 text-sm leading-6 text-slate-600'>{item.text}</p>
</div>
</div>
</Link>
))}
</div>
</CardBox>
</section>
</main>
</div>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
HomePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -511,28 +512,14 @@ const EditInvoicesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Invoice"
pluralLabel="Invoices"
listHref="/invoices/invoices-list"
viewHref={`/invoices/invoices-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1628,6 +1615,7 @@ const EditInvoicesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/invoices/invoices-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -371,29 +372,14 @@ const EditIssuesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Project' labelFor='project'>
<EnhancedEntityEditShell
entityLabel="Issue"
pluralLabel="Issues"
listHref="/issues/issues-list"
viewHref={`/issues/issues-view/?id=${id}`}
record={initialValues}
>
<FormField label='Project' labelFor='project'>
<Field
name='project'
id='project'
@ -1094,6 +1080,7 @@ const EditIssuesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/issues/issues-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,791 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/issues/issuesSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/issues/issuesSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const IssuesView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { issues } = useAppSelector((state) => state.issues)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View issues')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View issues')} main>
<BaseButton
color='info'
label='Edit'
href={`/issues/issues-edit/?id=${id}`}
const IssuesView = () => (
<EntityRecordViewPage
singularLabel="Issue"
pluralLabel="Issues"
stateKey="issues"
recordKey="issues"
fetchRecord={fetch}
listHref="/issues/issues-list"
editHref={(id) => `/issues/issues-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Project</p>
<p>{issues?.project?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>IssueTitle</p>
<p>{issues?.title}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Description</p>
{issues.description
? <p dangerouslySetInnerHTML={{__html: issues.description}}/>
: <p>No data</p>
}
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Severity</p>
<p>{issues?.severity ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{issues?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>AssignedTo</p>
<p>{issues?.assigned_to_user?.firstName ?? 'No data'}</p>
</div>
<FormField label='OpenedAt'>
{issues.opened_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={issues.opened_at ?
new Date(
dayjs(issues.opened_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No OpenedAt</p>}
</FormField>
<FormField label='DueAt'>
{issues.due_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={issues.due_at ?
new Date(
dayjs(issues.due_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No DueAt</p>}
</FormField>
<FormField label='ResolvedAt'>
{issues.resolved_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={issues.resolved_at ?
new Date(
dayjs(issues.resolved_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No ResolvedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{issues?.organizations?.name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/issues/issues-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
IssuesView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_ISSUES'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_ISSUES'}>{page}</LayoutAuthenticated>;
};
export default IssuesView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -399,28 +400,14 @@ const EditLedger_entriesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Ledger Entry"
pluralLabel="Ledger Entries"
listHref="/ledger_entries/ledger_entries-list"
viewHref={`/ledger_entries/ledger_entries-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1038,6 +1025,7 @@ const EditLedger_entriesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/ledger_entries/ledger_entries-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,701 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/ledger_entries/ledger_entriesSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/ledger_entries/ledger_entriesSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Ledger_entriesView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { ledger_entries } = useAppSelector((state) => state.ledger_entries)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View ledger_entries')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View ledger_entries')} main>
<BaseButton
color='info'
label='Edit'
href={`/ledger_entries/ledger_entries-edit/?id=${id}`}
const LedgerEntriesView = () => (
<EntityRecordViewPage
singularLabel="Ledger Entry"
pluralLabel="Ledger Entries"
stateKey="ledger_entries"
recordKey="ledger_entries"
fetchRecord={fetch}
listHref="/ledger_entries/ledger_entries-list"
editHref={(id) => `/ledger_entries/ledger_entries-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{ledger_entries?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>FiscalYear</p>
<p>{ledger_entries?.fiscal_year?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>EntryReference</p>
<p>{ledger_entries?.entry_reference}</p>
</div>
<FormField label='EntryDate'>
{ledger_entries.entry_date ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={ledger_entries.entry_date ?
new Date(
dayjs(ledger_entries.entry_date).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No EntryDate</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>EntryType</p>
<p>{ledger_entries?.entry_type ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{ledger_entries?.currency ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>DebitAmount</p>
<p>{ledger_entries?.debit_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>CreditAmount</p>
<p>{ledger_entries?.credit_amount || 'No data'}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={ledger_entries?.description} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordType</p>
<p>{ledger_entries?.record_type}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordKey</p>
<p>{ledger_entries?.record_key}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/ledger_entries/ledger_entries-list')}
/>
</CardBox>
</SectionMain>
</>
);
LedgerEntriesView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_LEDGER_ENTRIES'}>{page}</LayoutAuthenticated>;
};
Ledger_entriesView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_LEDGER_ENTRIES'}
>
{page}
</LayoutAuthenticated>
)
}
export default Ledger_entriesView;
export default LedgerEntriesView;

View File

@ -21,6 +21,17 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link';
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();
@ -40,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'
@ -58,16 +71,18 @@ export default function Login() {
}, []);
// Fetch user data
useEffect(() => {
if (token) {
const existingToken = token || (typeof window !== 'undefined' ? localStorage.getItem('token') : '');
if (existingToken) {
dispatch(findMe());
}
}, [token, dispatch]);
// Redirect to dashboard if user is logged in
}, [dispatch, token]);
// Redirect to the intended deep link or the role workspace if user is logged in
useEffect(() => {
if (currentUser?.id) {
router.push('/dashboard');
router.replace(getPostLoginRoute(currentUser?.app_role?.name, router.query.redirect));
}
}, [currentUser?.id, router]);
}, [currentUser?.app_role?.name, currentUser?.id, router, router.query.redirect]);
// Show error message if there is one
useEffect(() => {
if (errorMessage){
@ -167,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
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>
<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={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'
@ -211,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>

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -343,28 +344,14 @@ const EditNotificationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Notification"
pluralLabel="Notifications"
listHref="/notifications/notifications-list"
viewHref={`/notifications/notifications-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -910,6 +897,7 @@ const EditNotificationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/notifications/notifications-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,640 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/notifications/notificationsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/notifications/notificationsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const NotificationsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { notifications } = useAppSelector((state) => state.notifications)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View notifications')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View notifications')} main>
<BaseButton
color='info'
label='Edit'
href={`/notifications/notifications-edit/?id=${id}`}
const NotificationsView = () => (
<EntityRecordViewPage
singularLabel="Notification"
pluralLabel="Notifications"
stateKey="notifications"
recordKey="notifications"
fetchRecord={fetch}
listHref="/notifications/notifications-list"
editHref={(id) => `/notifications/notifications-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{notifications?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>User</p>
<p>{notifications?.user?.firstName ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Type</p>
<p>{notifications?.type ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Title</p>
<p>{notifications?.title}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={notifications?.message} />
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordType</p>
<p>{notifications?.record_type}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RecordKey</p>
<p>{notifications?.record_key}</p>
</div>
<FormField label='Read'>
<SwitchField
field={{name: 'read', value: notifications?.read}}
form={{setFieldValue: () => null}}
disabled
/>
</FormField>
<FormField label='SentAt'>
{notifications.sent_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={notifications.sent_at ?
new Date(
dayjs(notifications.sent_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No SentAt</p>}
</FormField>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/notifications/notifications-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
NotificationsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_NOTIFICATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_NOTIFICATIONS'}>{page}</LayoutAuthenticated>;
};
export default NotificationsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -371,28 +372,14 @@ const EditObligationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Obligation"
pluralLabel="Obligations"
listHref="/obligations/obligations-list"
viewHref={`/obligations/obligations-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1178,6 +1165,7 @@ const EditObligationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/obligations/obligations-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,873 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/obligations/obligationsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/obligations/obligationsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const ObligationsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { obligations } = useAppSelector((state) => state.obligations)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View obligations')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View obligations')} main>
<BaseButton
color='info'
label='Edit'
href={`/obligations/obligations-edit/?id=${id}`}
const ObligationsView = () => (
<EntityRecordViewPage
singularLabel="Obligation"
pluralLabel="Obligations"
stateKey="obligations"
recordKey="obligations"
fetchRecord={fetch}
listHref="/obligations/obligations-list"
editHref={(id) => `/obligations/obligations-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{obligations?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>BudgetLine</p>
<p>{obligations?.budget_line?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Contract</p>
<p>{obligations?.contract?.contract_number ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Project</p>
<p>{obligations?.project?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ObligationNumber</p>
<p>{obligations?.obligation_number}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Amount</p>
<p>{obligations?.amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{obligations?.currency ?? 'No data'}</p>
</div>
<FormField label='ObligatedAt'>
{obligations.obligated_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={obligations.obligated_at ?
new Date(
dayjs(obligations.obligated_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No ObligatedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{obligations?.status ?? 'No data'}</p>
</div>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={obligations?.notes} />
</FormField>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/obligations/obligations-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
);
ObligationsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_OBLIGATIONS'}
>
{page}
</LayoutAuthenticated>
)
}
return <LayoutAuthenticated permission={'READ_OBLIGATIONS'}>{page}</LayoutAuthenticated>;
};
export default ObligationsView;

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const OperationsCommandPage: any = ExecutiveSummaryPage;
OperationsCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default OperationsCommandPage;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -119,10 +120,14 @@ const EditOrganizationsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField
<EnhancedEntityEditShell
entityLabel="Organization"
pluralLabel="Organizations"
listHref="/organizations/organizations-list"
viewHref={`/organizations/organizations-view/?id=${id}`}
record={initialValues}
>
<FormField
label="Name"
>
<Field
@ -165,6 +170,7 @@ const EditOrganizationsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/organizations/organizations-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -315,28 +316,14 @@ const EditPayment_batchesPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Payment Batch"
pluralLabel="Payment Batchs"
listHref="/payment_batches/payment_batches-list"
viewHref={`/payment_batches/payment_batches-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -853,6 +840,7 @@ const EditPayment_batchesPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/payment_batches/payment_batches-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,717 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/payment_batches/payment_batchesSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/payment_batches/payment_batchesSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Payment_batchesView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { payment_batches } = useAppSelector((state) => state.payment_batches)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View payment_batches')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View payment_batches')} main>
<BaseButton
color='info'
label='Edit'
href={`/payment_batches/payment_batches-edit/?id=${id}`}
const PaymentBatchesView = () => (
<EntityRecordViewPage
singularLabel="Payment Batch"
pluralLabel="Payment Batchs"
stateKey="payment_batches"
recordKey="payment_batches"
fetchRecord={fetch}
listHref="/payment_batches/payment_batches-list"
editHref={(id) => `/payment_batches/payment_batches-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{payment_batches?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>BatchNumber</p>
<p>{payment_batches?.batch_number}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{payment_batches?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{payment_batches?.currency ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>TotalAmount</p>
<p>{payment_batches?.total_amount || 'No data'}</p>
</div>
<FormField label='ScheduledAt'>
{payment_batches.scheduled_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={payment_batches.scheduled_at ?
new Date(
dayjs(payment_batches.scheduled_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No ScheduledAt</p>}
</FormField>
<FormField label='ProcessedAt'>
{payment_batches.processed_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={payment_batches.processed_at ?
new Date(
dayjs(payment_batches.processed_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No ProcessedAt</p>}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>PreparedBy</p>
<p>{payment_batches?.prepared_by_user?.firstName ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Payments PaymentBatch</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>PaymentReference</th>
<th>PaymentDate</th>
<th>Amount</th>
<th>Currency</th>
<th>Status</th>
<th>FailureReason</th>
</tr>
</thead>
<tbody>
{payment_batches.payments_batch && Array.isArray(payment_batches.payments_batch) &&
payment_batches.payments_batch.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/payments/payments-view/?id=${item.id}`)}>
<td data-label="payment_reference">
{ item.payment_reference }
</td>
<td data-label="payment_date">
{ dataFormatter.dateTimeFormatter(item.payment_date) }
</td>
<td data-label="amount">
{ item.amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="failure_reason">
{ item.failure_reason }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!payment_batches?.payments_batch?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/payment_batches/payment_batches-list')}
/>
</CardBox>
</SectionMain>
</>
);
PaymentBatchesView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_PAYMENT_BATCHES'}>{page}</LayoutAuthenticated>;
};
Payment_batchesView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_PAYMENT_BATCHES'}
>
{page}
</LayoutAuthenticated>
)
}
export default Payment_batchesView;
export default PaymentBatchesView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -371,28 +372,14 @@ const EditPayment_requestsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Payment Request"
pluralLabel="Payment Requests"
listHref="/payment_requests/payment_requests-list"
viewHref={`/payment_requests/payment_requests-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1086,6 +1073,7 @@ const EditPayment_requestsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/payment_requests/payment_requests-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

View File

@ -1,874 +1,22 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import dayjs from "dayjs";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import {useRouter} from "next/router";
import { fetch } from '../../stores/payment_requests/payment_requestsSlice'
import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from "../../components/ImageField";
import LayoutAuthenticated from "../../layouts/Authenticated";
import {getPageTitle} from "../../config";
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
import SectionMain from "../../components/SectionMain";
import CardBox from "../../components/CardBox";
import BaseButton from "../../components/BaseButton";
import BaseDivider from "../../components/BaseDivider";
import {mdiChartTimelineVariant} from "@mdi/js";
import {SwitchField} from "../../components/SwitchField";
import FormField from "../../components/FormField";
import React, { ReactElement } from 'react';
import LayoutAuthenticated from '../../layouts/Authenticated';
import { fetch } from '../../stores/payment_requests/payment_requestsSlice';
import EntityRecordViewPage from '../../components/EntityPage/EntityRecordViewPage';
import {hasPermission} from "../../helpers/userPermissions";
const Payment_requestsView = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { payment_requests } = useAppSelector((state) => state.payment_requests)
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View payment_requests')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View payment_requests')} main>
<BaseButton
color='info'
label='Edit'
href={`/payment_requests/payment_requests-edit/?id=${id}`}
const PaymentRequestsView = () => (
<EntityRecordViewPage
singularLabel="Payment Request"
pluralLabel="Payment Requests"
stateKey="payment_requests"
recordKey="payment_requests"
fetchRecord={fetch}
listHref="/payment_requests/payment_requests-list"
editHref={(id) => `/payment_requests/payment_requests-edit/?id=${id ?? ''}`}
/>
</SectionTitleLineWithButton>
<CardBox>
);
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Organization</p>
<p>{payment_requests?.organization?.name ?? 'No data'}</p>
</div>
}
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Invoice</p>
<p>{payment_requests?.invoice?.invoice_number ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RequestNumber</p>
<p>{payment_requests?.request_number}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RequestedAmount</p>
<p>{payment_requests?.requested_amount || 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Currency</p>
<p>{payment_requests?.currency ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>PaymentMethod</p>
<p>{payment_requests?.payment_method ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{payment_requests?.status ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>RequestedBy</p>
<p>{payment_requests?.requested_by_user?.firstName ?? 'No data'}</p>
</div>
<FormField label='RequestedAt'>
{payment_requests.requested_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
selected={payment_requests.requested_at ?
new Date(
dayjs(payment_requests.requested_at).format('YYYY-MM-DD hh:mm'),
) : null
}
disabled
/> : <p>No RequestedAt</p>}
</FormField>
<FormField label='Multi Text' hasTextareaHeight>
<textarea className={'w-full'} disabled value={payment_requests?.justification} />
</FormField>
<>
<p className={'block font-bold mb-2'}>Payments PaymentRequest</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>PaymentReference</th>
<th>PaymentDate</th>
<th>Amount</th>
<th>Currency</th>
<th>Status</th>
<th>FailureReason</th>
</tr>
</thead>
<tbody>
{payment_requests.payments_payment_request && Array.isArray(payment_requests.payments_payment_request) &&
payment_requests.payments_payment_request.map((item: any) => (
<tr key={item.id} onClick={() => router.push(`/payments/payments-view/?id=${item.id}`)}>
<td data-label="payment_reference">
{ item.payment_reference }
</td>
<td data-label="payment_date">
{ dataFormatter.dateTimeFormatter(item.payment_date) }
</td>
<td data-label="amount">
{ item.amount }
</td>
<td data-label="currency">
{ item.currency }
</td>
<td data-label="status">
{ item.status }
</td>
<td data-label="failure_reason">
{ item.failure_reason }
</td>
</tr>
))}
</tbody>
</table>
</div>
{!payment_requests?.payments_payment_request?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/payment_requests/payment_requests-list')}
/>
</CardBox>
</SectionMain>
</>
);
PaymentRequestsView.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_PAYMENT_REQUESTS'}>{page}</LayoutAuthenticated>;
};
Payment_requestsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_PAYMENT_REQUESTS'}
>
{page}
</LayoutAuthenticated>
)
}
export default Payment_requestsView;
export default PaymentRequestsView;

View File

@ -10,6 +10,7 @@ import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import EnhancedEntityEditShell from '../../components/EntityPage/EnhancedEntityEditShell'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
@ -371,28 +372,14 @@ const EditPaymentsPage = () => {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<EnhancedEntityEditShell
entityLabel="Payment"
pluralLabel="Payments"
listHref="/payments/payments-list"
viewHref={`/payments/payments-view/?id=${id}`}
record={initialValues}
>
{hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
<FormField label='Organization' labelFor='organization'>
<Field
name='organization'
@ -1178,6 +1165,7 @@ const EditPaymentsPage = () => {
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/payments/payments-list')}/>
</BaseButtons>
</EnhancedEntityEditShell>
</Form>
</Formik>
</CardBox>

Some files were not shown because too many files have changed in this diff Show More