From 9ba48de55b15509716995b7f1d06b6958b96125c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 27 Sep 2026 14:27:01 +0200 Subject: [PATCH 1/2] ui tweaks --- .../src/pages/ChangeManagementPage.tsx | 21 +- .../src/pages/SiteVisitsPage.tsx | 203 +----------------- 2 files changed, 19 insertions(+), 205 deletions(-) diff --git a/PostFundManagement.ConsoleUI/src/pages/ChangeManagementPage.tsx b/PostFundManagement.ConsoleUI/src/pages/ChangeManagementPage.tsx index bbdd318..796a040 100644 --- a/PostFundManagement.ConsoleUI/src/pages/ChangeManagementPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/ChangeManagementPage.tsx @@ -783,15 +783,26 @@ export const ChangeManagementPage: React.FC = () => { diff --git a/PostFundManagement.ConsoleUI/src/pages/SiteVisitsPage.tsx b/PostFundManagement.ConsoleUI/src/pages/SiteVisitsPage.tsx index f3ad7a4..91a2c8a 100644 --- a/PostFundManagement.ConsoleUI/src/pages/SiteVisitsPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/SiteVisitsPage.tsx @@ -54,7 +54,6 @@ export const SiteVisitsPage: React.FC = () => { const { success, error: toastError } = useToast(); const [selectedProjectId, setSelectedProjectId] = useState('all'); - const [activeTab, setActiveTab] = useState<'visits' | 'feedback'>('visits'); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); @@ -250,38 +249,9 @@ export const SiteVisitsPage: React.FC = () => { - {/* Tabs */} + {/* Site Visits Register View */}
-
- - - -
- - {/* Tab 1: Visits Table View */} - {activeTab === 'visits' && ( -
-
+
{/* Search */}
@@ -449,174 +419,7 @@ export const SiteVisitsPage: React.FC = () => {
-
- )} - - {/* Tab 2: Field Observations & Feedback Cards */} - {activeTab === 'feedback' && ( -
-
-
- {/* Search */} -
- - setSearchTerm(e.target.value)} - className="w-full bg-slate-900 border border-slate-800 rounded-xl pl-8 pr-3 py-1.5 text-xs text-white focus:outline-none focus:border-emerald-500 shadow-xs" - /> -
- - {/* Status Filter */} -
- -
- - {/* Project Selector */} -
- - -
-
- - {/* Actions: Refresh, Export, Log Site Visit */} -
- - - - - -
-
- - {loadingVisits ? ( -
-
-
- Loading observation cards... -
-
- ) : sortedVisits.length === 0 ? ( -
- No site visit observations or feedback found matching criteria. -
- ) : ( -
- {sortedVisits.map((v) => ( -
-
-
-
-
- -
-
-

- {v.inspectorName || 'Field Auditor'} -

-
- - {formatDate(v.visitedAt)} -
-
-
-
- -
-
- -
-

- Field Observations & Findings -

- {v.findings || 'No detailed findings recorded.'} -
-
- -
- ID #{v.id} -
- - -
-
-
- ))} -
- )} -
- )} -
+
{/* Modal: Create/Edit Site Visit */} Date: Sun, 27 Sep 2026 14:35:52 +0200 Subject: [PATCH 2/2] Fixed error handling --- .../src/components/ui/Toast.tsx | 63 ++++- .../src/pages/BeneficiariesPage.tsx | 10 +- .../src/pages/ChangeManagementPage.tsx | 12 +- .../src/pages/SiteVisitsPage.tsx | 4 +- .../src/utils/apiErrorParser.ts | 215 ++++++++++++------ 5 files changed, 214 insertions(+), 90 deletions(-) diff --git a/PostFundManagement.ConsoleUI/src/components/ui/Toast.tsx b/PostFundManagement.ConsoleUI/src/components/ui/Toast.tsx index 41a8b19..43f9b12 100644 --- a/PostFundManagement.ConsoleUI/src/components/ui/Toast.tsx +++ b/PostFundManagement.ConsoleUI/src/components/ui/Toast.tsx @@ -155,22 +155,43 @@ const ToastCard: React.FC<{ )}
- {/* Display errors from ProblemDetails comma-delimited detail or errors list */} - {toast.errorsList && toast.errorsList.length > 1 ? ( -
-
    + {/* Display comma-separated error items from detail property one under another */} + {toast.errorsList && toast.errorsList.length > 0 ? ( +
    +
      {toast.errorsList.map((err, idx) => ( -
    • - • - {err} +
    • + • + {err}
    • ))}
    ) : toast.message ? ( -

    - {toast.message} -

    +
    + {toast.message.includes('\n') ? ( +
      + {toast.message + .split('\n') + .map((l) => l.trim()) + .filter(Boolean) + .map((line, idx) => ( +
    • + • + {line.replace(/^[•\-*]\s*/, '')} +
    • + ))} +
    + ) : ( +

    {toast.message}

    + )} +
    ) : null} {/* Optional collapsible technical reference */} @@ -310,14 +331,32 @@ export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) = // Parse friendly non-technical error with ProblemDetails awareness const parsed = parseFriendlyError(rawError, actionTitle); - addToast('error', parsed.title, parsed.message, { + let finalMessage = parsed.message; + if ( + typeof messageOrError === 'string' && + messageOrError.trim() && + !messageOrError.includes('Request failed with status code') && + (!parsed.hasReturnObject || !parsed.errorsList?.length) + ) { + finalMessage = messageOrError.trim(); + } + + let finalErrorsList = parsed.errorsList || options?.errorsList; + if (!finalErrorsList && typeof finalMessage === 'string' && finalMessage.includes(',')) { + const parts = finalMessage.split(',').map((p) => p.trim()).filter(Boolean); + if (parts.length > 1) { + finalErrorsList = parts; + } + } + + addToast('error', parsed.title, finalMessage, { duration: options?.duration || DEFAULT_DURATIONS.error, statusCode: parsed.statusCode || options?.statusCode, category: options?.category || parsed.category, categoryLabel: options?.categoryLabel || parsed.categoryLabel, technicalDetails: parsed.technicalDetails || options?.technicalDetails, fieldErrors: parsed.fieldErrors || options?.fieldErrors, - errorsList: parsed.errorsList || options?.errorsList, + errorsList: finalErrorsList, }); }; diff --git a/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx b/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx index 0bd7122..43f6354 100644 --- a/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx @@ -163,7 +163,7 @@ export const BeneficiariesPage: React.FC = () => { benForm.reset(); success('Beneficiary Registered', 'New project beneficiary profile successfully created.'); }, - onError: (err: any) => toastError('Registration Failed', err.message || 'Unable to save beneficiary.'), + onError: (err: any) => toastError('Registration Failed', err), }); const updateBenMutation = useMutation({ @@ -185,7 +185,7 @@ export const BeneficiariesPage: React.FC = () => { benForm.reset(); success('Beneficiary Updated', 'Beneficiary details and targets updated.'); }, - onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update beneficiary.'), + onError: (err: any) => toastError('Update Failed', err), }); const createCompMutation = useMutation({ @@ -199,7 +199,7 @@ export const BeneficiariesPage: React.FC = () => { compForm.reset(); success('Compliance Requirement Logged', 'Compliance item registered for project beneficiaries.'); }, - onError: (err: any) => toastError('Creation Failed', err.message || 'Unable to record compliance item.'), + onError: (err: any) => toastError('Creation Failed', err), }); const updateCompMutation = useMutation({ @@ -219,7 +219,7 @@ export const BeneficiariesPage: React.FC = () => { compForm.reset(); success('Compliance Item Updated', 'Status and requirement record updated.'); }, - onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update compliance item.'), + onError: (err: any) => toastError('Update Failed', err), }); const createOutcomeMutation = useMutation({ @@ -233,7 +233,7 @@ export const BeneficiariesPage: React.FC = () => { outcomeForm.reset(); success('Outcome Logged', 'Beneficiary outcome and impact metric registered.'); }, - onError: (err: any) => toastError('Failed to Record Outcome', err.message || 'Unable to save outcome.'), + onError: (err: any) => toastError('Failed to Record Outcome', err), }); // Filtered & Sorted Beneficiaries diff --git a/PostFundManagement.ConsoleUI/src/pages/ChangeManagementPage.tsx b/PostFundManagement.ConsoleUI/src/pages/ChangeManagementPage.tsx index 796a040..92bd341 100644 --- a/PostFundManagement.ConsoleUI/src/pages/ChangeManagementPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/ChangeManagementPage.tsx @@ -235,7 +235,7 @@ export const ChangeManagementPage: React.FC = () => { issueForm.reset(); success('Issue Logged', 'New project issue created and assigned.'); }, - onError: (err: any) => toastError('Creation Failed', err.message || 'Unable to log issue.'), + onError: (err: any) => toastError('Creation Failed', err), }); const updateIssueMutation = useMutation({ @@ -270,7 +270,7 @@ export const ChangeManagementPage: React.FC = () => { editIssueForm.reset(); success('Issue Updated', 'Issue record and resolution status saved successfully.'); }, - onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update issue.'), + onError: (err: any) => toastError('Update Failed', err), }); const resolveIssueMutation = useMutation({ @@ -294,7 +294,7 @@ export const ChangeManagementPage: React.FC = () => { resolveIssueForm.reset(); success('Issue Resolved', 'Resolution recorded and issue status updated.'); }, - onError: (err: any) => toastError('Resolution Failed', err.message || 'Unable to resolve issue.'), + onError: (err: any) => toastError('Resolution Failed', err), }); const createChangeMutation = useMutation({ @@ -314,7 +314,7 @@ export const ChangeManagementPage: React.FC = () => { changeForm.reset(); success('Change Request Submitted', 'Formal change request submitted for review.'); }, - onError: (err: any) => toastError('Submission Failed', err.message || 'Unable to submit change request.'), + onError: (err: any) => toastError('Submission Failed', err), }); const resolveChangeMutation = useMutation({ @@ -335,7 +335,7 @@ export const ChangeManagementPage: React.FC = () => { resolveChangeForm.reset(); success('Change Decision Applied', 'Change request decision successfully resolved.'); }, - onError: (err: any) => toastError('Decision Failed', err.message || 'Unable to resolve change request.'), + onError: (err: any) => toastError('Decision Failed', err), }); const updateChangeMutation = useMutation({ @@ -356,7 +356,7 @@ export const ChangeManagementPage: React.FC = () => { editChangeForm.reset(); success('Change Request Updated', 'Change proposal and governance status saved.'); }, - onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update change request.'), + onError: (err: any) => toastError('Update Failed', err), }); // Filtered & Sorted Issues diff --git a/PostFundManagement.ConsoleUI/src/pages/SiteVisitsPage.tsx b/PostFundManagement.ConsoleUI/src/pages/SiteVisitsPage.tsx index 91a2c8a..41688fd 100644 --- a/PostFundManagement.ConsoleUI/src/pages/SiteVisitsPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/SiteVisitsPage.tsx @@ -111,7 +111,7 @@ export const SiteVisitsPage: React.FC = () => { visitForm.reset(); success('Site Visit Logged', 'Field inspection recorded with inspector findings.'); }, - onError: (err: any) => toastError('Logging Failed', err.message || 'Unable to save site visit.'), + onError: (err: any) => toastError('Logging Failed', err), }); const updateVisitMutation = useMutation({ @@ -132,7 +132,7 @@ export const SiteVisitsPage: React.FC = () => { visitForm.reset(); success('Site Visit Updated', 'Inspection report and verification status updated.'); }, - onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update site visit.'), + onError: (err: any) => toastError('Update Failed', err), }); // Filtered & Sorted Visits diff --git a/PostFundManagement.ConsoleUI/src/utils/apiErrorParser.ts b/PostFundManagement.ConsoleUI/src/utils/apiErrorParser.ts index cda4092..9295cdf 100644 --- a/PostFundManagement.ConsoleUI/src/utils/apiErrorParser.ts +++ b/PostFundManagement.ConsoleUI/src/utils/apiErrorParser.ts @@ -29,6 +29,8 @@ export interface ProblemDetailsResponse { title?: string | null; status?: number | null; detail?: string | null; + details?: string | null; + Details?: string | null; instance?: string | null; errors?: Record | any; [key: string]: any; @@ -124,7 +126,17 @@ export function inspectErrorResponseObject(error: unknown): ProblemDetailsRespon // 2. Direct object inspection (e.g. if response.data was passed directly) if (typeof error === 'object' && error !== null) { - if ('detail' in anyErr || 'status' in anyErr || 'title' in anyErr || 'type' in anyErr) { + if ( + 'details' in anyErr || + 'Details' in anyErr || + 'detail' in anyErr || + 'Detail' in anyErr || + 'status' in anyErr || + 'title' in anyErr || + 'type' in anyErr || + 'errors' in anyErr || + 'Errors' in anyErr + ) { return anyErr as ProblemDetailsResponse; } } @@ -138,8 +150,11 @@ export function inspectErrorResponseObject(error: unknown): ProblemDetailsRespon export function isProblemDetailsObject(data: any): data is ProblemDetails { if (!data || typeof data !== 'object') return false; return ( + 'details' in data || + 'Details' in data || 'title' in data || 'detail' in data || + 'Detail' in data || 'status' in data || 'type' in data || 'errors' in data || @@ -163,9 +178,13 @@ export function parseCommaDelimitedErrors(detail?: string | null): string[] { const errors: string[] = []; for (const part of rawParts) { - const cleaned = cleanTechnicalJargon(part.trim()); + const pTrim = part.trim(); + if (!pTrim) continue; + const cleaned = cleanTechnicalJargon(pTrim); if (cleaned && cleaned.length > 0) { errors.push(cleaned); + } else if (pTrim.length > 0 && pTrim.toLowerCase() !== 'error') { + errors.push(pTrim); } } @@ -301,7 +320,16 @@ export function parseFriendlyError( // 3. Special handling for Auth (401) and Forbid (403) if (statusCode === 401) { - const rawDetail = returnObject?.detail || (error as any)?.message; + const rawDetail = + returnObject?.details ?? + returnObject?.Details ?? + returnObject?.detail ?? + returnObject?.Detail ?? + anyErr?.response?.data?.details ?? + anyErr?.response?.data?.Details ?? + anyErr?.response?.data?.detail ?? + anyErr?.details ?? + anyErr?.message; const cleanDetail = cleanTechnicalJargon(rawDetail || ''); return { title: 'Authentication Required', @@ -316,7 +344,16 @@ export function parseFriendlyError( } if (statusCode === 403) { - const rawDetail = returnObject?.detail || (error as any)?.message; + const rawDetail = + returnObject?.details ?? + returnObject?.Details ?? + returnObject?.detail ?? + returnObject?.Detail ?? + anyErr?.response?.data?.details ?? + anyErr?.response?.data?.Details ?? + anyErr?.response?.data?.detail ?? + anyErr?.details ?? + anyErr?.message; const detailErrors = parseCommaDelimitedErrors(rawDetail); const friendlyMessage = detailErrors.length > 0 @@ -340,16 +377,38 @@ export function parseFriendlyError( // 4. Case A: Response has a valid return object (e.g. RFC 9110 ProblemDetails) if (hasReturnObject && returnObject) { + // Priority: 'detail' property from response, then 'details', etc. const rawDetail = returnObject.detail ?? returnObject.Detail ?? + returnObject.details ?? + returnObject.Details ?? + anyErr?.response?.data?.detail ?? + anyErr?.response?.data?.Detail ?? + anyErr?.response?.data?.details ?? + anyErr?.response?.data?.Details ?? + anyErr?.detail ?? + anyErr?.Detail ?? + anyErr?.details ?? + anyErr?.Details ?? returnObject.message ?? - returnObject.error; + returnObject.Message ?? + returnObject.error ?? + returnObject.Error; - // Parse comma-delimited errors from detail property - const errorsList = parseCommaDelimitedErrors(rawDetail); + // Parse errors list from details property + const errorsList: string[] = []; - // If detail was empty, check if errors array/dictionary exists + if (Array.isArray(rawDetail)) { + rawDetail.forEach((e: any) => { + const cleaned = cleanTechnicalJargon(typeof e === 'string' ? e : e?.message || JSON.stringify(e)); + if (cleaned) errorsList.push(cleaned); + }); + } else if (typeof rawDetail === 'string' && rawDetail.trim()) { + errorsList.push(...parseCommaDelimitedErrors(rawDetail)); + } + + // If details was empty, check if errors array/dictionary exists if (errorsList.length === 0 && returnObject.errors) { if (Array.isArray(returnObject.errors)) { returnObject.errors.forEach((e: any) => { @@ -406,7 +465,7 @@ export function parseFriendlyError( } else if (errorsList.length === 1) { friendlyMessage = errorsList[0]; } else { - // Fallback message if detail was empty string + // Fallback message ONLY if details property was empty friendlyMessage = operationIntent === 'creation' ? 'Failed to create record. Please verify the submitted parameters.' @@ -440,77 +499,103 @@ export function parseFriendlyError( let category: ErrorCategory = 'general'; let categoryLabel: string = 'Error'; - switch (statusCode) { - case 400: - category = operationIntent === 'creation' ? 'creation' : operationIntent === 'update' ? 'update' : 'validation'; - categoryLabel = 'Bad Request'; - message = - operationIntent === 'creation' - ? 'The new record could not be created. Please review the submitted parameters.' - : operationIntent === 'update' - ? 'The record could not be updated. Please verify the submitted parameters.' - : 'The request could not be processed due to invalid parameters.'; - break; + // Check if error or response data has a detail or details property + const directDetail = + anyErr?.response?.data?.detail ?? + anyErr?.response?.data?.Detail ?? + anyErr?.response?.data?.details ?? + anyErr?.response?.data?.Details ?? + anyErr?.detail ?? + anyErr?.Detail ?? + anyErr?.details ?? + anyErr?.Details; - case 404: - title = 'Record Not Found'; - category = 'notFound'; - categoryLabel = 'Not Found'; - message = 'The requested resource could not be found (HTTP 404). It may have been archived, removed, or the link is outdated.'; - break; + const fallbackErrorsList: string[] = []; - case 409: - title = 'Resource Conflict'; - category = 'conflict'; - categoryLabel = 'Conflict'; - message = 'This operation conflicts with the current status of the resource (HTTP 409). Please refresh the page and try again.'; - break; + if (directDetail) { + if (typeof directDetail === 'string' && directDetail.trim()) { + fallbackErrorsList.push(...parseCommaDelimitedErrors(directDetail.trim())); + message = fallbackErrorsList.join('\n'); + } else if (Array.isArray(directDetail)) { + fallbackErrorsList.push(...directDetail.map(String)); + message = fallbackErrorsList.join('\n'); + } + } - case 422: - category = 'validation'; - categoryLabel = 'Incomplete Data'; - message = 'The submitted information could not be processed by the server (HTTP 422). Please verify all required fields.'; - break; + if (!message) { + switch (statusCode) { + case 400: + category = operationIntent === 'creation' ? 'creation' : operationIntent === 'update' ? 'update' : 'validation'; + categoryLabel = 'Bad Request'; + message = + operationIntent === 'creation' + ? 'The new record could not be created. Please review the submitted parameters.' + : operationIntent === 'update' + ? 'The record could not be updated. Please verify the submitted parameters.' + : 'The request could not be processed due to invalid parameters.'; + break; - case 429: - title = 'Rate Limit Exceeded'; - category = 'server'; - categoryLabel = 'Rate Limit'; - message = 'The system received too many requests in a short time (HTTP 429). Please pause for a moment before trying again.'; - break; + case 404: + title = 'Record Not Found'; + category = 'notFound'; + categoryLabel = 'Not Found'; + message = 'The requested resource could not be found (HTTP 404). It may have been archived, removed, or the link is outdated.'; + break; - case 500: - case 502: - case 503: - case 504: - title = 'Service Temporarily Unavailable'; - category = 'server'; - categoryLabel = 'Service Issue'; - message = `The server encountered an error while processing your request (HTTP ${statusCode}). Please try again in a few moments.`; - break; + case 409: + title = 'Resource Conflict'; + category = 'conflict'; + categoryLabel = 'Conflict'; + message = 'This operation conflicts with the current status of the resource (HTTP 409). Please refresh the page and try again.'; + break; - default: - if (typeof error === 'string' && error.trim()) { - message = cleanTechnicalJargon(error); - } else if (error instanceof Error && error.message) { - message = cleanTechnicalJargon(error.message); - } - if (!message) { - message = statusCode - ? `An unexpected server issue occurred (HTTP ${statusCode}). Please try again or contact support.` - : 'An unexpected issue occurred while processing this request. Please try again.'; - } - break; + case 422: + category = 'validation'; + categoryLabel = 'Incomplete Data'; + message = 'The submitted information could not be processed by the server (HTTP 422). Please verify all required fields.'; + break; + + case 429: + title = 'Rate Limit Exceeded'; + category = 'server'; + categoryLabel = 'Rate Limit'; + message = 'The system received too many requests in a short time (HTTP 429). Please pause for a moment before trying again.'; + break; + + case 500: + case 502: + case 503: + case 504: + title = 'Service Temporarily Unavailable'; + category = 'server'; + categoryLabel = 'Service Issue'; + message = `The server encountered an error while processing your request (HTTP ${statusCode}). Please try again in a few moments.`; + break; + + default: + if (typeof error === 'string' && error.trim()) { + message = cleanTechnicalJargon(error); + } else if (error instanceof Error && error.message) { + message = cleanTechnicalJargon(error.message); + } + if (!message) { + message = statusCode + ? `An unexpected server issue occurred (HTTP ${statusCode}). Please try again or contact support.` + : 'An unexpected issue occurred while processing this request. Please try again.'; + } + break; + } } return { title, message, statusCode, - hasReturnObject: false, + hasReturnObject: fallbackErrorsList.length > 0, isProblemDetails: false, category, categoryLabel, + errorsList: fallbackErrorsList.length > 0 ? fallbackErrorsList : undefined, technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : (statusCode ? `HTTP Status ${statusCode}` : undefined), }; }