@@ -3,7 +3,7 @@ import type Schedule1Response from '@/interfaces/Schedule1Response'
33import type { LineItem } from '@/interfaces/Schedule1Response'
44import type Schedule1Request from '@/interfaces/Schedule1Request'
55import type CheckStatusResponse from '@/interfaces/CheckStatusResponse'
6- import { useCallback , useState } from 'react'
6+ import { useState } from 'react'
77import { useNavigate } from '@tanstack/react-router'
88import {
99 Button ,
@@ -21,11 +21,10 @@ import {
2121 TextArea ,
2222 TextInput ,
2323} from '@carbon/react'
24- import apiService from '@/service/api-service'
2524import { WRITABLE_LINE_ITEM_CODES } from '@/interfaces/Schedule1Request'
26- import useMillYear from '@/context/millYear/useMillYear '
25+ import { useScheduleContextGuard } from '@/hooks/useScheduleContextGuard '
2726import { useScheduleDocument } from '@/hooks/useScheduleDocument'
28- import { extractDetail } from '@/utils/error '
27+ import { useScheduleMutations } from '@/hooks/useScheduleMutations '
2928import { fmtCurrency , fmtNumber , groupInput , numStrGroup , toNum } from '@/utils/number'
3029import LoadingScreen from '@/components/core/LoadingScreen'
3130import NotificationColumn from '@/components/core/NotificationColumn'
@@ -123,25 +122,30 @@ function buildRequest(doc: Schedule1Response, form: FieldValues): Schedule1Reque
123122}
124123
125124const Schedule1 : FC = ( ) => {
126- const { millId, year } = useMillYear ( )
125+ const { millId, year, contextMissing , isCurrent } = useScheduleContextGuard ( )
127126 const navigate = useNavigate ( )
128- const contextMissing = millId === null || year === null
129127
130- const [ saving , setSaving ] = useState ( false )
131- const [ saveMessage , setSaveMessage ] = useState < string | null > ( null )
132- const [ saveError , setSaveError ] = useState < string | null > ( null )
128+ // Save/delete/check-status all run through the shared hook's guarded run() (Story 29.6): a stale
129+ // in-flight write can no longer repaint a newly-switched mill/year. `saving` is the single in-flight
130+ // lock for every write (it also gates Check Status), replacing the old separate save/checking locks.
131+ const {
132+ saving,
133+ message : saveMessage ,
134+ actionError : saveError ,
135+ checkResult,
136+ setMessage : setSaveMessage ,
137+ setActionError : setSaveError ,
138+ setCheckResult,
139+ clearBanners,
140+ resetBanners,
141+ save,
142+ remove,
143+ checkStatus,
144+ } = useScheduleMutations < CheckStatusResponse > ( { path : '/v1/schedule1' , millId, year, isCurrent } )
145+
133146 const [ confirmDeleteOpen , setConfirmDeleteOpen ] = useState ( false )
134147 const [ confirmNavOpen , setConfirmNavOpen ] = useState ( false )
135148 const [ otherCostsBlockedOpen , setOtherCostsBlockedOpen ] = useState ( false )
136- const [ checking , setChecking ] = useState ( false )
137- const [ checkResult , setCheckResult ] = useState < CheckStatusResponse | null > ( null )
138-
139- // Clear the save/check notifications whenever a fresh document loads (mill/year change).
140- const resetMessages = useCallback ( ( ) => {
141- setSaveMessage ( null )
142- setSaveError ( null )
143- setCheckResult ( null )
144- } , [ ] )
145149
146150 const { data, setData, form, setForm, setField, errorDetail, isLoading } =
147151 useScheduleDocument < Schedule1Response > ( {
@@ -151,7 +155,7 @@ const Schedule1: FC = () => {
151155 contextMissing,
152156 seedForm,
153157 mapLoadError : mapLoadErrorDetail ,
154- onReset : resetMessages ,
158+ onReset : resetBanners ,
155159 } )
156160
157161 // Re-group a numeric field's value on blur, so it reads like the plain-text cells beside it. Only
@@ -177,44 +181,30 @@ const Schedule1: FC = () => {
177181 setSaveError ( 'Please correct the highlighted fields before saving.' )
178182 return
179183 }
180- setSaving ( true )
181- setSaveMessage ( null )
182- setSaveError ( null )
183- setCheckResult ( null ) // a prior Check Status result is stale once the data changes
184- apiService
185- . getAxiosInstance ( )
186- . put < Schedule1Response > (
187- `/v1/schedule1?millId=${ millId } &year=${ year } ` ,
188- buildRequest ( data , form ) ,
189- )
190- . then ( ( response ) => {
191- setData ( response . data )
192- setForm ( seedForm ( response . data ) )
184+ clearBanners ( ) // drop any prior banners incl. a now-stale Check Status result
185+ save < Schedule1Response > ( buildRequest ( data , form ) , {
186+ fallback : 'Schedule could not be saved.' ,
187+ onSuccess : ( doc ) => {
188+ setData ( doc )
189+ setForm ( seedForm ( doc ) )
193190 // SUC-001 verbatim from the API message field (AD-8), never hardcoded.
194- setSaveMessage ( response . data . message ?. text ?? null )
195- } )
196- . catch ( ( error : unknown ) => {
197- // Keep the entered values (S23/S24); surface the API's verbatim ProblemDetail.detail.
198- setSaveError ( extractDetail ( error ) || 'Schedule could not be saved.' )
199- } )
200- . finally ( ( ) => setSaving ( false ) )
191+ setSaveMessage ( doc . message ?. text ?? null )
192+ } ,
193+ } )
201194 }
202195
203196 const handleDelete = ( ) => {
204197 if ( saving ) {
205198 return
206199 }
207200 setConfirmDeleteOpen ( false )
208- setSaving ( true )
209- setSaveMessage ( null )
210- setSaveError ( null )
211- setCheckResult ( null ) // the deleted schedule's check result is stale
212- apiService
213- . getAxiosInstance ( )
214- . delete < { message ?: { text ?: string } } > ( `/v1/schedule1?millId=${ millId } &year=${ year } ` )
215- . then ( ( response ) => {
216- // Delete removed the summary; a re-GET would 404, so reset to an empty schedule in place
217- // (no re-fetch) and show SUC-002 from the API message.
201+ clearBanners ( ) // the deleted schedule's check result / save banner are stale
202+ remove < { message ?: { text ?: string } } > ( {
203+ fallback : 'Unable to delete Schedule 1.' ,
204+ // Delete removed the summary; a re-GET would 404, so reset to an empty schedule in place (no
205+ // re-fetch) and show SUC-002 from the API message. This per-page empty-state lives at the call
206+ // site (Story 29.6): single-doc Schedules 1/3 reset in place; list pages re-seed from a reload.
207+ onSuccess : ( resp ) => {
218208 setData ( ( prev ) =>
219209 prev
220210 ? {
@@ -236,32 +226,20 @@ const Schedule1: FC = () => {
236226 : prev ,
237227 )
238228 setForm ( { } )
239- setSaveMessage ( response . data ?. message ?. text ?? null )
240- } )
241- . catch ( ( error : unknown ) => {
242- setSaveError ( extractDetail ( error ) || 'Unable to delete Schedule 1.' )
243- } )
244- . finally ( ( ) => setSaving ( false ) )
229+ setSaveMessage ( resp ?. message ?. text ?? null )
230+ } ,
231+ } )
245232 }
246233
247234 const handleCheckStatus = ( ) => {
248- if ( ! data || checking || saving ) {
235+ if ( ! data || saving ) {
249236 return
250237 }
251- setChecking ( true )
252- setCheckResult ( null )
253- setSaveError ( null )
254- setSaveMessage ( null ) // don't leave a stale Save success banner beside a new check result
255- apiService
256- . getAxiosInstance ( )
257- . post < CheckStatusResponse > ( `/v1/schedule1/check-status?millId=${ millId } &year=${ year } ` )
258- . then ( ( response ) => {
259- setCheckResult ( response . data )
260- } )
261- . catch ( ( error : unknown ) => {
262- setSaveError ( extractDetail ( error ) || 'Unable to check status.' )
263- } )
264- . finally ( ( ) => setChecking ( false ) )
238+ clearBanners ( ) // don't leave a stale Save success banner beside a new check result
239+ checkStatus < CheckStatusResponse > ( {
240+ fallback : 'Unable to check status.' ,
241+ onSuccess : setCheckResult ,
242+ } )
265243 }
266244
267245 const handleOtherCosts = ( ) => {
@@ -541,7 +519,9 @@ const Schedule1: FC = () => {
541519 className = "schedule-1__actions"
542520 editable = { editable }
543521 saving = { saving }
544- checking = { checking }
522+ // Check Status now shares the single `saving` lock (it runs through the same run()); there is no
523+ // separate checking flag, so both Save and Check disable together while any write is in flight.
524+ checking = { false }
545525 onSave = { handleSave }
546526 onCheckStatus = { handleCheckStatus }
547527 onDelete = { ( ) => setConfirmDeleteOpen ( true ) }
0 commit comments