@@ -52,18 +52,139 @@ <h1 class="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-10
5252 < tr >
5353 < th > Date</ th > < th > Account</ th > < th > Description</ th >
5454 < th > Category</ th > < th class ="num "> Amount</ th > < th class ="num "> Balance</ th >
55+ < th > </ th >
5556 </ tr >
5657 </ thead >
57- < tbody id ="txn-tbody "> < tr > < td colspan ="6 " class ="empty "> Loading…</ td > </ tr > </ tbody >
58+ < tbody id ="txn-tbody "> < tr > < td colspan ="7 " class ="empty "> Loading…</ td > </ tr > </ tbody >
5859 </ table >
5960 < div class ="flex items-center justify-end gap-2 p-3 border-t border-zinc-100 bg-zinc-50/70 ">
6061 < button type ="button " class ="btn-ghost " id ="prev-page " onclick ="window.loadTxns(window.currentPage - 1) "> ‹ Prev</ button >
6162 < button type ="button " class ="btn-ghost " id ="next-page " onclick ="window.loadTxns(window.currentPage + 1) "> Next ›</ button >
6263 </ div >
6364</ div >
6465
66+ <!-- Edit modal: hidden until a row's "edit" button is clicked -->
67+ < div id ="edit-modal " class ="fixed inset-0 z-50 hidden items-center justify-center bg-zinc-900/60 ">
68+ < div class ="bg-white dark:bg-zinc-900 rounded-xl shadow-xl w-full max-w-md mx-4 p-5 ">
69+ < div class ="flex items-center justify-between mb-4 ">
70+ < h2 class ="text-lg font-semibold text-zinc-900 dark:text-zinc-100 "> Edit transaction</ h2 >
71+ < button type ="button " id ="edit-close " class ="text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 "> ✕</ button >
72+ </ div >
73+ < form id ="edit-form " class ="grid gap-3 ">
74+ < input type ="hidden " name ="id ">
75+ < label class ="filter-field "> Date
76+ < input name ="date " type ="date " required >
77+ </ label >
78+ < label class ="filter-field "> Description
79+ < input name ="description " type ="text " required >
80+ </ label >
81+ < div class ="grid grid-cols-2 gap-3 ">
82+ < label class ="filter-field "> Amount
83+ < input name ="amount " type ="number " step ="0.01 " required >
84+ </ label >
85+ < label class ="filter-field "> Balance
86+ < input name ="balance " type ="number " step ="0.01 ">
87+ </ label >
88+ </ div >
89+ < label class ="filter-field "> Category
90+ < select name ="category_id ">
91+ < option value =""> — Uncategorised —</ option >
92+ </ select >
93+ </ label >
94+ < div id ="edit-error " class ="text-sm text-rose-600 hidden "> </ div >
95+ < div class ="flex items-center justify-end gap-2 mt-2 ">
96+ < button type ="button " id ="edit-cancel " class ="px-3 py-1.5 text-sm rounded-md text-zinc-600 hover:text-zinc-900 dark:hover:text-zinc-100 "> Cancel</ button >
97+ < button type ="submit " class ="btn-primary "> Save</ button >
98+ </ div >
99+ </ form >
100+ </ div >
101+ </ div >
102+
65103< script >
66104 window . currentPage = 1 ;
105+ window . categories = [ ] ;
106+ window . txnsById = { } ;
107+
108+ function csrfToken ( ) {
109+ return ( document . cookie . match ( / (?: ^ | ; ) c s r f t o k e n = ( [ ^ ; ] + ) / ) || [ ] ) [ 1 ] || "" ;
110+ }
111+
112+ async function loadCategories ( ) {
113+ const d = window . dashboard ;
114+ window . categories = await d . fetchJson ( "/api/categories/" ) ;
115+ const select = document . querySelector ( "#edit-form select[name='category_id']" ) ;
116+ // Group options by kind for readability
117+ const byKind = { } ;
118+ for ( const c of window . categories ) {
119+ ( byKind [ c . kind ] ||= [ ] ) . push ( c ) ;
120+ }
121+ let html = '<option value="">— Uncategorised —</option>' ;
122+ for ( const kind of Object . keys ( byKind ) . sort ( ) ) {
123+ html += `<optgroup label="${ kind } ">` ;
124+ for ( const c of byKind [ kind ] ) html += `<option value="${ c . id } ">${ c . name } </option>` ;
125+ html += "</optgroup>" ;
126+ }
127+ select . innerHTML = html ;
128+ }
129+
130+ function openEdit ( t ) {
131+ const form = document . getElementById ( "edit-form" ) ;
132+ form . elements . id . value = t . id ;
133+ form . elements . date . value = t . date ;
134+ form . elements . description . value = t . description ;
135+ form . elements . amount . value = t . amount ;
136+ form . elements . balance . value = t . balance ?? "" ;
137+ form . elements . category_id . value = t . category ? t . category . id : "" ;
138+ document . getElementById ( "edit-error" ) . classList . add ( "hidden" ) ;
139+ document . getElementById ( "edit-modal" ) . classList . remove ( "hidden" ) ;
140+ document . getElementById ( "edit-modal" ) . classList . add ( "flex" ) ;
141+ }
142+
143+ function closeEdit ( ) {
144+ document . getElementById ( "edit-modal" ) . classList . add ( "hidden" ) ;
145+ document . getElementById ( "edit-modal" ) . classList . remove ( "flex" ) ;
146+ }
147+
148+ document . getElementById ( "edit-close" ) . addEventListener ( "click" , closeEdit ) ;
149+ document . getElementById ( "edit-cancel" ) . addEventListener ( "click" , closeEdit ) ;
150+ document . getElementById ( "edit-modal" ) . addEventListener ( "click" , e => {
151+ // Click on backdrop (the modal container itself) closes; clicks inside the panel bubble up but stop here
152+ if ( e . target . id === "edit-modal" ) closeEdit ( ) ;
153+ } ) ;
154+
155+ document . getElementById ( "edit-form" ) . addEventListener ( "submit" , async e => {
156+ e . preventDefault ( ) ;
157+ const form = e . currentTarget ;
158+ const id = form . elements . id . value ;
159+ const payload = {
160+ date : form . elements . date . value ,
161+ description : form . elements . description . value ,
162+ amount : form . elements . amount . value ,
163+ balance : form . elements . balance . value === "" ? null : form . elements . balance . value ,
164+ category_id : form . elements . category_id . value === "" ? null : parseInt ( form . elements . category_id . value , 10 ) ,
165+ } ;
166+ const err = document . getElementById ( "edit-error" ) ;
167+ err . classList . add ( "hidden" ) ;
168+ try {
169+ const resp = await fetch ( `/api/transactions/${ id } /` , {
170+ method : "PATCH" ,
171+ headers : { "Content-Type" : "application/json" , "X-CSRFToken" : csrfToken ( ) } ,
172+ credentials : "same-origin" ,
173+ body : JSON . stringify ( payload ) ,
174+ } ) ;
175+ if ( ! resp . ok ) {
176+ const data = await resp . json ( ) . catch ( ( ) => ( { } ) ) ;
177+ err . textContent = JSON . stringify ( data ) || `HTTP ${ resp . status } ` ;
178+ err . classList . remove ( "hidden" ) ;
179+ return ;
180+ }
181+ closeEdit ( ) ;
182+ window . loadTxns ( window . currentPage ) ;
183+ } catch ( e ) {
184+ err . textContent = e . message ;
185+ err . classList . remove ( "hidden" ) ;
186+ }
187+ } ) ;
67188
68189 window . loadTxns = async function ( page ) {
69190 const d = window . dashboard ;
@@ -76,11 +197,13 @@ <h1 class="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-10
76197 else if ( el . value ) params . set ( el . name , el . value ) ;
77198 }
78199 const data = await d . fetchJson ( "/api/transactions/?" + params . toString ( ) ) ;
200+ window . txnsById = { } ;
79201 const tbody = document . getElementById ( "txn-tbody" ) ;
80202 if ( ! data . results . length ) {
81- tbody . innerHTML = '<tr><td colspan="6 " class="empty">No transactions match.</td></tr>' ;
203+ tbody . innerHTML = '<tr><td colspan="7 " class="empty">No transactions match.</td></tr>' ;
82204 } else {
83205 tbody . innerHTML = data . results . map ( t => {
206+ window . txnsById [ t . id ] = t ;
84207 const amt = parseFloat ( t . amount ) ;
85208 const bal = t . balance !== null ? d . formatCurrency ( parseFloat ( t . balance ) , true ) : "—" ;
86209 const catTag = t . category
@@ -97,8 +220,12 @@ <h1 class="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-10
97220 <td>${ catTag } </td>
98221 <td class="num font-medium ${ amt >= 0 ? "text-emerald-600" : "text-rose-600" } ">${ d . formatCurrency ( amt , true ) } </td>
99222 <td class="num text-zinc-500">${ bal } </td>
223+ <td class="num"><button type="button" data-edit="${ t . id } " class="text-xs text-indigo-600 hover:text-indigo-500">edit</button></td>
100224 </tr>` ;
101225 } ) . join ( "" ) ;
226+ tbody . querySelectorAll ( "button[data-edit]" ) . forEach ( btn => {
227+ btn . addEventListener ( "click" , ( ) => openEdit ( window . txnsById [ btn . dataset . edit ] ) ) ;
228+ } ) ;
102229 }
103230 const total = data . count ;
104231 const shown = data . results . length ;
@@ -108,7 +235,7 @@ <h1 class="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-10
108235 document . getElementById ( "next-page" ) . disabled = ! data . next ;
109236 } ;
110237
111- window . addEventListener ( "load" , ( ) => {
238+ window . addEventListener ( "load" , async ( ) => {
112239 const scopeSelect = document . querySelector ( '#filters select[name="scope"]' ) ;
113240 document . querySelectorAll ( "#view-tabs button" ) . forEach ( b => {
114241 b . addEventListener ( "click" , ( ) => {
@@ -118,6 +245,7 @@ <h1 class="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-10
118245 window . loadTxns ( 1 ) ;
119246 } ) ;
120247 } ) ;
248+ await loadCategories ( ) ;
121249 window . loadTxns ( 1 ) ;
122250 } ) ;
123251</ script >
0 commit comments