package handlers import ( "database/sql" "encoding/json" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" ) // JournalHandler hanterar totaljournal och transaktionsdetaljer type JournalHandler struct { ledgerDB *sql.DB } func NewJournalHandler(ledgerDB *sql.DB) *JournalHandler { return &JournalHandler{ledgerDB: ledgerDB} } // JournalEntry representerar en verifikation type JournalEntry struct { ID string `json:"id"` EntryNumber int `json:"entry_number"` Description string `json:"description"` EntryDate time.Time `json:"entry_date"` CreatedAt time.Time `json:"created_at"` CreatedBy string `json:"created_by"` Period string `json:"period"` FiscalYear int `json:"fiscal_year"` Status string `json:"status"` Lines []JournalLine `json:"lines,omitempty"` } // JournalLine representerar en bokföringsrad type JournalLine struct { ID string `json:"id"` AccountID string `json:"account_id"` AccountCode string `json:"account_code"` AccountName string `json:"account_name"` Description string `json:"description"` Debit float64 `json:"debit"` Credit float64 `json:"credit"` } // AccountTransactions representerar alla transaktioner för ett konto type AccountTransactions struct { AccountCode string `json:"account_code"` AccountName string `json:"account_name"` AccountType string `json:"account_type"` OpeningBalance float64 `json:"opening_balance"` Transactions []JournalEntry `json:"transactions"` ClosingBalance float64 `json:"closing_balance"` TotalDebit float64 `json:"total_debit"` TotalCredit float64 `json:"total_credit"` } // GetJournalEntries returnerar alla verifikationer med pagination func (h *JournalHandler) GetJournalEntries(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 || limit > 100 { limit = 50 } offset := (page - 1) * limit accountFilter := r.URL.Query().Get("account") periodFilter := r.URL.Query().Get("period") query := ` SELECT id, entry_number, description, entry_date, created_at, created_by, period, fiscal_year, status FROM journal_entries WHERE 1=1 ` args := []interface{}{} argCount := 0 if accountFilter != "" { argCount++ query += ` AND EXISTS ( SELECT 1 FROM journal_lines jl JOIN accounts a ON jl.account_id = a.id WHERE jl.journal_entry_id = journal_entries.id AND a.code = $` + strconv.Itoa(argCount) + ` )` args = append(args, accountFilter) } if periodFilter != "" { argCount++ query += ` AND period = $` + strconv.Itoa(argCount) args = append(args, periodFilter) } query += ` ORDER BY entry_date DESC, entry_number DESC LIMIT $` + strconv.Itoa(argCount+1) + ` OFFSET $` + strconv.Itoa(argCount+2) args = append(args, limit, offset) rows, err := h.ledgerDB.Query(query, args...) if err != nil { http.Error(w, `{"error":"failed to fetch journal entries"}`, http.StatusInternalServerError) return } defer rows.Close() entries := []JournalEntry{} for rows.Next() { var e JournalEntry rows.Scan(&e.ID, &e.EntryNumber, &e.Description, &e.EntryDate, &e.CreatedAt, &e.CreatedBy, &e.Period, &e.FiscalYear, &e.Status) entries = append(entries, e) } // Hämta total count var total int countQuery := `SELECT COUNT(*) FROM journal_entries WHERE 1=1` if accountFilter != "" { countQuery += ` AND EXISTS ( SELECT 1 FROM journal_lines jl JOIN accounts a ON jl.account_id = a.id WHERE jl.journal_entry_id = journal_entries.id AND a.code = '` + accountFilter + `' )` } if periodFilter != "" { countQuery += ` AND period = '` + periodFilter + `'` } h.ledgerDB.QueryRow(countQuery).Scan(&total) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "entries": entries, "total": total, "page": page, "limit": limit, "pages": (total + limit - 1) / limit, }) } // GetJournalEntry returnerar en specifik verifikation med alla rader func (h *JournalHandler) GetJournalEntry(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var entry JournalEntry err := h.ledgerDB.QueryRow(` SELECT id, entry_number, description, entry_date, created_at, created_by, period, fiscal_year, status FROM journal_entries WHERE id = $1 `, id).Scan(&entry.ID, &entry.EntryNumber, &entry.Description, &entry.EntryDate, &entry.CreatedAt, &entry.CreatedBy, &entry.Period, &entry.FiscalYear, &entry.Status) if err != nil { if err == sql.ErrNoRows { http.Error(w, `{"error":"journal entry not found"}`, http.StatusNotFound) return } http.Error(w, `{"error":"failed to fetch journal entry"}`, http.StatusInternalServerError) return } // Hämta alla rader rows, err := h.ledgerDB.Query(` SELECT jl.id, jl.account_id, a.code, a.name, jl.description, jl.debit, jl.credit FROM journal_lines jl JOIN accounts a ON jl.account_id = a.id WHERE jl.journal_entry_id = $1 ORDER BY jl.debit DESC, jl.credit DESC `, id) if err != nil { http.Error(w, `{"error":"failed to fetch journal lines"}`, http.StatusInternalServerError) return } defer rows.Close() for rows.Next() { var line JournalLine rows.Scan(&line.ID, &line.AccountID, &line.AccountCode, &line.AccountName, &line.Description, &line.Debit, &line.Credit) entry.Lines = append(entry.Lines, line) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(entry) } // GetAccountTransactions returnerar alla transaktioner för ett specifikt konto func (h *JournalHandler) GetAccountTransactions(w http.ResponseWriter, r *http.Request) { accountCode := chi.URLParam(r, "code") // Hämta kontoinformation var accountID, accountName, accountType string err := h.ledgerDB.QueryRow(` SELECT id, name, account_type FROM accounts WHERE code = $1 `, accountCode).Scan(&accountID, &accountName, &accountType) if err != nil { if err == sql.ErrNoRows { http.Error(w, `{"error":"account not found"}`, http.StatusNotFound) return } http.Error(w, `{"error":"failed to fetch account"}`, http.StatusInternalServerError) return } // Beräkna balans var totalDebit, totalCredit float64 h.ledgerDB.QueryRow(` SELECT COALESCE(SUM(debit), 0), COALESCE(SUM(credit), 0) FROM journal_lines WHERE account_id = $1 `, accountID).Scan(&totalDebit, &totalCredit) balance := totalDebit - totalCredit if accountType != "Asset" && accountType != "Expense" { balance = totalCredit - totalDebit } // Hämta alla transaktioner för detta konto rows, err := h.ledgerDB.Query(` SELECT je.id, je.entry_number, je.description, je.entry_date, je.period, je.status, jl.description as line_description, jl.debit, jl.credit FROM journal_lines jl JOIN journal_entries je ON jl.journal_entry_id = je.id WHERE jl.account_id = $1 ORDER BY je.entry_date DESC, je.entry_number DESC LIMIT 100 `, accountID) if err != nil { http.Error(w, `{"error":"failed to fetch transactions"}`, http.StatusInternalServerError) return } defer rows.Close() type Transaction struct { JournalEntryID string `json:"journal_entry_id"` EntryNumber int `json:"entry_number"` Description string `json:"description"` EntryDate time.Time `json:"entry_date"` Period string `json:"period"` Status string `json:"status"` LineDescription string `json:"line_description"` Debit float64 `json:"debit"` Credit float64 `json:"credit"` } transactions := []Transaction{} for rows.Next() { var t Transaction rows.Scan(&t.JournalEntryID, &t.EntryNumber, &t.Description, &t.EntryDate, &t.Period, &t.Status, &t.LineDescription, &t.Debit, &t.Credit) transactions = append(transactions, t) } result := AccountTransactions{ AccountCode: accountCode, AccountName: accountName, AccountType: accountType, ClosingBalance: balance, TotalDebit: totalDebit, TotalCredit: totalCredit, } // Konvertera till JournalEntry-format för frontend for _, t := range transactions { entry := JournalEntry{ ID: t.JournalEntryID, EntryNumber: t.EntryNumber, Description: t.Description, EntryDate: t.EntryDate, Period: t.Period, Status: t.Status, Lines: []JournalLine{{ Description: t.LineDescription, Debit: t.Debit, Credit: t.Credit, }}, } result.Transactions = append(result.Transactions, entry) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // DrillDownRequest används för att söka i totaljournalen type DrillDownRequest struct { AccountCode string `json:"account_code,omitempty"` Period string `json:"period,omitempty"` DateFrom string `json:"date_from,omitempty"` DateTo string `json:"date_to,omitempty"` MinAmount float64 `json:"min_amount,omitempty"` MaxAmount float64 `json:"max_amount,omitempty"` } // PostDrillDown hanterar avancerade sökningar i totaljournalen func (h *JournalHandler) PostDrillDown(w http.ResponseWriter, r *http.Request) { var req DrillDownRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } query := ` SELECT DISTINCT je.id, je.entry_number, je.description, je.entry_date, je.created_at, je.created_by, je.period, je.fiscal_year, je.status FROM journal_entries je JOIN journal_lines jl ON je.id = jl.journal_entry_id JOIN accounts a ON jl.account_id = a.id WHERE 1=1 ` args := []interface{}{} argCount := 0 if req.AccountCode != "" { argCount++ query += ` AND a.code = $` + strconv.Itoa(argCount) args = append(args, req.AccountCode) } if req.Period != "" { argCount++ query += ` AND je.period = $` + strconv.Itoa(argCount) args = append(args, req.Period) } if req.DateFrom != "" { argCount++ query += ` AND je.entry_date >= $` + strconv.Itoa(argCount) args = append(args, req.DateFrom) } if req.DateTo != "" { argCount++ query += ` AND je.entry_date <= $` + strconv.Itoa(argCount) args = append(args, req.DateTo) } if req.MinAmount > 0 { argCount++ query += ` AND (jl.debit >= $` + strconv.Itoa(argCount) + ` OR jl.credit >= $` + strconv.Itoa(argCount) + `)` args = append(args, req.MinAmount) } query += ` ORDER BY je.entry_date DESC LIMIT 100` rows, err := h.ledgerDB.Query(query, args...) if err != nil { http.Error(w, `{"error":"failed to search journal"}`, http.StatusInternalServerError) return } defer rows.Close() entries := []JournalEntry{} for rows.Next() { var e JournalEntry rows.Scan(&e.ID, &e.EntryNumber, &e.Description, &e.EntryDate, &e.CreatedAt, &e.CreatedBy, &e.Period, &e.FiscalYear, &e.Status) entries = append(entries, e) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "entries": entries, "total": len(entries), "filters": req, }) }