Skip to content

Commit 00fccc9

Browse files
committed
add transaction in web dashboard
1 parent f328405 commit 00fccc9

6 files changed

Lines changed: 404 additions & 3 deletions

File tree

internal/model/transactions.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func (Transaction) TableName() string {
144144
return "transactions"
145145
}
146146

147-
// Get category enum values as a slice of strings
147+
// GetTransactionCategories returns all categories
148148
func GetTransactionCategories() []string {
149149
return []string{
150150
string(CategorySalary),
@@ -169,15 +169,15 @@ func GetTransactionCategories() []string {
169169
}
170170
}
171171

172-
// Get transaction type enum values as a slice of strings
172+
// GetTransactionTypes returns all transaction types
173173
func GetTransactionTypes() []string {
174174
return []string{
175175
string(TypeIncome),
176176
string(TypeExpense),
177177
}
178178
}
179179

180-
// Get currency type enum values as a slice of strings
180+
// GetCurrencyTypes returns all currency types
181181
func GetCurrencyTypes() []string {
182182
return []string{
183183
string(CurrencyEUR),
@@ -187,3 +187,34 @@ func GetCurrencyTypes() []string {
187187
string(CurrencyCHF),
188188
}
189189
}
190+
191+
// GetIncomeCategories returns only income categories
192+
func GetIncomeCategories() []string {
193+
return []string{
194+
string(CategorySalary),
195+
string(CategoryOtherIncomes),
196+
}
197+
}
198+
199+
// GetExpenseCategories returns only expense categories
200+
func GetExpenseCategories() []string {
201+
return []string{
202+
string(CategoryCar),
203+
string(CategoryClothes),
204+
string(CategoryGrocery),
205+
string(CategoryHouse),
206+
string(CategoryBills),
207+
string(CategoryEntertainment),
208+
string(CategorySport),
209+
string(CategoryEatingOut),
210+
string(CategoryTransport),
211+
string(CategoryLearning),
212+
string(CategoryToiletry),
213+
string(CategoryHealth),
214+
string(CategoryTech),
215+
string(CategoryGifts),
216+
string(CategoryTravel),
217+
string(CategoryPets),
218+
string(CategoryOtherExpenses),
219+
}
220+
}

internal/web/dashboard.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package web
22

33
import (
4+
"encoding/json"
45
"html/template"
56
"net/http"
67
"time"
@@ -168,3 +169,103 @@ func (s *Server) handleAPITransactions(w http.ResponseWriter, r *http.Request) {
168169

169170
s.sendJSONSuccess(w, response)
170171
}
172+
173+
// handleAPICategories returns available categories based on transaction type
174+
func (s *Server) handleAPICategories(w http.ResponseWriter, r *http.Request) {
175+
user := client.GetUserFromContext(r.Context())
176+
if user == nil {
177+
s.sendJSONError(w, "Unauthorized", http.StatusUnauthorized)
178+
return
179+
}
180+
181+
txType := r.URL.Query().Get("type")
182+
183+
var categories []string
184+
switch txType {
185+
case string(model.TypeIncome):
186+
categories = model.GetIncomeCategories()
187+
case string(model.TypeExpense):
188+
categories = model.GetExpenseCategories()
189+
default:
190+
s.sendJSONError(w, "Invalid transaction type", http.StatusBadRequest)
191+
return
192+
}
193+
194+
s.sendJSONSuccess(w, map[string]any{
195+
"categories": categories,
196+
})
197+
}
198+
199+
// handleAPICreateTransaction creates a new transaction
200+
func (s *Server) handleAPICreateTransaction(w http.ResponseWriter, r *http.Request) {
201+
if r.Method != http.MethodPost {
202+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
203+
return
204+
}
205+
206+
user := client.GetUserFromContext(r.Context())
207+
if user == nil {
208+
s.sendJSONError(w, "Unauthorized", http.StatusUnauthorized)
209+
return
210+
}
211+
212+
var req struct {
213+
Type string `json:"type"`
214+
Category string `json:"category"`
215+
Amount float64 `json:"amount"`
216+
Description string `json:"description"`
217+
Date string `json:"date"`
218+
}
219+
220+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
221+
s.sendJSONError(w, "Invalid request", http.StatusBadRequest)
222+
return
223+
}
224+
225+
// Validate type
226+
if req.Type != string(model.TypeIncome) && req.Type != string(model.TypeExpense) {
227+
s.sendJSONError(w, "Invalid transaction type", http.StatusBadRequest)
228+
return
229+
}
230+
231+
// Validate category
232+
if !model.IsValidTransactionCategory(req.Category) {
233+
s.sendJSONError(w, "Invalid category", http.StatusBadRequest)
234+
return
235+
}
236+
237+
// Validate amount
238+
if req.Amount <= 0 {
239+
s.sendJSONError(w, "Amount must be greater than 0", http.StatusBadRequest)
240+
return
241+
}
242+
243+
// Parse date
244+
date, err := time.Parse("2006-01-02", req.Date)
245+
if err != nil {
246+
s.sendJSONError(w, "Invalid date format", http.StatusBadRequest)
247+
return
248+
}
249+
250+
// Create transaction
251+
transaction := model.Transaction{
252+
TgID: user.TgID,
253+
Type: model.TransactionType(req.Type),
254+
Category: model.TransactionCategory(req.Category),
255+
Amount: req.Amount,
256+
Description: req.Description,
257+
Date: date,
258+
Currency: model.CurrencyEUR, // Default to EUR
259+
}
260+
261+
err = s.repositories.Transactions.Add(transaction)
262+
if err != nil {
263+
s.logger.Errorf("Failed to create transaction: %v", err)
264+
s.sendJSONError(w, "Failed to create transaction", http.StatusInternalServerError)
265+
return
266+
}
267+
268+
s.sendJSONSuccess(w, map[string]any{
269+
"message": "Transaction created successfully",
270+
})
271+
}

internal/web/web.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ func Router(s *Server) http.Handler {
2323
// Dashboard routes (protected)
2424
mux.HandleFunc(basePath+"/dashboard", s.requireAuth(s.handleDashboard))
2525
mux.HandleFunc(basePath+"/api/transactions", s.requireAuth(s.handleAPITransactions))
26+
mux.HandleFunc(basePath+"/api/transactions/create", s.requireAuth(s.handleAPICreateTransaction))
27+
mux.HandleFunc(basePath+"/api/categories", s.requireAuth(s.handleAPICategories))
2628
mux.HandleFunc(basePath+"/api/stats", s.requireAuth(s.handleAPIStats))
2729

2830
return s.loggingMiddleware(mux)

web/static/css/dashboard.css

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ body {
143143
font-weight: 600;
144144
margin-bottom: 1.5rem;
145145
color: #333;
146+
padding-bottom: 0.75rem;
147+
border-bottom: 2px solid #f0f0f0;
146148
}
147149

148150
.section-header {
@@ -307,6 +309,103 @@ body {
307309
margin-bottom: 1rem;
308310
}
309311

312+
/* Transaction Form */
313+
.transaction-form {
314+
background: white;
315+
padding: 1.5rem;
316+
border-radius: 8px;
317+
margin-bottom: 1rem;
318+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
319+
}
320+
321+
.form-row {
322+
display: grid;
323+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
324+
gap: 1rem;
325+
margin-bottom: 1.5rem;
326+
}
327+
328+
.form-group {
329+
display: flex;
330+
flex-direction: column;
331+
}
332+
333+
.form-group label {
334+
margin-bottom: 0.5rem;
335+
font-weight: 500;
336+
color: #555;
337+
font-size: 14px;
338+
}
339+
340+
.form-group input,
341+
.form-group select {
342+
padding: 0.75rem;
343+
border: 1px solid #ddd;
344+
border-radius: 4px;
345+
font-size: 16px;
346+
transition: border-color 0.2s, box-shadow 0.2s;
347+
}
348+
349+
.form-group input:focus,
350+
.form-group select:focus {
351+
outline: none;
352+
border-color: #007bff;
353+
box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
354+
}
355+
356+
.form-group select:disabled {
357+
background-color: #f5f5f5;
358+
cursor: not-allowed;
359+
}
360+
361+
.submit-btn {
362+
width: 100%;
363+
padding: 1rem;
364+
background: #28a745;
365+
color: white;
366+
border: none;
367+
border-radius: 4px;
368+
font-size: 16px;
369+
font-weight: 600;
370+
cursor: pointer;
371+
transition: background 0.2s, transform 0.1s;
372+
margin-top: 1.5rem;
373+
}
374+
375+
.submit-btn:hover {
376+
background: #218838;
377+
transform: translateY(-1px);
378+
}
379+
380+
.submit-btn:active {
381+
transform: translateY(0);
382+
}
383+
384+
.submit-btn:disabled {
385+
background: #6c757d;
386+
cursor: not-allowed;
387+
transform: none;
388+
}
389+
390+
.message {
391+
margin-top: 1rem;
392+
padding: 0.75rem;
393+
border-radius: 4px;
394+
text-align: center;
395+
}
396+
397+
.message.success {
398+
background: #d4edda;
399+
color: #155724;
400+
border: 1px solid #c3e6cb;
401+
}
402+
403+
.message.error {
404+
background: #f8d7da;
405+
color: #721c24;
406+
border: 1px solid #f5c6cb;
407+
}
408+
310409
@media (max-width: 768px) {
311410
.header-content {
312411
flex-direction: column;
@@ -319,4 +418,7 @@ body {
319418
flex-direction: column;
320419
gap: 1rem;
321420
}
421+
.form-row {
422+
grid-template-columns: 1fr;
423+
}
322424
}

0 commit comments

Comments
 (0)