diff --git a/src/components/ui/retry-status-indicator.tsx b/src/components/ui/retry-status-indicator.tsx index 73a68dd7..4955f100 100644 --- a/src/components/ui/retry-status-indicator.tsx +++ b/src/components/ui/retry-status-indicator.tsx @@ -1,56 +1,168 @@ import { useEffect, useState } from 'react'; -import { Loader2 } from 'lucide-react'; +import { Loader2, CheckCircle2, XCircle } from 'lucide-react'; import { Card } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; +import { Button } from '@/components/ui/button'; interface RetryStatus { + id: string; attempt: number; maxAttempts: number; delay: number; type: string; + state: 'retrying' | 'success' | 'failed'; + errorId?: string; } /** * Global retry status indicator * Shows visual feedback when submissions are being retried due to transient failures + * Supports success/failure states and multiple concurrent retries */ export function RetryStatusIndicator() { - const [retryStatus, setRetryStatus] = useState(null); - const [countdown, setCountdown] = useState(0); + const [retries, setRetries] = useState>(new Map()); useEffect(() => { const handleRetry = (event: Event) => { - const customEvent = event as CustomEvent; - const { attempt, maxAttempts, delay, type } = customEvent.detail; - setRetryStatus({ attempt, maxAttempts, delay, type }); - setCountdown(delay); + const customEvent = event as CustomEvent>; + const { id, attempt, maxAttempts, delay, type } = customEvent.detail; + + setRetries(prev => { + const next = new Map(prev); + next.set(id, { id, attempt, maxAttempts, delay, type, state: 'retrying', countdown: delay }); + return next; + }); + }; + + const handleSuccess = (event: Event) => { + const customEvent = event as CustomEvent<{ id: string }>; + const { id } = customEvent.detail; + + setRetries(prev => { + const retry = prev.get(id); + if (!retry) return prev; + + const next = new Map(prev); + next.set(id, { ...retry, state: 'success', countdown: 0 }); + return next; + }); + + // Remove after 2 seconds + setTimeout(() => { + setRetries(prev => { + const next = new Map(prev); + next.delete(id); + return next; + }); + }, 2000); + }; + + const handleFailure = (event: Event) => { + const customEvent = event as CustomEvent<{ id: string; errorId: string }>; + const { id, errorId } = customEvent.detail; + + setRetries(prev => { + const retry = prev.get(id); + if (!retry) return prev; + + const next = new Map(prev); + next.set(id, { ...retry, state: 'failed', errorId, countdown: 0 }); + return next; + }); }; window.addEventListener('submission-retry', handleRetry); - return () => window.removeEventListener('submission-retry', handleRetry); + window.addEventListener('submission-retry-success', handleSuccess); + window.addEventListener('submission-retry-failed', handleFailure); + + return () => { + window.removeEventListener('submission-retry', handleRetry); + window.removeEventListener('submission-retry-success', handleSuccess); + window.removeEventListener('submission-retry-failed', handleFailure); + }; }, []); + // Countdown timer for retrying state useEffect(() => { - if (countdown > 0) { - const timer = setInterval(() => { - setCountdown((prev) => { - if (prev <= 100) { - setRetryStatus(null); - return 0; + const timer = setInterval(() => { + setRetries(prev => { + let hasChanges = false; + const next = new Map(prev); + + next.forEach((retry, id) => { + if (retry.state === 'retrying' && retry.countdown > 0) { + const newCountdown = retry.countdown - 100; + next.set(id, { ...retry, countdown: Math.max(0, newCountdown) }); + hasChanges = true; } - return prev - 100; }); - }, 100); - return () => clearInterval(timer); - } - }, [countdown]); + + return hasChanges ? next : prev; + }); + }, 100); + + return () => clearInterval(timer); + }, []); - if (!retryStatus) return null; - - const progress = ((retryStatus.delay - countdown) / retryStatus.delay) * 100; + if (retries.size === 0) return null; return ( - +
+ {Array.from(retries.values()).map((retry) => ( + + ))} +
+ ); +} + +function RetryCard({ retry }: { retry: RetryStatus & { countdown: number } }) { + if (retry.state === 'success') { + return ( + +
+ +

+ {retry.type} submitted successfully! +

+
+
+ ); + } + + if (retry.state === 'failed') { + return ( + +
+ +
+

+ Submission failed +

+ {retry.errorId && ( + <> +

+ Error ID: {retry.errorId} +

+ + + )} +
+
+
+ ); + } + + // Retrying state + const progress = retry.delay > 0 ? ((retry.delay - retry.countdown) / retry.delay) * 100 : 0; + + return ( +
@@ -59,12 +171,12 @@ export function RetryStatusIndicator() { Retrying submission...

- {retryStatus.attempt}/{retryStatus.maxAttempts} + {retry.attempt}/{retry.maxAttempts}

- Network issue detected. Retrying {retryStatus.type} submission in {Math.ceil(countdown / 1000)}s + Network issue detected. Retrying {retry.type} submission in {Math.ceil(retry.countdown / 1000)}s

diff --git a/src/lib/companyHelpers.ts b/src/lib/companyHelpers.ts index c58843da..09dd81df 100644 --- a/src/lib/companyHelpers.ts +++ b/src/lib/companyHelpers.ts @@ -49,6 +49,8 @@ export async function submitCompanyCreation( } // Create submission with retry logic + const retryId = crypto.randomUUID(); + const result = await withRetry( async () => { // Create the main submission record @@ -99,7 +101,7 @@ export async function submitCompanyCreation( // Emit event for UI indicator window.dispatchEvent(new CustomEvent('submission-retry', { - detail: { attempt, maxAttempts: 3, delay, type: companyType } + detail: { id: retryId, attempt, maxAttempts: 3, delay, type: companyType } })); }, shouldRetry: (error) => { @@ -116,11 +118,23 @@ export async function submitCompanyCreation( return isRetryableError(error); } } - ).catch((error) => { - handleError(error, { + ).then((data) => { + // Emit success event + window.dispatchEvent(new CustomEvent('submission-retry-success', { + detail: { id: retryId } + })); + return data; + }).catch((error) => { + const errorId = handleError(error, { action: `${companyType} submission`, metadata: { retriesExhausted: true }, }); + + // Emit failure event + window.dispatchEvent(new CustomEvent('submission-retry-failed', { + detail: { id: retryId, errorId } + })); + throw error; }); @@ -178,6 +192,8 @@ export async function submitCompanyUpdate( } // Create submission with retry logic + const retryId = crypto.randomUUID(); + const result = await withRetry( async () => { // Create the main submission record @@ -230,7 +246,7 @@ export async function submitCompanyUpdate( // Emit event for UI indicator window.dispatchEvent(new CustomEvent('submission-retry', { - detail: { attempt, maxAttempts: 3, delay, type: `${existingCompany.company_type} update` } + detail: { id: retryId, attempt, maxAttempts: 3, delay, type: `${existingCompany.company_type} update` } })); }, shouldRetry: (error) => { @@ -247,11 +263,23 @@ export async function submitCompanyUpdate( return isRetryableError(error); } } - ).catch((error) => { - handleError(error, { + ).then((data) => { + // Emit success event + window.dispatchEvent(new CustomEvent('submission-retry-success', { + detail: { id: retryId } + })); + return data; + }).catch((error) => { + const errorId = handleError(error, { action: `${existingCompany.company_type} update`, metadata: { retriesExhausted: true, companyId }, }); + + // Emit failure event + window.dispatchEvent(new CustomEvent('submission-retry-failed', { + detail: { id: retryId, errorId } + })); + throw error; }); diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index e381c8a5..9cb1c597 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -744,6 +744,8 @@ export async function submitParkUpdate( let processedImages = data.images; // Main submission logic with retry and error handling + const retryId = crypto.randomUUID(); + const result = await withRetry( async () => { // Create the main submission record @@ -796,7 +798,7 @@ export async function submitParkUpdate( // Emit event for UI retry indicator window.dispatchEvent(new CustomEvent('submission-retry', { - detail: { attempt, maxAttempts: 3, delay, type: 'park update' } + detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'park update' } })); }, shouldRetry: (error) => { @@ -814,12 +816,24 @@ export async function submitParkUpdate( return isRetryableError(error); } } - ).catch((error) => { - handleError(error, { + ).then((data) => { + // Emit success event + window.dispatchEvent(new CustomEvent('submission-retry-success', { + detail: { id: retryId } + })); + return data; + }).catch((error) => { + const errorId = handleError(error, { action: 'Park update submission', userId, metadata: { retriesExhausted: true, parkId }, }); + + // Emit failure event + window.dispatchEvent(new CustomEvent('submission-retry-failed', { + detail: { id: retryId, errorId } + })); + throw error; }); @@ -989,6 +1003,8 @@ export async function submitRideCreation( } // Create submission with retry logic + const retryId = crypto.randomUUID(); + const result = await withRetry( async () => { // Create the main submission record @@ -1079,7 +1095,7 @@ export async function submitRideCreation( // Emit event for UI indicator window.dispatchEvent(new CustomEvent('submission-retry', { - detail: { attempt, maxAttempts: 3, delay, type: 'ride' } + detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'ride' } })); }, shouldRetry: (error) => { @@ -1096,11 +1112,23 @@ export async function submitRideCreation( return isRetryableError(error); } } - ).catch((error) => { - handleError(error, { + ).then((data) => { + // Emit success event + window.dispatchEvent(new CustomEvent('submission-retry-success', { + detail: { id: retryId } + })); + return data; + }).catch((error) => { + const errorId = handleError(error, { action: 'Ride submission', metadata: { retriesExhausted: true }, }); + + // Emit failure event + window.dispatchEvent(new CustomEvent('submission-retry-failed', { + detail: { id: retryId, errorId } + })); + throw error; }); @@ -1178,6 +1206,8 @@ export async function submitRideUpdate( let processedImages = data.images; // Main submission logic with retry and error handling + const retryId = crypto.randomUUID(); + const result = await withRetry( async () => { // Create the main submission record @@ -1230,7 +1260,7 @@ export async function submitRideUpdate( // Emit event for UI retry indicator window.dispatchEvent(new CustomEvent('submission-retry', { - detail: { attempt, maxAttempts: 3, delay, type: 'ride update' } + detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'ride update' } })); }, shouldRetry: (error) => { @@ -1248,12 +1278,24 @@ export async function submitRideUpdate( return isRetryableError(error); } } - ).catch((error) => { - handleError(error, { + ).then((data) => { + // Emit success event + window.dispatchEvent(new CustomEvent('submission-retry-success', { + detail: { id: retryId } + })); + return data; + }).catch((error) => { + const errorId = handleError(error, { action: 'Ride update submission', userId, metadata: { retriesExhausted: true, rideId }, }); + + // Emit failure event + window.dispatchEvent(new CustomEvent('submission-retry-failed', { + detail: { id: retryId, errorId } + })); + throw error; });