Merge pull request 'Updated api usage, corrected table designs' (#109) from finishline into main
Reviewed-on: #109
This commit was merged in pull request #109.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1193,7 +1193,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/api/projects/beneficiaries/{projectId}": {
|
"/v1/api/projects/{projectId}/beneficiaries": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Projects"
|
"Projects"
|
||||||
@@ -1355,7 +1355,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/api/projects/changerequests/{projectId}": {
|
"/v1/api/projects/{projectId}/changerequests": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Projects"
|
"Projects"
|
||||||
@@ -1573,7 +1573,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/api/projects/issues/{projectId}": {
|
"/v1/api/projects/{projectId}/issues": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Projects"
|
"Projects"
|
||||||
@@ -2308,7 +2308,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/api/projects/sitevisits/{projectId}": {
|
"/v1/api/projects/{projectId}/sitevisits": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Projects"
|
"Projects"
|
||||||
@@ -2740,6 +2740,54 @@
|
|||||||
"security": [
|
"security": [
|
||||||
{}
|
{}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"Governance"
|
||||||
|
],
|
||||||
|
"description": "Updates a specific budget",
|
||||||
|
"operationId": "updatebudget",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateBudgetRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/api/grants/contracts": {
|
"/v1/api/grants/contracts": {
|
||||||
@@ -3770,6 +3818,66 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/api/governance/auditlog": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Governance"
|
||||||
|
],
|
||||||
|
"description": "Retrieves audit logs based on pagination parameters",
|
||||||
|
"operationId": "getauditlogs",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Pagination"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AuditLog"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/api/governance/instruments/{programmeId}": {
|
"/v1/api/governance/instruments/{programmeId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -3876,6 +3984,124 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/api/governance/organisations/{organisationId}/portfolio": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Governance"
|
||||||
|
],
|
||||||
|
"description": "Get all portfolios belonging to a specific organisation",
|
||||||
|
"operationId": "getorganisationportfolios",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "organisationId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Portfolio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v1/api/governance/portfolios/{portfolioId}/value": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Governance"
|
||||||
|
],
|
||||||
|
"description": "Calculates and returns a portfolio's total project's budget value",
|
||||||
|
"operationId": "getporfoliovalue",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "portfolioId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
|
"format": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/api/governance/portfolios/{portfolioId}": {
|
"/v1/api/governance/portfolios/{portfolioId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -3998,23 +4224,23 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/api/governance/organisations/{organisationId}/portfolio": {
|
"/v1/api/governance/organisations/portfolios": {
|
||||||
"get": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Governance"
|
"Governance"
|
||||||
],
|
],
|
||||||
"description": "Get all portfolios",
|
"description": "Get all portfolios",
|
||||||
"operationId": "getportfolios",
|
"operationId": "getportfolios",
|
||||||
"parameters": [
|
"requestBody": {
|
||||||
{
|
"content": {
|
||||||
"name": "organisationId",
|
"application/json; version=1": {
|
||||||
"in": "path",
|
"schema": {
|
||||||
"required": true,
|
"$ref": "#/components/schemas/Pagination"
|
||||||
"schema": {
|
}
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
"required": true
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -4104,6 +4330,71 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/api/governance/programmes/{programmeId}/value": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Governance"
|
||||||
|
],
|
||||||
|
"description": "Calculates and returns a programme's total project budget value",
|
||||||
|
"operationId": "getprogrammevalue",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "programmeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
|
"format": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"content": {
|
||||||
|
"application/json; version=1": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/api/governance/tenants/{tenantId}": {
|
"/v1/api/governance/tenants/{tenantId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -5935,6 +6226,63 @@
|
|||||||
"ApprovalStatus": {
|
"ApprovalStatus": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"AuditLog": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"pattern": "^-?(?:0|[1-9]\\d*)$",
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"tenantId": {
|
||||||
|
"pattern": "^-?(?:0|[1-9]\\d*)$",
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"integer",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"entityName": {
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entityId": {
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"changes": {
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"array"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"performedBy": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Award": {
|
"Award": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6158,6 +6506,9 @@
|
|||||||
"string"
|
"string"
|
||||||
],
|
],
|
||||||
"format": "double"
|
"format": "double"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/components/schemas/ApprovalStatus"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -8481,6 +8832,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"UpdateBudgetRequest": {
|
||||||
|
"required": [
|
||||||
|
"budgetId",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"budgetId": {
|
||||||
|
"pattern": "^-?(?:0|[1-9]\\d*)$",
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/components/schemas/ApprovalStatus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"UpdateChangeRequest": {
|
"UpdateChangeRequest": {
|
||||||
"required": [
|
"required": [
|
||||||
"changeRequestId",
|
"changeRequestId",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { Download, FileSpreadsheet, FileText, ChevronDown } from 'lucide-react';
|
import { Download, FileSpreadsheet, FileText, ChevronDown } from 'lucide-react';
|
||||||
import { ExportColumn, exportToSpreadsheet, exportToPdf } from '../../utils/exportUtils';
|
import { ExportColumn, exportToSpreadsheet, exportToPdf } from '../../utils/exportUtils';
|
||||||
import { useAuth } from '../../auth/AuthProvider';
|
import { useAuth } from '../../auth/AuthProvider';
|
||||||
@@ -25,24 +26,46 @@ export function ExportButton<T>({
|
|||||||
className = '',
|
className = '',
|
||||||
}: ExportButtonProps<T>) {
|
}: ExportButtonProps<T>) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [menuPosition, setMenuPosition] = useState<{ top: number; right: number } | null>(null);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const { user, claims, tenantId } = useAuth();
|
const { user, claims, tenantId } = useAuth();
|
||||||
|
|
||||||
const isDisabled = !data || data.length === 0;
|
const isDisabled = !data || data.length === 0;
|
||||||
|
|
||||||
|
const updatePosition = useCallback(() => {
|
||||||
|
if (!dropdownRef.current) return;
|
||||||
|
const rect = dropdownRef.current.getBoundingClientRect();
|
||||||
|
setMenuPosition({
|
||||||
|
top: rect.bottom + 8,
|
||||||
|
right: Math.max(16, window.innerWidth - rect.right),
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
const target = event.target as Node;
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(target) &&
|
||||||
|
(!menuRef.current || !menuRef.current.contains(target))
|
||||||
|
) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
updatePosition();
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
window.addEventListener('resize', updatePosition);
|
||||||
|
window.addEventListener('scroll', updatePosition, true);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
window.removeEventListener('resize', updatePosition);
|
||||||
|
window.removeEventListener('scroll', updatePosition, true);
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen, updatePosition]);
|
||||||
|
|
||||||
const handleExportSpreadsheet = () => {
|
const handleExportSpreadsheet = () => {
|
||||||
if (isDisabled) return;
|
if (isDisabled) return;
|
||||||
@@ -96,44 +119,56 @@ export function ExportButton<T>({
|
|||||||
<ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
<ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && !isDisabled && (
|
{isOpen && !isDisabled && menuPosition && typeof document !== 'undefined' &&
|
||||||
<div className="absolute right-0 mt-2 w-56 bg-[#0d121f] border border-slate-700/90 rounded-xl shadow-2xl z-[99999] overflow-hidden backdrop-blur-md animate-in fade-in-50 zoom-in-95 ring-1 ring-black/40">
|
createPortal(
|
||||||
<div className="p-2 border-b border-slate-800/80">
|
<div
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400 px-2 block">
|
ref={menuRef}
|
||||||
Export Format ({data.length} items)
|
style={{
|
||||||
</span>
|
position: 'fixed',
|
||||||
</div>
|
top: `${menuPosition.top}px`,
|
||||||
<div className="p-1 space-y-0.5">
|
right: `${menuPosition.right}px`,
|
||||||
<button
|
zIndex: 999999,
|
||||||
type="button"
|
}}
|
||||||
onClick={handleExportSpreadsheet}
|
className="w-56 bg-[#0d121f] border border-slate-700/90 rounded-xl shadow-2xl overflow-hidden backdrop-blur-md animate-in fade-in-50 zoom-in-95 ring-1 ring-black/40"
|
||||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-xs text-left text-slate-200 hover:text-white hover:bg-emerald-500/15 rounded-lg transition-colors cursor-pointer group"
|
>
|
||||||
>
|
<div className="p-2 border-b border-slate-800/80">
|
||||||
<div className="w-6 h-6 rounded-md bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center shrink-0 group-hover:bg-emerald-500/20">
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400 px-2 block">
|
||||||
<FileSpreadsheet className="w-3.5 h-3.5 text-emerald-400" />
|
Export Format ({data.length} items)
|
||||||
</div>
|
</span>
|
||||||
<div>
|
</div>
|
||||||
<span className="font-semibold block leading-tight">Spreadsheet (.xlsx)</span>
|
<div className="p-1 space-y-0.5">
|
||||||
<span className="text-[10px] text-slate-400">Excel / Google Sheets</span>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
</button>
|
onClick={handleExportSpreadsheet}
|
||||||
|
className="w-full flex items-center gap-2.5 px-3 py-2 text-xs text-left text-slate-200 hover:text-white hover:bg-emerald-500/15 rounded-lg transition-colors cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 rounded-md bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center shrink-0 group-hover:bg-emerald-500/20">
|
||||||
|
<FileSpreadsheet className="w-3.5 h-3.5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold block leading-tight">Spreadsheet (.xlsx)</span>
|
||||||
|
<span className="text-[10px] text-slate-400">Excel / Google Sheets</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleExportPdf}
|
onClick={handleExportPdf}
|
||||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-xs text-left text-slate-200 hover:text-white hover:bg-blue-500/15 rounded-lg transition-colors cursor-pointer group"
|
className="w-full flex items-center gap-2.5 px-3 py-2 text-xs text-left text-slate-200 hover:text-white hover:bg-blue-500/15 rounded-lg transition-colors cursor-pointer group"
|
||||||
>
|
>
|
||||||
<div className="w-6 h-6 rounded-md bg-blue-500/10 border border-blue-500/20 flex items-center justify-center shrink-0 group-hover:bg-blue-500/20">
|
<div className="w-6 h-6 rounded-md bg-blue-500/10 border border-blue-500/20 flex items-center justify-center shrink-0 group-hover:bg-blue-500/20">
|
||||||
<FileText className="w-3.5 h-3.5 text-blue-400" />
|
<FileText className="w-3.5 h-3.5 text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold block leading-tight">PDF Document (.pdf)</span>
|
<span className="font-semibold block leading-tight">PDF Document (.pdf)</span>
|
||||||
<span className="text-[10px] text-slate-400">Landscape with PFM Logo</span>
|
<span className="text-[10px] text-slate-400">Landscape with PFM Logo</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
)}
|
document.body
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -494,31 +494,31 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 shadow-lg backdrop-blur-sm overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left text-xs border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-800/80 bg-slate-900/50 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
<tr className="bg-[#0d121f]/95 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold text-[11px]">
|
||||||
<SortableTh column="name" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort}>
|
<SortableTh column="name" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort} className="py-3.5 px-5 whitespace-nowrap">
|
||||||
Beneficiary / Cohort
|
Beneficiary / Cohort
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="category" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort}>
|
<SortableTh column="category" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Category
|
Category
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="location" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort}>
|
<SortableTh column="location" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Location
|
Location
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="targetCount" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort}>
|
<SortableTh column="targetCount" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Target Headcount
|
Target Headcount
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="reachedCount" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort}>
|
<SortableTh column="reachedCount" currentColumn={benSortCol} direction={benSortDir} onSort={handleBenSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Reached Headcount
|
Reached Headcount
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<th className="py-3 px-4">Reach Progress</th>
|
<th className="py-3.5 px-4 font-semibold whitespace-nowrap">Reach Progress</th>
|
||||||
<th className="py-3 px-4 text-right">Actions</th>
|
<th className="py-3.5 px-5 text-right font-medium whitespace-nowrap">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/60 text-xs">
|
<tbody className="divide-y divide-slate-800/60">
|
||||||
{loadingBens ? (
|
{loadingBens ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="py-12 text-center text-slate-400">
|
<td colSpan={7} className="py-12 text-center text-slate-400">
|
||||||
@@ -538,27 +538,27 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
sortedBens.map((b) => {
|
sortedBens.map((b) => {
|
||||||
const prog = b.targetCount && b.targetCount > 0 ? Math.round(((b.reachedCount || 0) / b.targetCount) * 100) : 0;
|
const prog = b.targetCount && b.targetCount > 0 ? Math.round(((b.reachedCount || 0) / b.targetCount) * 100) : 0;
|
||||||
return (
|
return (
|
||||||
<tr key={b.id} className="hover:bg-slate-800/30 transition">
|
<tr key={b.id} className="hover:bg-slate-800/40 transition-colors">
|
||||||
<td className="py-3.5 px-4 font-semibold text-white">
|
<td className="py-3.5 px-5 whitespace-nowrap font-semibold text-white">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="w-3.5 h-3.5 text-blue-400 shrink-0" />
|
<Users className="w-3.5 h-3.5 text-blue-400 shrink-0" />
|
||||||
<span>{b.name}</span>
|
<span>{b.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 text-slate-300">
|
<td className="py-3.5 px-4 whitespace-nowrap text-slate-300">
|
||||||
<span className="bg-slate-800/80 px-2 py-0.5 rounded-md border border-slate-700/60 text-[11px]">
|
<span className="bg-slate-800/80 px-2 py-0.5 rounded-md border border-slate-700/60 text-[11px]">
|
||||||
{b.category || 'General'}
|
{b.category || 'General'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 text-slate-300">
|
<td className="py-3.5 px-4 whitespace-nowrap text-slate-300">
|
||||||
<div className="flex items-center gap-1.5 text-slate-400">
|
<div className="flex items-center gap-1.5 text-slate-400">
|
||||||
<MapPin className="w-3 h-3 text-slate-500" />
|
<MapPin className="w-3 h-3 text-slate-500" />
|
||||||
<span>{b.location || 'Not Specified'}</span>
|
<span>{b.location || 'Not Specified'}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 font-medium text-slate-200">{(b.targetCount || 0).toLocaleString()}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap font-medium text-slate-200">{(b.targetCount || 0).toLocaleString()}</td>
|
||||||
<td className="py-3.5 px-4 font-bold text-emerald-400">{(b.reachedCount || 0).toLocaleString()}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap font-bold text-emerald-400">{(b.reachedCount || 0).toLocaleString()}</td>
|
||||||
<td className="py-3.5 px-4">
|
<td className="py-3.5 px-4 whitespace-nowrap">
|
||||||
<div className="flex items-center gap-2 max-w-[140px]">
|
<div className="flex items-center gap-2 max-w-[140px]">
|
||||||
<div className="flex-1 bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
<div className="flex-1 bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
||||||
<div className="bg-emerald-500 h-full rounded-full" style={{ width: `${Math.min(100, prog)}%` }} />
|
<div className="bg-emerald-500 h-full rounded-full" style={{ width: `${Math.min(100, prog)}%` }} />
|
||||||
@@ -566,7 +566,7 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
<span className="text-[11px] font-mono text-slate-400">{prog}%</span>
|
<span className="text-[11px] font-mono text-slate-400">{prog}%</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 text-right space-x-2">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedBenForDetail(b);
|
setSelectedBenForDetail(b);
|
||||||
@@ -679,27 +679,27 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 shadow-lg backdrop-blur-sm overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left text-xs border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-800/80 bg-slate-900/50 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
<tr className="bg-[#0d121f]/95 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold text-[11px]">
|
||||||
<SortableTh column="title" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort}>
|
<SortableTh column="title" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort} className="py-3.5 px-5 whitespace-nowrap">
|
||||||
Compliance Title
|
Compliance Title
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="requirements" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort}>
|
<SortableTh column="requirements" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Requirements
|
Requirements
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="status" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort}>
|
<SortableTh column="status" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Verification Status
|
Verification Status
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="createdAt" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort}>
|
<SortableTh column="createdAt" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Created
|
Created
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<th className="py-3 px-4 text-right">Actions</th>
|
<th className="py-3.5 px-5 text-right font-medium whitespace-nowrap">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/60 text-xs">
|
<tbody className="divide-y divide-slate-800/60">
|
||||||
{loadingComps ? (
|
{loadingComps ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="py-12 text-center text-slate-400">
|
<td colSpan={5} className="py-12 text-center text-slate-400">
|
||||||
@@ -717,8 +717,8 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
sortedComps.map((c) => (
|
sortedComps.map((c) => (
|
||||||
<tr key={c.id} className="hover:bg-slate-800/30 transition">
|
<tr key={c.id} className="hover:bg-slate-800/40 transition-colors">
|
||||||
<td className="py-3.5 px-4 font-semibold text-white">
|
<td className="py-3.5 px-5 whitespace-nowrap font-semibold text-white">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ShieldCheck className="w-3.5 h-3.5 text-cyan-400 shrink-0" />
|
<ShieldCheck className="w-3.5 h-3.5 text-cyan-400 shrink-0" />
|
||||||
<span>{c.title}</span>
|
<span>{c.title}</span>
|
||||||
@@ -727,9 +727,9 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
<td className="py-3.5 px-4 text-slate-300 max-w-xs truncate" title={c.requirements || ''}>
|
<td className="py-3.5 px-4 text-slate-300 max-w-xs truncate" title={c.requirements || ''}>
|
||||||
{c.requirements || 'None'}
|
{c.requirements || 'None'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4">{getApprovalStatusBadge(c.status)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap">{getApprovalStatusBadge(c.status)}</td>
|
||||||
<td className="py-3.5 px-4 text-slate-400">{formatDate(c.createdAt)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap text-slate-400">{formatDate(c.createdAt)}</td>
|
||||||
<td className="py-3.5 px-4 text-right">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpenEditComp(c)}
|
onClick={() => handleOpenEditComp(c)}
|
||||||
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
|
className="p-1 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition cursor-pointer"
|
||||||
|
|||||||
@@ -534,28 +534,28 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 shadow-lg backdrop-blur-sm overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left text-xs border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-800/80 bg-slate-900/50 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
<tr className="bg-[#0d121f]/95 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold text-[11px]">
|
||||||
<SortableTh column="title" currentColumn={issueSortCol} direction={issueSortDir} onSort={handleIssueSort}>
|
<SortableTh column="title" currentColumn={issueSortCol} direction={issueSortDir} onSort={handleIssueSort} className="py-3.5 px-5 whitespace-nowrap">
|
||||||
Issue Summary
|
Issue Summary
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="priority" currentColumn={issueSortCol} direction={issueSortDir} onSort={handleIssueSort}>
|
<SortableTh column="priority" currentColumn={issueSortCol} direction={issueSortDir} onSort={handleIssueSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Priority
|
Priority
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="assignedTo" currentColumn={issueSortCol} direction={issueSortDir} onSort={handleIssueSort}>
|
<SortableTh column="assignedTo" currentColumn={issueSortCol} direction={issueSortDir} onSort={handleIssueSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Assigned To
|
Assigned To
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="status" currentColumn={issueSortCol} direction={issueSortDir} onSort={handleIssueSort}>
|
<SortableTh column="status" currentColumn={issueSortCol} direction={issueSortDir} onSort={handleIssueSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Status
|
Status
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<th className="py-3 px-4">Resolution Notes</th>
|
<th className="py-3.5 px-4 font-semibold whitespace-nowrap">Resolution Notes</th>
|
||||||
<th className="py-3 px-4 text-right">Actions</th>
|
<th className="py-3.5 px-5 text-right font-medium whitespace-nowrap">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/60 text-xs">
|
<tbody className="divide-y divide-slate-800/60">
|
||||||
{loadingIssues ? (
|
{loadingIssues ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="py-12 text-center text-slate-400">
|
<td colSpan={6} className="py-12 text-center text-slate-400">
|
||||||
@@ -575,8 +575,8 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
sortedIssues.map((iss) => {
|
sortedIssues.map((iss) => {
|
||||||
const isResolved = iss.status === ActivityStatus.Inactive || !!iss.resolvedAt;
|
const isResolved = iss.status === ActivityStatus.Inactive || !!iss.resolvedAt;
|
||||||
return (
|
return (
|
||||||
<tr key={iss.id} className="hover:bg-slate-800/30 transition">
|
<tr key={iss.id} className="hover:bg-slate-800/40 transition-colors">
|
||||||
<td className="py-3.5 px-4 font-semibold text-white">
|
<td className="py-3.5 px-5 font-semibold text-white">
|
||||||
<div className="flex items-start gap-2.5">
|
<div className="flex items-start gap-2.5">
|
||||||
<AlertOctagon
|
<AlertOctagon
|
||||||
className={`w-4 h-4 mt-0.5 shrink-0 ${isResolved ? 'text-emerald-400' : 'text-rose-400'}`}
|
className={`w-4 h-4 mt-0.5 shrink-0 ${isResolved ? 'text-emerald-400' : 'text-rose-400'}`}
|
||||||
@@ -587,14 +587,14 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4">{getPriorityBadge(iss.priority)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap">{getPriorityBadge(iss.priority)}</td>
|
||||||
<td className="py-3.5 px-4 text-slate-300">
|
<td className="py-3.5 px-4 whitespace-nowrap text-slate-300">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<User className="w-3.5 h-3.5 text-slate-400" />
|
<User className="w-3.5 h-3.5 text-slate-400" />
|
||||||
<span className="truncate max-w-[150px]">{iss.assignedTo ? getUserEmail(iss.assignedTo) : 'Unassigned'}</span>
|
<span className="truncate max-w-[150px]">{iss.assignedTo ? getUserEmail(iss.assignedTo) : 'Unassigned'}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4">
|
<td className="py-3.5 px-4 whitespace-nowrap">
|
||||||
{isResolved ? (
|
{isResolved ? (
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
|
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
|
||||||
<CheckCircle2 className="w-3 h-3" /> Resolved
|
<CheckCircle2 className="w-3 h-3" /> Resolved
|
||||||
@@ -608,7 +608,7 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
<td className="py-3.5 px-4 text-slate-400 max-w-xs truncate">
|
<td className="py-3.5 px-4 text-slate-400 max-w-xs truncate">
|
||||||
{iss.resolution || <span className="italic text-slate-600">Pending resolution...</span>}
|
{iss.resolution || <span className="italic text-slate-600">Pending resolution...</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 text-right space-x-2">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => openResolveIssue(iss)}
|
onClick={() => openResolveIssue(iss)}
|
||||||
className="px-2.5 py-1 text-xs font-medium text-amber-400 hover:text-amber-300 bg-amber-950/40 hover:bg-amber-900/40 rounded-lg border border-amber-800/40 transition cursor-pointer"
|
className="px-2.5 py-1 text-xs font-medium text-amber-400 hover:text-amber-300 bg-amber-950/40 hover:bg-amber-900/40 rounded-lg border border-amber-800/40 transition cursor-pointer"
|
||||||
@@ -733,27 +733,27 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 shadow-lg backdrop-blur-sm overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left text-xs border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-800/80 bg-slate-900/50 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
<tr className="bg-[#0d121f]/95 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold text-[11px]">
|
||||||
<SortableTh column="title" currentColumn={changeSortCol} direction={changeSortDir} onSort={handleChangeSort}>
|
<SortableTh column="title" currentColumn={changeSortCol} direction={changeSortDir} onSort={handleChangeSort} className="py-3.5 px-5 whitespace-nowrap">
|
||||||
Change Proposal
|
Change Proposal
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="justification" currentColumn={changeSortCol} direction={changeSortDir} onSort={handleChangeSort}>
|
<SortableTh column="justification" currentColumn={changeSortCol} direction={changeSortDir} onSort={handleChangeSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Justification
|
Justification
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="revisedBudget" currentColumn={changeSortCol} direction={changeSortDir} onSort={handleChangeSort}>
|
<SortableTh column="revisedBudget" currentColumn={changeSortCol} direction={changeSortDir} onSort={handleChangeSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Revised Budget
|
Revised Budget
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="status" currentColumn={changeSortCol} direction={changeSortDir} onSort={handleChangeSort}>
|
<SortableTh column="status" currentColumn={changeSortCol} direction={changeSortDir} onSort={handleChangeSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Governance Status
|
Governance Status
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<th className="py-3 px-4 text-right">In-Place Decision</th>
|
<th className="py-3.5 px-5 text-right font-medium whitespace-nowrap">In-Place Decision</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/60 text-xs">
|
<tbody className="divide-y divide-slate-800/60">
|
||||||
{loadingChanges ? (
|
{loadingChanges ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="py-12 text-center text-slate-400">
|
<td colSpan={5} className="py-12 text-center text-slate-400">
|
||||||
@@ -771,8 +771,8 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
sortedChanges.map((cr) => (
|
sortedChanges.map((cr) => (
|
||||||
<tr key={cr.id} className="hover:bg-slate-800/30 transition">
|
<tr key={cr.id} className="hover:bg-slate-800/40 transition-colors">
|
||||||
<td className="py-3.5 px-4 font-semibold text-white">
|
<td className="py-3.5 px-5 whitespace-nowrap font-semibold text-white">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<GitPullRequest className="w-4 h-4 text-amber-400 shrink-0" />
|
<GitPullRequest className="w-4 h-4 text-amber-400 shrink-0" />
|
||||||
<span>{cr.title}</span>
|
<span>{cr.title}</span>
|
||||||
@@ -781,11 +781,11 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
<td className="py-3.5 px-4 text-slate-300 max-w-xs truncate" title={cr.justification || ''}>
|
<td className="py-3.5 px-4 text-slate-300 max-w-xs truncate" title={cr.justification || ''}>
|
||||||
{cr.justification || 'None'}
|
{cr.justification || 'None'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 font-mono font-semibold text-slate-200">
|
<td className="py-3.5 px-4 whitespace-nowrap font-mono font-semibold text-slate-200">
|
||||||
{formatZAR(cr.revisedBudget || 0)}
|
{formatZAR(cr.revisedBudget || 0)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4">{getApprovalStatusBadge(cr.status)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap">{getApprovalStatusBadge(cr.status)}</td>
|
||||||
<td className="py-3.5 px-4 text-right space-x-2">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
||||||
{cr.status === ApprovalStatus.Approved ? (
|
{cr.status === ApprovalStatus.Approved ? (
|
||||||
<span className="text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
|
<span className="text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
|
||||||
Resolved & Enacted
|
Resolved & Enacted
|
||||||
|
|||||||
@@ -1268,7 +1268,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter & Action Toolbar */}
|
{/* Filter & Action Toolbar */}
|
||||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-4 rounded-2xl border border-slate-800/80 shadow-xl flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3">
|
<div className="relative z-30 bg-[#0f1523]/80 backdrop-blur-md p-4 rounded-2xl border border-slate-800/80 shadow-xl flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3">
|
||||||
<div className="relative flex-1 max-w-md">
|
<div className="relative flex-1 max-w-md">
|
||||||
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
|
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -353,27 +353,27 @@ export const SiteVisitsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 shadow-lg backdrop-blur-sm overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left text-xs border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-800/80 bg-slate-900/50 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
<tr className="bg-[#0d121f]/95 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold text-[11px]">
|
||||||
<SortableTh column="inspectorName" currentColumn={visitSortCol} direction={visitSortDir} onSort={handleVisitSort}>
|
<SortableTh column="inspectorName" currentColumn={visitSortCol} direction={visitSortDir} onSort={handleVisitSort} className="py-3.5 px-5 whitespace-nowrap">
|
||||||
Inspector Name
|
Inspector Name
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="visitedAt" currentColumn={visitSortCol} direction={visitSortDir} onSort={handleVisitSort}>
|
<SortableTh column="visitedAt" currentColumn={visitSortCol} direction={visitSortDir} onSort={handleVisitSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Inspection Date
|
Inspection Date
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="findings" currentColumn={visitSortCol} direction={visitSortDir} onSort={handleVisitSort}>
|
<SortableTh column="findings" currentColumn={visitSortCol} direction={visitSortDir} onSort={handleVisitSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Field Observations & Findings
|
Field Observations & Findings
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<SortableTh column="status" currentColumn={visitSortCol} direction={visitSortDir} onSort={handleVisitSort}>
|
<SortableTh column="status" currentColumn={visitSortCol} direction={visitSortDir} onSort={handleVisitSort} className="py-3.5 px-4 whitespace-nowrap">
|
||||||
Verification Status
|
Verification Status
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<th className="py-3 px-4 text-right">Actions</th>
|
<th className="py-3.5 px-5 text-right font-medium whitespace-nowrap">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/60 text-xs">
|
<tbody className="divide-y divide-slate-800/60">
|
||||||
{loadingVisits ? (
|
{loadingVisits ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="py-12 text-center text-slate-400">
|
<td colSpan={5} className="py-12 text-center text-slate-400">
|
||||||
@@ -391,14 +391,14 @@ export const SiteVisitsPage: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
sortedVisits.map((v) => (
|
sortedVisits.map((v) => (
|
||||||
<tr key={v.id} className="hover:bg-slate-800/30 transition">
|
<tr key={v.id} className="hover:bg-slate-800/40 transition-colors">
|
||||||
<td className="py-3.5 px-4 font-semibold text-white">
|
<td className="py-3.5 px-5 whitespace-nowrap font-semibold text-white">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<User className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
<User className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||||
<span>{v.inspectorName || 'Field Officer'}</span>
|
<span>{v.inspectorName || 'Field Officer'}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 text-slate-300 font-medium">
|
<td className="py-3.5 px-4 whitespace-nowrap text-slate-300 font-medium">
|
||||||
<div className="flex items-center gap-1.5 text-slate-300">
|
<div className="flex items-center gap-1.5 text-slate-300">
|
||||||
<Calendar className="w-3 h-3 text-slate-500" />
|
<Calendar className="w-3 h-3 text-slate-500" />
|
||||||
<span>{formatDate(v.visitedAt)}</span>
|
<span>{formatDate(v.visitedAt)}</span>
|
||||||
@@ -407,8 +407,8 @@ export const SiteVisitsPage: React.FC = () => {
|
|||||||
<td className="py-3.5 px-4 text-slate-300 max-w-sm truncate" title={v.findings || ''}>
|
<td className="py-3.5 px-4 text-slate-300 max-w-sm truncate" title={v.findings || ''}>
|
||||||
{v.findings || 'No findings recorded.'}
|
{v.findings || 'No findings recorded.'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4">{getApprovalStatusBadge(v.status)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap">{getApprovalStatusBadge(v.status)}</td>
|
||||||
<td className="py-3.5 px-4 text-right space-x-2">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedVisitForDetail(v);
|
setSelectedVisitForDetail(v);
|
||||||
|
|||||||
@@ -1531,8 +1531,20 @@ export const updateTenant = async (payload: UpdateTenantRequest): Promise<void>
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
export const getProjectBeneficiaries = async (projectId: string | number): Promise<Beneficiary[]> => {
|
export const getProjectBeneficiaries = async (projectId: string | number): Promise<Beneficiary[]> => {
|
||||||
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
||||||
const res = await apiClient.get<Beneficiary[]>(`/v1/api/projects/beneficiaries/${hashedProjectId}`);
|
try {
|
||||||
return res.data || [];
|
const res = await apiClient.get<Beneficiary[]>(`/v1/api/projects/${hashedProjectId}/beneficiaries`);
|
||||||
|
return res.data || [];
|
||||||
|
} catch (err: any) {
|
||||||
|
if (String(projectId) !== String(hashedProjectId)) {
|
||||||
|
try {
|
||||||
|
const fallbackRes = await apiClient.get<Beneficiary[]>(`/v1/api/projects/${projectId}/beneficiaries`);
|
||||||
|
return fallbackRes.data || [];
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBeneficiary = async (beneficiaryId: string | number): Promise<Beneficiary> => {
|
export const getBeneficiary = async (beneficiaryId: string | number): Promise<Beneficiary> => {
|
||||||
@@ -1605,8 +1617,20 @@ export const updateOutcome = async (payload: UpdateOutcomeRequest): Promise<void
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
export const getProjectChangeRequests = async (projectId: string | number): Promise<ChangeRequest[]> => {
|
export const getProjectChangeRequests = async (projectId: string | number): Promise<ChangeRequest[]> => {
|
||||||
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
||||||
const res = await apiClient.get<ChangeRequest[]>(`/v1/api/projects/changerequests/${hashedProjectId}`);
|
try {
|
||||||
return res.data || [];
|
const res = await apiClient.get<ChangeRequest[]>(`/v1/api/projects/${hashedProjectId}/changerequests`);
|
||||||
|
return res.data || [];
|
||||||
|
} catch (err: any) {
|
||||||
|
if (String(projectId) !== String(hashedProjectId)) {
|
||||||
|
try {
|
||||||
|
const fallbackRes = await apiClient.get<ChangeRequest[]>(`/v1/api/projects/${projectId}/changerequests`);
|
||||||
|
return fallbackRes.data || [];
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getChangeRequest = async (changeRequestId: string | number): Promise<ChangeRequest> => {
|
export const getChangeRequest = async (changeRequestId: string | number): Promise<ChangeRequest> => {
|
||||||
@@ -1676,8 +1700,20 @@ export const updateComplianceItem = async (payload: UpdateComplianceItemRequest)
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
export const getProjectIssues = async (projectId: string | number): Promise<Issue[]> => {
|
export const getProjectIssues = async (projectId: string | number): Promise<Issue[]> => {
|
||||||
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
||||||
const res = await apiClient.get<Issue[]>(`/v1/api/projects/issues/${hashedProjectId}`);
|
try {
|
||||||
return res.data || [];
|
const res = await apiClient.get<Issue[]>(`/v1/api/projects/${hashedProjectId}/issues`);
|
||||||
|
return res.data || [];
|
||||||
|
} catch (err: any) {
|
||||||
|
if (String(projectId) !== String(hashedProjectId)) {
|
||||||
|
try {
|
||||||
|
const fallbackRes = await apiClient.get<Issue[]>(`/v1/api/projects/${projectId}/issues`);
|
||||||
|
return fallbackRes.data || [];
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getIssue = async (issueId: string | number): Promise<Issue> => {
|
export const getIssue = async (issueId: string | number): Promise<Issue> => {
|
||||||
@@ -1718,8 +1754,20 @@ export const updateIssue = async (payload: UpdateIssueRequest): Promise<void> =>
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
export const getProjectSiteVisits = async (projectId: string | number): Promise<SiteVisit[]> => {
|
export const getProjectSiteVisits = async (projectId: string | number): Promise<SiteVisit[]> => {
|
||||||
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
||||||
const res = await apiClient.get<SiteVisit[]>(`/v1/api/projects/sitevisits/${hashedProjectId}`);
|
try {
|
||||||
return res.data || [];
|
const res = await apiClient.get<SiteVisit[]>(`/v1/api/projects/${hashedProjectId}/sitevisits`);
|
||||||
|
return res.data || [];
|
||||||
|
} catch (err: any) {
|
||||||
|
if (String(projectId) !== String(hashedProjectId)) {
|
||||||
|
try {
|
||||||
|
const fallbackRes = await apiClient.get<SiteVisit[]>(`/v1/api/projects/${projectId}/sitevisits`);
|
||||||
|
return fallbackRes.data || [];
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSiteVisit = async (siteVisitId: string | number): Promise<SiteVisit> => {
|
export const getSiteVisit = async (siteVisitId: string | number): Promise<SiteVisit> => {
|
||||||
|
|||||||
@@ -108,12 +108,15 @@ export const SortableTh: React.FC<SortableThProps> = ({
|
|||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const isCurrent = currentColumn === column && direction !== null;
|
const isCurrent = currentColumn === column && direction !== null;
|
||||||
|
const hasPadding = className.includes('p-') || className.includes('py-') || className.includes('px-');
|
||||||
|
const defaultPadding = hasPadding ? '' : 'py-3.5 px-4';
|
||||||
|
const defaultWhitespace = className.includes('whitespace-') ? '' : 'whitespace-nowrap';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
{...props}
|
{...props}
|
||||||
onClick={() => onSort(column)}
|
onClick={() => onSort(column)}
|
||||||
className={`cursor-pointer select-none group transition-colors hover:text-white ${className}`}
|
className={`cursor-pointer select-none group transition-colors hover:text-white ${defaultPadding} ${defaultWhitespace} ${className}`.trim()}
|
||||||
title={`Click to sort (Ascending, Descending, Clear)`}
|
title={`Click to sort (Ascending, Descending, Clear)`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user