@@ -47,6 +47,46 @@ function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
4747 // rating/feedback
4848 const [ rating , setRating ] = useState ( 0 ) ;
4949 const [ feedback , setFeedback ] = useState ( "" ) ;
50+ // non-blocking warning when status check fails
51+ const [ statusCheckFailed , setStatusCheckFailed ] = useState ( false ) ;
52+
53+ // Helper function to handle status check errors consistently
54+ const handleStatusCheckError = (
55+ error : unknown ,
56+ logContext : Record < string , string | number | boolean > ,
57+ ) => {
58+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
59+ const errorType = / n e t w o r k | f e t c h / i. test ( errorMessage )
60+ ? "network"
61+ : / A P I S e r v e r e r r o r / i. test ( errorMessage )
62+ ? "server"
63+ : / I n t e r n a l s e r v e r e r r o r / i. test ( errorMessage )
64+ ? "server"
65+ : "unknown" ;
66+
67+ // Report analytics for status check failure
68+ reportSupportFeedbackStatusCheckFailed ( {
69+ ticketId : ticket . id ,
70+ errorMessage,
71+ errorType,
72+ } ) ;
73+
74+ // Set degraded state for warning banner
75+ setStatusCheckFailed ( true ) ;
76+
77+ // Log the error
78+ console . warn (
79+ "[SUPPORT_FEEDBACK_CLIENT] Status check failed, but allowing feedback submission" ,
80+ {
81+ ...logContext ,
82+ error : errorMessage ,
83+ } ,
84+ ) ;
85+
86+ // Throw a custom error that React Query will catch, but the UI will handle gracefully
87+ // This prevents treating the error as "no feedback exists" which could cause duplicates
88+ throw new Error ( `Status check failed: ${ errorMessage } ` ) ;
89+ } ;
5090
5191 // Check if feedback has already been submitted for this ticket
5292 const feedbackStatusQuery = useQuery ( {
@@ -75,32 +115,8 @@ function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
75115 } ,
76116 ) ;
77117
78- // Report analytics for status check failure
79- const errorType = / n e t w o r k | f e t c h / i. test ( result . error )
80- ? "network"
81- : / A P I S e r v e r e r r o r / i. test ( result . error )
82- ? "server"
83- : / I n t e r n a l s e r v e r e r r o r / i. test ( result . error )
84- ? "server"
85- : "unknown" ;
86-
87- reportSupportFeedbackStatusCheckFailed ( {
88- ticketId : ticket . id ,
89- errorMessage : result . error ,
90- errorType,
91- } ) ;
92-
93- // Don't throw error for status check failures - allow user to still submit feedback
94- console . warn (
95- "[SUPPORT_FEEDBACK_CLIENT] Status check failed, but allowing feedback submission" ,
96- {
97- ...logContext ,
98- error : result . error ,
99- } ,
100- ) ;
101-
102- // Return false to indicate no feedback found (allow submission)
103- return false ;
118+ // Use shared error handler to maintain consistent behavior
119+ return handleStatusCheckError ( result . error , logContext ) ;
104120 }
105121
106122 console . log (
@@ -112,6 +128,9 @@ function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
112128 } ,
113129 ) ;
114130
131+ // Clear degraded state on success
132+ if ( statusCheckFailed ) setStatusCheckFailed ( false ) ;
133+
115134 // Additional debugging for production
116135 if ( result . hasFeedback ) {
117136 console . log (
@@ -139,34 +158,8 @@ function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
139158 errorName : error instanceof Error ? error . name : "Unknown" ,
140159 } ) ;
141160
142- // Report analytics for status check failure
143- const errorType = / n e t w o r k | f e t c h / i. test (
144- error instanceof Error ? error . message : String ( error ) ,
145- )
146- ? "network"
147- : / A P I S e r v e r e r r o r / i. test (
148- error instanceof Error ? error . message : String ( error ) ,
149- )
150- ? "server"
151- : "unknown" ;
152-
153- reportSupportFeedbackStatusCheckFailed ( {
154- ticketId : ticket . id ,
155- errorMessage : error instanceof Error ? error . message : String ( error ) ,
156- errorType,
157- } ) ;
158-
159- // Don't throw error - allow user to still submit feedback
160- console . warn (
161- "[SUPPORT_FEEDBACK_CLIENT] Status check failed with exception, but allowing feedback submission" ,
162- {
163- ...logContext ,
164- error : error instanceof Error ? error . message : String ( error ) ,
165- } ,
166- ) ;
167-
168- // Return false to indicate no feedback found (allow submission)
169- return false ;
161+ // Use shared error handler to maintain consistent behavior
162+ return handleStatusCheckError ( error , logContext ) ;
170163 }
171164 } ,
172165 enabled : ticket . status === "closed" ,
@@ -176,7 +169,8 @@ function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
176169
177170 const feedbackSubmitted = feedbackStatusQuery . data ?? false ;
178171 const isLoading = feedbackStatusQuery . isLoading ;
179- const hasError = feedbackStatusQuery . isError ;
172+ // query never throws; use local degraded flag for the inline warning
173+ const hasError = statusCheckFailed ;
180174
181175 const handleStarClick = ( starIndex : number ) => {
182176 setRating ( starIndex + 1 ) ;
@@ -508,75 +502,77 @@ function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
508502 </ div >
509503 ) }
510504
511- { ticket . status === "closed" && ! isLoading && ! feedbackSubmitted && (
512- < div className = "border-t p-6" >
513- < p className = "text-muted-foreground text-sm" >
514- This ticket is closed. Give us a quick rating to let us know how
515- we did!
516- </ p >
517- { hasError && (
518- < div className = "mt-2" >
519- < p className = "text-destructive text-xs" >
520- Couldn't verify prior feedback right now — you can still
521- submit a rating.
522- </ p >
505+ { ticket . status === "closed" &&
506+ ! isLoading &&
507+ ( ! feedbackSubmitted || hasError ) && (
508+ < div className = "border-t p-6" >
509+ < p className = "text-muted-foreground text-sm" >
510+ This ticket is closed. Give us a quick rating to let us know how
511+ we did!
512+ </ p >
513+ { hasError && (
514+ < div className = "mt-2" >
515+ < p className = "text-destructive text-xs" >
516+ Couldn't verify prior feedback right now — you can still
517+ submit a rating.
518+ </ p >
519+ </ div >
520+ ) }
521+
522+ < div className = "flex gap-2 mb-6 mt-4" >
523+ { [ 1 , 2 , 3 , 4 , 5 ] . map ( ( starValue ) => (
524+ < button
525+ key = { `star-${ starValue } ` }
526+ type = "button"
527+ onClick = { ( ) => handleStarClick ( starValue - 1 ) }
528+ className = "transition-colors"
529+ aria-label = { `Rate ${ starValue } out of 5 stars` }
530+ >
531+ < StarIcon
532+ size = { 32 }
533+ className = { cn (
534+ "transition-colors" ,
535+ starValue <= rating
536+ ? "text-pink-500 fill-current stroke-current"
537+ : "text-muted-foreground fill-current stroke-current" ,
538+ "hover:text-pink-500" ,
539+ ) }
540+ strokeWidth = { starValue <= rating ? 2 : 1 }
541+ />
542+ </ button >
543+ ) ) }
523544 </ div >
524- ) }
525545
526- < div className = "flex gap-2 mb-6 mt-4" >
527- { [ 1 , 2 , 3 , 4 , 5 ] . map ( ( starValue ) => (
528- < button
529- key = { `star-${ starValue } ` }
546+ < div className = "relative" >
547+ < AutoResizeTextarea
548+ value = { feedback }
549+ onChange = { ( e ) => setFeedback ( e . target . value ) }
550+ placeholder = "Optional: Tell us how we can improve."
551+ maxLength = { 1000 }
552+ className = "text-sm w-full bg-card text-foreground rounded-lg p-4 pr-28 min-h-[100px] resize-none border border-border focus:outline-none placeholder:text-muted-foreground"
553+ />
554+ < Button
530555 type = "button"
531- onClick = { ( ) => handleStarClick ( starValue - 1 ) }
532- className = "transition-colors"
533- aria-label = { `Rate ${ starValue } out of 5 stars` }
556+ onClick = { handleSendFeedback }
557+ disabled = { submitFeedbackMutation . isPending || rating === 0 }
558+ className = "absolute bottom-3 right-3 rounded-full h-auto py-2 px-4"
559+ variant = "secondary"
560+ size = "sm"
534561 >
535- < StarIcon
536- size = { 32 }
537- className = { cn (
538- "transition-colors" ,
539- starValue <= rating
540- ? "text-pink-500 fill-current stroke-current"
541- : "text-muted-foreground fill-current stroke-current" ,
542- "hover:text-pink-500" ,
543- ) }
544- strokeWidth = { starValue <= rating ? 2 : 1 }
545- />
546- </ button >
547- ) ) }
548- </ div >
549-
550- < div className = "relative" >
551- < AutoResizeTextarea
552- value = { feedback }
553- onChange = { ( e ) => setFeedback ( e . target . value ) }
554- placeholder = "Optional: Tell us how we can improve."
555- maxLength = { 1000 }
556- className = "text-sm w-full bg-card text-foreground rounded-lg p-4 pr-28 min-h-[100px] resize-none border border-border focus:outline-none placeholder:text-muted-foreground"
557- />
558- < Button
559- type = "button"
560- onClick = { handleSendFeedback }
561- disabled = { submitFeedbackMutation . isPending || rating === 0 }
562- className = "absolute bottom-3 right-3 rounded-full h-auto py-2 px-4"
563- variant = "secondary"
564- size = "sm"
565- >
566- { submitFeedbackMutation . isPending ? (
567- < >
568- < Spinner className = "size-4 mr-2" />
569- Sending...
570- </ >
571- ) : (
572- "Send Feedback"
573- ) }
574- </ Button >
562+ { submitFeedbackMutation . isPending ? (
563+ < >
564+ < Spinner className = "size-4 mr-2" />
565+ Sending...
566+ </ >
567+ ) : (
568+ "Send Feedback"
569+ ) }
570+ </ Button >
571+ </ div >
575572 </ div >
576- </ div >
577- ) }
573+ ) }
578574
579- { ticket . status === "closed" && feedbackSubmitted && (
575+ { ticket . status === "closed" && feedbackSubmitted && ! hasError && (
580576 < div className = "border-t p-6" >
581577 < p className = "text-muted-foreground text-sm" >
582578 Thank you for your feedback! We appreciate your input and will use
0 commit comments