77 */
88import PropTypes from 'prop-types' ;
99import { useCallback , useEffect , useState } from 'react' ;
10- import { fetchBugReport , fetchBugReports } from '../lib/bug-report-api.js' ;
10+ import { fetchBugReport , fetchBugReports , updateBugReport } from '../lib/bug-report-api.js' ;
1111
1212const STATUS_LABELS = {
1313 open : '未対応' ,
@@ -44,17 +44,56 @@ const hideBrokenImage = event => {
4444 event . currentTarget . style . display = 'none' ;
4545} ;
4646
47- const BugReportDetail = ( { reportId, onBack} ) => {
47+ const BugReportDetail = ( { reportId, onBack, onChanged } ) => {
4848 const [ detail , setDetail ] = useState ( null ) ;
4949 const [ error , setError ] = useState ( '' ) ;
50+ const [ nextStatus , setNextStatus ] = useState ( '' ) ;
51+ const [ reply , setReply ] = useState ( '' ) ;
52+ const [ confirming , setConfirming ] = useState ( false ) ;
53+ const [ busy , setBusy ] = useState ( false ) ;
54+ const [ savedAt , setSavedAt ] = useState ( null ) ;
5055
5156 useEffect ( ( ) => {
5257 fetchBugReport ( reportId )
53- . then ( setDetail )
58+ . then ( data => {
59+ setDetail ( data ) ;
60+ setNextStatus ( data . status ) ;
61+ setReply ( data . developerReply || '' ) ;
62+ } )
5463 . catch ( err => setError ( err . message ) ) ;
5564 } , [ reportId ] ) ;
5665
57- if ( error ) {
66+ const dirty = detail &&
67+ ( nextStatus !== detail . status || reply !== ( detail . developerReply || '' ) ) ;
68+ const turnsTerminal = detail && nextStatus !== detail . status &&
69+ ( nextStatus === 'resolved' || nextStatus === 'wont_fix' ) ;
70+
71+ const handleStatusChange = useCallback ( e => setNextStatus ( e . target . value ) , [ ] ) ;
72+ const handleReplyChange = useCallback ( e => setReply ( e . target . value ) , [ ] ) ;
73+ const handleArm = useCallback ( ( ) => setConfirming ( true ) , [ ] ) ;
74+ const handleDisarm = useCallback ( ( ) => setConfirming ( false ) , [ ] ) ;
75+ const handleSave = useCallback ( async ( ) => {
76+ if ( ! detail ) return ;
77+ setBusy ( true ) ;
78+ setError ( '' ) ;
79+ try {
80+ // Send only what changed — the API rejects an empty update.
81+ const updates = { } ;
82+ if ( nextStatus !== detail . status ) updates . status = nextStatus ;
83+ if ( reply !== ( detail . developerReply || '' ) ) updates . developerReply = reply ;
84+ await updateBugReport ( reportId , updates ) ;
85+ setDetail ( prev => ( { ...prev , status : nextStatus , developerReply : reply } ) ) ;
86+ setConfirming ( false ) ;
87+ setSavedAt ( new Date ( ) . toISOString ( ) ) ;
88+ onChanged ( ) ;
89+ } catch ( err ) {
90+ setError ( err . message ) ;
91+ } finally {
92+ setBusy ( false ) ;
93+ }
94+ } , [ detail , nextStatus , reply , reportId , onChanged ] ) ;
95+
96+ if ( error && ! detail ) {
5897 return ( < p
5998 className = "admin-error"
6099 data-testid = "bug-admin-error"
@@ -78,12 +117,6 @@ const BugReportDetail = ({reportId, onBack}) => {
78117 { `報告者: ${ detail . ownerEmail || '(メール非公開)' } ・ ${ formatDate ( detail . createdAt ) } ` }
79118 </ p >
80119 < p data-testid = "bug-admin-description" > { detail . description } </ p >
81- { detail . developerReply ? (
82- < p
83- className = "admin-meta"
84- data-testid = "bug-admin-reply"
85- > { `開発者からの返信: ${ detail . developerReply } ` } </ p >
86- ) : null }
87120 { detail . appContext ? (
88121 < details >
89122 < summary > { 'アプリの状態 (appContext)' } </ summary >
@@ -118,12 +151,82 @@ const BugReportDetail = ({reportId, onBack}) => {
118151 onError = { hideBrokenImage }
119152 />
120153 ) ) }
154+ < div className = "admin-respond" >
155+ < h4 > { '対応(報告者に表示されます)' } </ h4 >
156+ < label className = "admin-meta" >
157+ { '状態: ' }
158+ < select
159+ data-testid = "bug-admin-status-select"
160+ disabled = { busy }
161+ value = { nextStatus }
162+ onChange = { handleStatusChange }
163+ >
164+ { Object . entries ( STATUS_LABELS ) . map ( ( [ value , label ] ) => (
165+ < option
166+ key = { value }
167+ value = { value }
168+ > { label } </ option >
169+ ) ) }
170+ </ select >
171+ </ label >
172+ < textarea
173+ data-testid = "bug-admin-reply-input"
174+ disabled = { busy }
175+ maxLength = { 2000 }
176+ placeholder = "進捗や対応内容のコメント(報告者の「私の不具合報告」に表示されます)"
177+ rows = { 4 }
178+ value = { reply }
179+ onChange = { handleReplyChange }
180+ />
181+ { error ? < p
182+ className = "admin-error"
183+ data-testid = "bug-admin-save-error"
184+ > { error } </ p > : null }
185+ < div className = "admin-actions" >
186+ { confirming ? (
187+ < span
188+ className = "admin-confirm"
189+ data-testid = "bug-admin-save-confirm"
190+ >
191+ { turnsTerminal ?
192+ 'この状態にすると報告と添付は一定期間後に自動削除されます。保存しますか?' :
193+ '状態・コメントを保存しますか?報告者にも表示されます。' }
194+ < button
195+ data-testid = "bug-admin-save-yes"
196+ disabled = { busy }
197+ type = "button"
198+ onClick = { handleSave }
199+ > { '保存する' } </ button >
200+ < button
201+ data-testid = "bug-admin-save-no"
202+ disabled = { busy }
203+ type = "button"
204+ onClick = { handleDisarm }
205+ > { 'やめる' } </ button >
206+ </ span >
207+ ) : (
208+ < button
209+ data-testid = "bug-admin-save"
210+ disabled = { ! dirty || busy }
211+ type = "button"
212+ onClick = { handleArm }
213+ > { '保存' } </ button >
214+ ) }
215+ { savedAt && ! dirty ? (
216+ < span
217+ className = "admin-meta"
218+ data-testid = "bug-admin-saved"
219+ > { '保存しました。' } </ span >
220+ ) : null }
221+ </ div >
222+ </ div >
121223 </ div >
122224 ) ;
123225} ;
124226
125227BugReportDetail . propTypes = {
126228 onBack : PropTypes . func . isRequired ,
229+ onChanged : PropTypes . func . isRequired ,
127230 reportId : PropTypes . string . isRequired
128231} ;
129232
@@ -133,14 +236,16 @@ const BugReportsView = () => {
133236 const [ selectedId , setSelectedId ] = useState ( null ) ;
134237 const [ error , setError ] = useState ( '' ) ;
135238
136- useEffect ( ( ) => {
239+ const reload = useCallback ( ( ) => {
137240 setError ( '' ) ;
138241 setReports ( null ) ;
139242 fetchBugReports ( status )
140243 . then ( data => setReports ( data . reports || [ ] ) )
141244 . catch ( err => setError ( err . message ) ) ;
142245 } , [ status ] ) ;
143246
247+ useEffect ( reload , [ reload ] ) ;
248+
144249 const handleStatusChange = useCallback ( e => setStatus ( e . target . value ) , [ ] ) ;
145250 const handleOpen = useCallback ( e => setSelectedId ( e . currentTarget . dataset . reportId ) , [ ] ) ;
146251 const handleBack = useCallback ( ( ) => setSelectedId ( null ) , [ ] ) ;
@@ -150,14 +255,15 @@ const BugReportsView = () => {
150255 < BugReportDetail
151256 reportId = { selectedId }
152257 onBack = { handleBack }
258+ onChanged = { reload }
153259 />
154260 ) ;
155261 }
156262
157263 return (
158264 < div data-testid = "bug-admin-view" >
159265 < p className = "admin-meta" >
160- { '閲覧専用です。状態の変更・返信は従来の運用手段で行ってください 。' }
266+ { '状態の変更とコメント(開発者からの返信)は、報告者の「私の不具合報告」にも反映されます 。' }
161267 </ p >
162268 < div className = "admin-search" >
163269 < select
0 commit comments