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 ? ( -
-
- {/* 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 */} | 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), }; }