@@ -28,13 +28,40 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
2828 <!-- Category -->
2929 < div >
3030 < label for ="category_id " class ="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 "> Category *</ label >
31- < select id ="category_id " name ="category_id " required
32- class ="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 transition-colors duration-150 ">
33- < option value =""> Select category</ option >
34- {{range .Categories}}
35- < option value ="{{.ID}} " {{if $.Subscription}}{{if eq $.Subscription.CategoryID .ID}}selected{{end}}{{end}} > {{.Name}}</ option >
36- {{end}}
37- </ select >
31+ < div class ="flex gap-2 ">
32+ < div class ="flex-1 " id ="category-select-container ">
33+ < select id ="category_id " name ="category_id " required
34+ class ="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 transition-colors duration-150 ">
35+ < option value =""> Select category</ option >
36+ {{range .Categories}}
37+ < option value ="{{.ID}} " {{if $.Subscription}}{{if eq $.Subscription.CategoryID .ID}}selected{{end}}{{end}} > {{.Name}}</ option >
38+ {{end}}
39+ </ select >
40+ </ div >
41+ < button type ="button " id ="add-category-btn " onclick ="showNewCategoryInput() "
42+ class ="px-3 py-2 text-sm font-medium text-primary bg-primary/10 border border-primary/30 rounded-lg hover:bg-primary/20 transition-colors duration-150 "
43+ title ="Add new category ">
44+ < svg class ="w-5 h-5 " fill ="none " stroke ="currentColor " viewBox ="0 0 24 24 ">
45+ < path stroke-linecap ="round " stroke-linejoin ="round " stroke-width ="2 " d ="M12 4v16m8-8H4 "> </ path >
46+ </ svg >
47+ </ button >
48+ </ div >
49+ <!-- Inline new category input (hidden by default) -->
50+ < div id ="new-category-container " class ="hidden mt-2 ">
51+ < div class ="flex gap-2 ">
52+ < input type ="text " id ="new-category-name " placeholder ="New category name "
53+ class ="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 transition-colors duration-150 ">
54+ < button type ="button " onclick ="createNewCategory() "
55+ class ="px-3 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors duration-150 ">
56+ Add
57+ </ button >
58+ < button type ="button " onclick ="hideNewCategoryInput() "
59+ class ="px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-150 ">
60+ Cancel
61+ </ button >
62+ </ div >
63+ < div id ="new-category-error " class ="text-sm text-danger mt-1 hidden "> </ div >
64+ </ div >
3865 </ div >
3966
4067 <!-- Cost -->
@@ -61,6 +88,8 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
6188 < option value ="SEK " {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "SEK"}}selected{{end}}{{end}}> kr SEK</ option >
6289 < option value ="PLN " {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "PLN"}}selected{{end}}{{end}}> zł PLN</ option >
6390 < option value ="INR " {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "INR"}}selected{{end}}{{end}}> ₹ INR</ option >
91+ < option value ="CHF " {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "CHF"}}selected{{end}}{{end}}> Fr. CHF</ option >
92+ < option value ="BRL " {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "BRL"}}selected{{end}}{{end}}> R$ BRL</ option >
6493 </ select >
6594 </ div >
6695
@@ -195,6 +224,88 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
195224</ div >
196225
197226< script >
227+ // Inline category creation functions
228+ function showNewCategoryInput ( ) {
229+ document . getElementById ( 'new-category-container' ) . classList . remove ( 'hidden' ) ;
230+ document . getElementById ( 'add-category-btn' ) . classList . add ( 'hidden' ) ;
231+ document . getElementById ( 'new-category-name' ) . focus ( ) ;
232+ }
233+
234+ function hideNewCategoryInput ( ) {
235+ document . getElementById ( 'new-category-container' ) . classList . add ( 'hidden' ) ;
236+ document . getElementById ( 'add-category-btn' ) . classList . remove ( 'hidden' ) ;
237+ document . getElementById ( 'new-category-name' ) . value = '' ;
238+ document . getElementById ( 'new-category-error' ) . classList . add ( 'hidden' ) ;
239+ }
240+
241+ let isCreatingCategory = false ;
242+
243+ async function createNewCategory ( ) {
244+ // Prevent double-submission
245+ if ( isCreatingCategory ) return ;
246+
247+ const nameInput = document . getElementById ( 'new-category-name' ) ;
248+ const errorDiv = document . getElementById ( 'new-category-error' ) ;
249+ const addBtn = document . querySelector ( '#new-category-container button' ) ;
250+ const name = nameInput . value . trim ( ) ;
251+
252+ if ( ! name ) {
253+ errorDiv . textContent = 'Please enter a category name' ;
254+ errorDiv . classList . remove ( 'hidden' ) ;
255+ return ;
256+ }
257+
258+ isCreatingCategory = true ;
259+ if ( addBtn ) addBtn . disabled = true ;
260+
261+ try {
262+ const response = await fetch ( '/api/categories' , {
263+ method : 'POST' ,
264+ headers : {
265+ 'Content-Type' : 'application/json' ,
266+ } ,
267+ body : JSON . stringify ( { name : name } ) ,
268+ } ) ;
269+
270+ if ( ! response . ok ) {
271+ const data = await response . json ( ) ;
272+ throw new Error ( data . error || 'Failed to create category' ) ;
273+ }
274+
275+ const newCategory = await response . json ( ) ;
276+
277+ // Validate response before adding
278+ if ( ! newCategory . id || ! newCategory . name ) {
279+ throw new Error ( 'Invalid response from server' ) ;
280+ }
281+
282+ // Add new option to dropdown and select it
283+ const select = document . getElementById ( 'category_id' ) ;
284+ const option = document . createElement ( 'option' ) ;
285+ option . value = newCategory . id ;
286+ option . textContent = newCategory . name ;
287+ option . selected = true ;
288+ select . appendChild ( option ) ;
289+
290+ // Hide the input and reset
291+ hideNewCategoryInput ( ) ;
292+ } catch ( error ) {
293+ errorDiv . textContent = error . message ;
294+ errorDiv . classList . remove ( 'hidden' ) ;
295+ } finally {
296+ isCreatingCategory = false ;
297+ if ( addBtn ) addBtn . disabled = false ;
298+ }
299+ }
300+
301+ // Allow Enter key to submit new category
302+ document . addEventListener ( 'keydown' , function ( e ) {
303+ if ( e . key === 'Enter' && document . activeElement . id === 'new-category-name' ) {
304+ e . preventDefault ( ) ;
305+ createNewCategory ( ) ;
306+ }
307+ } ) ;
308+
198309function calculateRenewalDate ( ) {
199310 const scheduleSelect = document . getElementById ( 'schedule' ) ;
200311 const statusSelect = document . getElementById ( 'status' ) ;
@@ -255,15 +366,11 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
255366// Add event listeners when modal content loads
256367function initRenewalCalculator ( ) {
257368 const scheduleSelect = document . getElementById ( 'schedule' ) ;
258- const statusSelect = document . getElementById ( 'status' ) ;
259369
260370 if ( scheduleSelect ) {
261371 scheduleSelect . addEventListener ( 'change' , calculateRenewalDate ) ;
262372 }
263-
264- if ( statusSelect ) {
265- statusSelect . addEventListener ( 'change' , calculateRenewalDate ) ;
266- }
373+ // Note: Status changes should NOT trigger renewal date recalculation (fixes #68)
267374}
268375
269376// Initialize immediately since this script loads with the form
0 commit comments