diff --git a/Portifolio b/Portifolio index 3442d55..cff6b19 100755 Binary files a/Portifolio and b/Portifolio differ diff --git a/app.db b/app.db index 46399b2..44df43f 100644 Binary files a/app.db and b/app.db differ diff --git a/internal/database/company.go b/internal/database/company.go index 3c3ac60..35b1093 100644 --- a/internal/database/company.go +++ b/internal/database/company.go @@ -8,13 +8,14 @@ import ( _ "github.com/mattn/go-sqlite3" ) -func GetCompanyBySymbol(db *sql.DB, symbol string) (*model.Company, error) { +func getCompany(db *sql.DB, where string, arg any) (*model.Company, error) { var c model.Company - err := db.QueryRow( - `SELECT id, symbol, shares_outstanding, price, currency_id FROM companies WHERE symbol = ?`, - symbol, - ).Scan(&c.ID, &c.Symbol, &c.SharesOutstanding, &c.Price, &c.CurrencyID) - + err := db.QueryRow(` + SELECT c.id, c.symbol, c.shares_outstanding, c.price, cur.id, cur.code + FROM companies c + JOIN currencies cur ON cur.id = c.currency_id + WHERE `+where, arg, + ).Scan(&c.ID, &c.Symbol, &c.SharesOutstanding, &c.Price, &c.CurrencyID, &c.CurrencyCode) if err == sql.ErrNoRows { return nil, nil } @@ -24,20 +25,12 @@ func GetCompanyBySymbol(db *sql.DB, symbol string) (*model.Company, error) { return &c, nil } -func GetCompanyByID(db *sql.DB, id int) (*model.Company, error) { - var c model.Company - err := db.QueryRow( - `SELECT id, symbol, shares_outstanding, price, currency_id FROM companies WHERE id = ?`, - id, - ).Scan(&c.ID, &c.Symbol, &c.SharesOutstanding, &c.Price, &c.CurrencyID) +func GetCompanyBySymbol(db *sql.DB, symbol string) (*model.Company, error) { + return getCompany(db, "c.symbol = ?", symbol) +} - if err == sql.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("query company: %w", err) - } - return &c, nil +func GetCompanyByID(db *sql.DB, id int) (*model.Company, error) { + return getCompany(db, "c.id = ?", id) } func AddCompany(db *sql.DB, input model.CompanyInput) (int, error) { @@ -66,7 +59,9 @@ func AddCompany(db *sql.DB, input model.CompanyInput) (int, error) { func GetAllCompanies(db *sql.DB) ([]model.Company, error) { rows, err := db.Query(` - SELECT id, symbol, shares_outstanding, price, currency_id FROM companies + SELECT c.id, c.symbol, c.shares_outstanding, c.price, cur.id, cur.code + FROM companies c + JOIN currencies cur ON cur.id = c.currency_id `) if err != nil { return nil, err @@ -77,7 +72,7 @@ func GetAllCompanies(db *sql.DB) ([]model.Company, error) { for rows.Next() { var c model.Company if err := rows.Scan( - &c.ID, &c.Symbol, &c.SharesOutstanding, &c.Price, &c.CurrencyID, + &c.ID, &c.Symbol, &c.SharesOutstanding, &c.Price, &c.CurrencyID, &c.CurrencyCode, ); err != nil { return nil, err } diff --git a/internal/database/main.go b/internal/database/main.go index 33f6798..de52136 100644 --- a/internal/database/main.go +++ b/internal/database/main.go @@ -82,7 +82,7 @@ func InitDB(db *sql.DB) { ); -- parent table - CREATE TABLE closed_positions ( + CREATE TABLE IF NOT EXISTS closed_positions ( id INTEGER PRIMARY KEY AUTOINCREMENT, symbol TEXT NOT NULL, currency_code TEXT NOT NULL, @@ -94,7 +94,7 @@ func InitDB(db *sql.DB) { ); -- child table, one row per close lot - CREATE TABLE close_entries ( + CREATE TABLE IF NOT EXISTS close_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, closed_position_id INTEGER NOT NULL REFERENCES closed_positions(id), shares INTEGER NOT NULL, diff --git a/internal/handlers/currency.go b/internal/handlers/currency.go deleted file mode 100644 index 9a179ce..0000000 --- a/internal/handlers/currency.go +++ /dev/null @@ -1,44 +0,0 @@ -package handlers - -import ( - "Portifolio/internal/model" - "Portifolio/internal/service" - "database/sql" - "encoding/json" - "net/http" - - _ "github.com/mattn/go-sqlite3" -) - -func AddCurrencyHandler(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var input model.CurrencyInput - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - http.Error(w, "invalid json", http.StatusBadRequest) - return - } - - id, err := service.InsertCurrency(db, input) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(map[string]any{"status": "created", "id": id}) - } -} - -func GetCurrenciesHandler(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - currencies, err := service.GetAllCurrencies(db) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(currencies) - } -} diff --git a/internal/handlers/portfolio.go b/internal/handlers/portfolio.go deleted file mode 100644 index 8fb1e85..0000000 --- a/internal/handlers/portfolio.go +++ /dev/null @@ -1,108 +0,0 @@ -package handlers - -import ( - "Portifolio/internal/database" - "Portifolio/internal/model" - "Portifolio/internal/service" - "database/sql" - "encoding/json" - "fmt" - "net/http" - - _ "github.com/mattn/go-sqlite3" -) - -func AddTradeHandler(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req model.AddTradeRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "invalid json", http.StatusBadRequest) - return - } - - if err := req.Validate(); err != nil { - http.Error(w, fmt.Sprintf("failed to validate trade: %s", err), http.StatusBadRequest) - return - } - - currency, err := database.GetCurrencyByCode(db, req.CurrencyCode) - if err != nil { - http.Error(w, fmt.Sprintf("failed to find currency: %s", err), http.StatusInternalServerError) - return - } - - switch model.TradeType(req.Type) { - case model.DividendType: - dividend, err := req.ToDividend() - if err != nil { - http.Error(w, fmt.Sprintf("failed to build dividend: %s", err), http.StatusBadRequest) - return - } - dividend.CurrencyCode = currency.Code - - if err := database.InsertDividend(db, dividend); err != nil { - http.Error(w, fmt.Sprintf("failed to insert dividend: %s", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{"success": true}) - - case model.BuyType, model.SellType: - trade, err := req.ToTrade() - if err != nil { - http.Error(w, fmt.Sprintf("failed to build trade: %s", err), http.StatusBadRequest) - return - } - trade.CurrencyCode = currency.Code - - if err := database.InsertTrade(db, trade); err != nil { - http.Error(w, fmt.Sprintf("failed to insert trade: %s", err), http.StatusInternalServerError) - return - } - - update := true - if err := service.UpdatePositionByTradeList(db); err != nil { - update = false - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{"success": true, "position_update": update}) - - default: - http.Error(w, fmt.Sprintf("unknown trade type: %d", req.Type), http.StatusBadRequest) - } - } -} - -func GetTradeListHandler(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - tradeList, err := database.GetTrades(db) - if err != nil { - http.Error(w, fmt.Sprintf("failed to fetch trades: %s", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(tradeList); err != nil { - http.Error(w, fmt.Sprintf("failed to encode trades: %s", err), http.StatusInternalServerError) - return - } - } -} - -func GetPositionListHandler(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - posList, err := database.GetPositions(db) - if err != nil { - http.Error(w, fmt.Sprintf("failed to fetch postiton: %s", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(posList); err != nil { - http.Error(w, fmt.Sprintf("failed to encode positions: %s", err), http.StatusInternalServerError) - return - } - } -} diff --git a/internal/model/company.go b/internal/model/company.go index cf8515e..871dc1f 100644 --- a/internal/model/company.go +++ b/internal/model/company.go @@ -6,6 +6,7 @@ type Company struct { SharesOutstanding int Price float64 CurrencyID int + CurrencyCode string } type CompanyInput struct { diff --git a/internal/service/currency.go b/internal/service/currency.go index 23818fb..6aa08fb 100644 --- a/internal/service/currency.go +++ b/internal/service/currency.go @@ -3,12 +3,51 @@ package service import ( "Portifolio/internal/model" "database/sql" + "encoding/json" + "net/http" _ "github.com/mattn/go-sqlite3" ) -func InsertCurrency(db *sql.DB, input model.CurrencyInput) (int, error) { - res, err := db.Exec( +type Service struct { + db *sql.DB +} + +func (s *Service) AddCurrencyHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var input model.CurrencyInput + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + + id, err := s.InsertCurrency(input) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]any{"status": "created", "id": id}) + } +} + +func (s *Service) GetCurrenciesHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + currencies, err := s.GetAllCurrencies() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(currencies) + } +} + +func (s *Service) InsertCurrency(input model.CurrencyInput) (int, error) { + res, err := s.db.Exec( `INSERT INTO currencies (code, name) VALUES (?, ?)`, input.Code, input.Name, ) @@ -19,9 +58,9 @@ func InsertCurrency(db *sql.DB, input model.CurrencyInput) (int, error) { return int(id), err } -func GetCurrencyByCode(db *sql.DB, code string) (*model.Currency, error) { +func (s *Service) GetCurrencyByCode(code string) (*model.Currency, error) { c := &model.Currency{} - err := db.QueryRow( + err := s.db.QueryRow( `SELECT id, code, name FROM currencies WHERE code = ?`, code, ).Scan(&c.ID, &c.Code, &c.Name) if err == sql.ErrNoRows { @@ -30,8 +69,8 @@ func GetCurrencyByCode(db *sql.DB, code string) (*model.Currency, error) { return c, err } -func GetAllCurrencies(db *sql.DB) ([]model.Currency, error) { - rows, err := db.Query(`SELECT id, code, name FROM currencies ORDER BY code`) +func (s *Service) GetAllCurrencies() ([]model.Currency, error) { + rows, err := s.db.Query(`SELECT id, code, name FROM currencies ORDER BY code`) if err != nil { return nil, err } diff --git a/internal/handlers/main.go b/internal/service/main.go similarity index 76% rename from internal/handlers/main.go rename to internal/service/main.go index 8deaae8..38ff69b 100644 --- a/internal/handlers/main.go +++ b/internal/service/main.go @@ -1,4 +1,4 @@ -package handlers +package service import ( "Portifolio/internal/database" @@ -14,11 +14,15 @@ import ( var startTime = time.Now() -func HealthHandler(db *sql.DB) http.HandlerFunc { +func New(db *sql.DB) *Service { + return &Service{db: db} +} + +func (s *Service) HealthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - health := checkHealth(db) + health := s.CheckHealth() if health["status"] != "ok" { w.WriteHeader(http.StatusServiceUnavailable) @@ -28,21 +32,17 @@ func HealthHandler(db *sql.DB) http.HandlerFunc { } } -func checkHealth(db *sql.DB) map[string]any { - // database +func (s *Service) CheckHealth() map[string]any { dbStatus := "ok" dbLatency := "" t := time.Now() - if err := db.Ping(); err != nil { + if err := s.db.Ping(); err != nil { dbStatus = "error: " + err.Error() } else { dbLatency = time.Since(t).String() } - // db pool stats - stats := db.Stats() - - // overall status + stats := s.db.Stats() status := "ok" if dbStatus != "ok" { status = "degraded" @@ -75,7 +75,7 @@ func bToMb(b uint64) float64 { return float64(b) / 1024 / 1024 } -func AddCompanyHandler(db *sql.DB) http.HandlerFunc { +func (s *Service) AddCompanyHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var input model.CompanyInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { @@ -83,7 +83,7 @@ func AddCompanyHandler(db *sql.DB) http.HandlerFunc { return } - id, err := database.AddCompany(db, input) + id, err := database.AddCompany(s.db, input) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -95,9 +95,14 @@ func AddCompanyHandler(db *sql.DB) http.HandlerFunc { } } -func GetCompaniesHandler(db *sql.DB) http.HandlerFunc { +func (s *Service) GetAllCompanies() ([]model.Company, error) { + companies, err := database.GetAllCompanies(s.db) + return companies, err +} + +func (s *Service) GetCompaniesHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - companies, err := database.GetAllCompanies(db) + companies, err := database.GetAllCompanies(s.db) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/service/portfolio.go b/internal/service/portfolio.go index bd55c2f..318376c 100644 --- a/internal/service/portfolio.go +++ b/internal/service/portfolio.go @@ -3,15 +3,16 @@ package service import ( "Portifolio/internal/database" "Portifolio/internal/model" - "database/sql" + "encoding/json" "fmt" + "net/http" _ "github.com/mattn/go-sqlite3" ) -func UpdatePositionByTradeList(db *sql.DB) error { +func (s *Service) UpdatePositionByTradeList() error { - trades, err := database.GetTrades(db) + trades, err := database.GetTrades(s.db) if err != nil { fmt.Printf("Failed to get the trades from db: %s", err) } @@ -42,10 +43,115 @@ func UpdatePositionByTradeList(db *sql.DB) error { NewPositinos = append(NewPositinos, pos) } - err = database.UpdatePositions(db, NewPositinos) + err = database.UpdatePositions(s.db, NewPositinos) if err != nil { return fmt.Errorf("Failed to insert the new postions number into db: %s", err) } return nil } + +func (s *Service) AddTradeHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req model.AddTradeRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + + if err := req.Validate(); err != nil { + http.Error(w, fmt.Sprintf("failed to validate trade: %s", err), http.StatusBadRequest) + return + } + + currency, err := database.GetCurrencyByCode(s.db, req.CurrencyCode) + if err != nil { + http.Error(w, fmt.Sprintf("failed to find currency: %s", err), http.StatusInternalServerError) + return + } + + switch model.TradeType(req.Type) { + case model.DividendType: + dividend, err := req.ToDividend() + if err != nil { + http.Error(w, fmt.Sprintf("failed to build dividend: %s", err), http.StatusBadRequest) + return + } + dividend.CurrencyCode = currency.Code + + if err := database.InsertDividend(s.db, dividend); err != nil { + http.Error(w, fmt.Sprintf("failed to insert dividend: %s", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{"success": true}) + + case model.BuyType, model.SellType: + trade, err := req.ToTrade() + if err != nil { + http.Error(w, fmt.Sprintf("failed to build trade: %s", err), http.StatusBadRequest) + return + } + trade.CurrencyCode = currency.Code + + if err := database.InsertTrade(s.db, trade); err != nil { + http.Error(w, fmt.Sprintf("failed to insert trade: %s", err), http.StatusInternalServerError) + return + } + + update := true + if err := s.UpdatePositionByTradeList(); err != nil { + update = false + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{"success": true, "position_update": update}) + + default: + http.Error(w, fmt.Sprintf("unknown trade type: %d", req.Type), http.StatusBadRequest) + } + } +} + +func (s *Service) GetTrades() ([]model.Trade, error) { + TradeList, err := database.GetTrades(s.db) + return TradeList, err +} + +func (s *Service) GetTradeListHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tradeList, err := database.GetTrades(s.db) + if err != nil { + http.Error(w, fmt.Sprintf("failed to fetch trades: %s", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(tradeList); err != nil { + http.Error(w, fmt.Sprintf("failed to encode trades: %s", err), http.StatusInternalServerError) + return + } + } +} + +func (s *Service) GetPositions() ([]model.Position, error) { + posList, err := database.GetPositions(s.db) + return posList, err +} + +func (s *Service) GetPositionListHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + posList, err := database.GetPositions(s.db) + if err != nil { + http.Error(w, fmt.Sprintf("failed to fetch postiton: %s", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(posList); err != nil { + http.Error(w, fmt.Sprintf("failed to encode positions: %s", err), http.StatusInternalServerError) + return + } + } +} diff --git a/internal/handlers/revenue.go b/internal/service/revenue.go similarity index 69% rename from internal/handlers/revenue.go rename to internal/service/revenue.go index 48b1b50..23d5216 100644 --- a/internal/handlers/revenue.go +++ b/internal/service/revenue.go @@ -1,9 +1,8 @@ -package handlers +package service import ( "Portifolio/internal/database" "Portifolio/internal/model" - "database/sql" "encoding/json" "fmt" "net/http" @@ -11,7 +10,7 @@ import ( _ "github.com/mattn/go-sqlite3" ) -func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc { +func (s *Service) AddRevenueEntryHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var input struct { CompanyID int `json:"company_id"` @@ -48,7 +47,7 @@ func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc { Value: input.Value, } - err := database.InsertRevenue(db, rev) + err := database.InsertRevenue(s.db, rev) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -60,27 +59,7 @@ func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc { } } -/* -func GetRevenueReportHandler(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - companyID, _ := strconv.Atoi(r.URL.Query().Get("company_id")) - periodType := r.URL.Query().Get("period_type") - year, _ := strconv.Atoi(r.URL.Query().Get("year")) - idx, _ := strconv.Atoi(r.URL.Query().Get("index")) - - entries, err := database.GetRevenueByPeriod(db, companyID, ) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(entries) - } -} -*/ - -func GetCompanyRevenueCategories(db *sql.DB) http.HandlerFunc { +func (s *Service) GetCompanyRevenueCategories() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var input struct { CompanyID int `json:"company_id"` @@ -90,7 +69,7 @@ func GetCompanyRevenueCategories(db *sql.DB) http.HandlerFunc { return } - catlist, err := database.GetCategoriesByCompanyID(db, input.CompanyID) + catlist, err := database.GetCategoriesByCompanyID(s.db, input.CompanyID) if err != nil { http.Error(w, fmt.Sprintf("Could not find categories by that id:%s", err), http.StatusBadRequest) return diff --git a/internal/shell/company.go b/internal/shell/company.go deleted file mode 100644 index 8158b0f..0000000 --- a/internal/shell/company.go +++ /dev/null @@ -1,76 +0,0 @@ -package shell - -import ( - "Portifolio/internal/database" - "Portifolio/internal/model" - "bufio" - "database/sql" - "fmt" - "strconv" - "strings" - - _ "github.com/mattn/go-sqlite3" -) - -func AddCompany(scanner *bufio.Scanner, db *sql.DB) { - input := model.CompanyInput{} - - fmt.Print(" symbol: ") - scanner.Scan() - input.Symbol = strings.TrimSpace(scanner.Text()) - - fmt.Print(" Shares outstanding: ") - scanner.Scan() - shares, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) - if err != nil { - fmt.Println(" Invalid number for shares.") - return - } - input.SharesOutstanding = shares - - fmt.Print(" Price: ") - scanner.Scan() - price, err := strconv.ParseFloat(strings.TrimSpace(scanner.Text()), 64) - if err != nil { - fmt.Println(" Invalid number for price.") - return - } - input.Price = price - - fmt.Print(" Currency Code: ") - scanner.Scan() - input.CurrencyCode = strings.TrimSpace(scanner.Text()) - - if _, err := database.AddCompany(db, input); err != nil { - fmt.Println(" Error:", err) - return - } - fmt.Printf(" ✓ Company '%s' added.\n", input.Symbol) -} - -func ListCompanies(db *sql.DB) { - companies, err := database.GetAllCompanies(db) - if err != nil { - fmt.Println(" ✗ Error:", err) - return - } - - if len(companies) == 0 { - fmt.Println(" No companies found.") - return - } - - fmt.Printf("\n %-5s %-20s %-10s %-15s %s\n", "ID", "NAME", "CURRENCY", "PRICE", "SHARES") - fmt.Println(" " + strings.Repeat("-", 60)) - for _, c := range companies { - - currency, err := database.GetCurrencyByID(db, c.CurrencyID) - if err != nil { - fmt.Println("No currency by id.") - return - } - - fmt.Printf(" %-5d %-20s %-10s %-15.2f %d\n", - c.ID, c.Symbol, currency.Code, c.Price, c.SharesOutstanding) - } -} diff --git a/internal/shell/currency.go b/internal/shell/currency.go deleted file mode 100644 index 551014c..0000000 --- a/internal/shell/currency.go +++ /dev/null @@ -1,44 +0,0 @@ -package shell - -import ( - "Portifolio/internal/model" - "Portifolio/internal/service" - "bufio" - "database/sql" - "fmt" - "strings" - - _ "github.com/mattn/go-sqlite3" -) - -func AddCurrency(scanner *bufio.Scanner, db *sql.DB) { - input := model.CurrencyInput{} - - fmt.Print(" Code (e.g. DKK): ") - scanner.Scan() - input.Code = strings.ToUpper(strings.TrimSpace(scanner.Text())) - - fmt.Print(" Name (e.g. Danish Krone): ") - scanner.Scan() - input.Name = strings.TrimSpace(scanner.Text()) - - id, err := service.InsertCurrency(db, input) - if err != nil { - fmt.Println(" ✗ Error:", err) - return - } - fmt.Printf(" ✓ Currency '%s' (%s) added with ID %d\n", input.Name, input.Code, id) -} - -func ListCurrencies(db *sql.DB) { - currencies, err := service.GetAllCurrencies(db) - if err != nil { - fmt.Println(" ✗ Error:", err) - return - } - fmt.Printf(" %-5s %-6s %s\n", "ID", "CODE", "NAME") - fmt.Println(" " + strings.Repeat("-", 30)) - for _, c := range currencies { - fmt.Printf(" %-5d %-6s %s\n", c.ID, c.Code, c.Name) - } -} diff --git a/internal/shell/revenue.go b/internal/shell/revenue.go deleted file mode 100644 index c957bc6..0000000 --- a/internal/shell/revenue.go +++ /dev/null @@ -1,145 +0,0 @@ -package shell - -import ( - "Portifolio/internal/database" - "Portifolio/internal/model" - "bufio" - "database/sql" - "fmt" - "strconv" - "strings" -) - -func promptInt(scanner *bufio.Scanner, label string) (int, error) { - fmt.Print(label) - scanner.Scan() - v, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) - if err != nil { - return 0, fmt.Errorf("invalid %s", label) - } - return v, nil -} - -func promptFloat(scanner *bufio.Scanner, label string) (float64, error) { - fmt.Print(label) - scanner.Scan() - v, err := strconv.ParseFloat(strings.TrimSpace(scanner.Text()), 64) - if err != nil { - return 0, fmt.Errorf("invalid %s", label) - } - return v, nil -} - -func promptString(scanner *bufio.Scanner, label string) string { - fmt.Print(label) - scanner.Scan() - return strings.TrimSpace(scanner.Text()) -} - -func promptPeriod(scanner *bufio.Scanner) (model.Period, error) { - periodType := strings.ToUpper(promptString(scanner, " Period type (Q/H/Y): ")) - year, err := promptInt(scanner, " Year: ") - if err != nil { - return model.Period{}, err - } - - switch periodType { - case "Y": - return model.FullYearPeriod(year), nil - case "Q", "H": - label := map[string]string{"Q": " Index (1-4): ", "H": " Index (1-2): "}[periodType] - idx, err := promptInt(scanner, label) - if err != nil { - return model.Period{}, err - } - if periodType == "Q" { - return model.QuarterPeriod(year, idx), nil - } - return model.HalfYearPeriod(year, idx), nil - default: - return model.Period{}, fmt.Errorf("invalid period type: %s", periodType) - } -} - -func AddRevenue(scanner *bufio.Scanner, db *sql.DB) { - companyID, err := promptInt(scanner, " Company ID: ") - if err != nil { - fmt.Println(" ✗", err) - return - } - // checking if company exits - _, err = database.GetCompanyByID(db, companyID) - if err != nil { - fmt.Println("No company by that id:", err) - return - } - currencyID, err := promptInt(scanner, " Currency ID: ") - if err != nil { - fmt.Println(" ✗", err) - return - } - period, err := promptPeriod(scanner) - if err != nil { - fmt.Println(" ✗", err) - return - } - category := promptString(scanner, " Category name: ") - - var parentID *int - parentStr := promptString(scanner, " Parent category ID (leave blank for root): ") - if parentStr != "" { - pid, err := strconv.Atoi(parentStr) - if err != nil { - fmt.Println(" ✗ Invalid parent ID") - return - } - parentID = &pid - } - - value, err := promptFloat(scanner, " Value: ") - if err != nil { - fmt.Println(" ✗", err) - return - } - - rev := model.RevenueInsert{ - CompanyID: companyID, - CurrencyID: currencyID, - CategoryName: category, - ParentID: *parentID, - Period: period, - Value: value, - } - - err = database.InsertRevenue(db, rev) - if err != nil { - fmt.Println(" ✗ Error:", err) - return - } - fmt.Printf(" ✓ Revenue added: %s = %.2f (%s)\n", category, value, period.String()) -} - -func ListRevenue(scanner *bufio.Scanner, db *sql.DB) { - companyID, err := promptInt(scanner, " Company ID: ") - if err != nil { - fmt.Println(" ✗", err) - return - } - period, err := promptPeriod(scanner) - if err != nil { - fmt.Println(" ✗", err) - return - } - - entries, err := database.GetRevenueByPeriod(db, companyID, period.ID) - if err != nil { - fmt.Println(" ✗ Error:", err) - return - } - - fmt.Printf("\n %-20s %12s\n", "CATEGORY", "VALUE") - fmt.Println(" " + strings.Repeat("-", 34)) - for _, e := range entries { - fmt.Printf(" %-20s %12.2f\n", e.Category, e.Value) - } -} diff --git a/internal/website/main.go b/internal/website/main.go new file mode 100644 index 0000000..a56ff4d --- /dev/null +++ b/internal/website/main.go @@ -0,0 +1,239 @@ +package website + +import ( + "Portifolio/internal/model" + "Portifolio/internal/service" + _ "embed" + "fmt" + "net/http" + "strconv" + + _ "github.com/mattn/go-sqlite3" +) + +//go:embed static/index.html +var indexHTML []byte + +func Getsite() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(indexHTML) + } +} + +//go:embed static/styles.css +var styleCSS []byte + +func Getstylesheet() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css; charset=utf-8") + w.Write(styleCSS) + } +} + +func HealthFragment(svc *service.Service) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h := svc.CheckHealth() + + status := h["status"].(string) + uptime := h["uptime"].(string) + + dotClass := "ok" + statusText := "backend ok" + if status != "ok" { + dotClass = "error" + statusText = "backend " + status + w.WriteHeader(http.StatusServiceUnavailable) // ← triggers htmx:responseError + } + + dbInfo := h["database"].(map[string]any) + dbStatus := dbInfo["status"].(string) + dbLatency := dbInfo["latency"].(string) + dbText := "db " + dbStatus + if dbLatency != "" { + dbText += " · " + dbLatency + } + + fmt.Fprintf(w, ` + + %s + · + up %s + · + %s + + + `, dotClass, statusText, uptime, dbText) + } +} + +// GET /positions/fragment — returns rows for positions tbody +func PositionsFragment(svc *service.Service) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + positions, err := svc.GetPositions() + if err != nil { + fmt.Fprintf(w, `load failed: %s`, err) + return + } + if len(positions) == 0 { + fmt.Fprint(w, `no positions`) + return + } + + var total float64 + for _, p := range positions { + total += p.CostBasis + } + + for _, p := range positions { + weight := 0.0 // ← renamed from w to avoid shadowing http.ResponseWriter + if total > 0 { + weight = p.CostBasis / total * 100 + } + fmt.Fprintf(w, ` + %s + %s + %s + %s +
%.1f%% +
+
+
+
+ `, p.Symbol, p.CurrencyCode, fmtInt(p.Shares), fmtNum(p.CostBasis), weight, weight) + } + } +} + +// GET /positions/fragment — returns rows for positions tbody +func CompanyFragment(svc *service.Service) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + companies, err := svc.GetAllCompanies() + if err != nil { + fmt.Fprintf(w, `load failed: %s`, err) + return + } + if len(companies) == 0 { + fmt.Fprint(w, `no positions`) + return + } + + for _, c := range companies { + fmt.Fprintf(w, ` + %s + %s + %.2f + %d + %.2f + `, c.Symbol, c.CurrencyCode, c.Price, c.SharesOutstanding, c.Price*float64(c.SharesOutstanding)) + } + } +} + +// GET /positions/fragment — returns rows for positions tbody +func SummaryFragment(svc *service.Service) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + positions, err := svc.GetPositions() + if err != nil { + fmt.Fprintf(w, `load failed: %s`, err) + return + } + if len(positions) == 0 { + fmt.Fprint(w, `no positions`) + return + } + + var total float64 + for _, p := range positions { + total += p.CostBasis + } + + for _, p := range positions { + weight := 0.0 // ← renamed from w to avoid shadowing http.ResponseWriter + if total > 0 { + weight = p.CostBasis / total * 100 + } + fmt.Fprintf(w, ` + %s + %s + %s + %s +
%.1f%% +
+
+
+
+ `, p.Symbol, p.CurrencyCode, fmtInt(p.Shares), fmtNum(p.CostBasis), weight, weight) + } + } +} + +func fmtNum(v float64) string { + if v == 0 { + return "—" + } + return strconv.FormatFloat(v, 'f', 2, 64) +} + +func fmtInt(v int) string { + return strconv.Itoa(v) +} + +var productLabels = map[model.TradeProduct]string{ + model.StockTrade: "Stock", + model.OptionCallTrade: "Call Option", + model.OptionPutTrade: "Put Option", + model.CurrencyTrade: "Currency", + model.BondTrade: "Bond", +} + +func TradeFragment(svc *service.Service) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + trades, err := svc.GetTrades() + if err != nil { + fmt.Fprintf(w, `load failed: %s`, err) + return + } + if len(trades) == 0 { + fmt.Fprint(w, `no trades`) + return + } + + for _, t := range trades { + dir, cls := "SELL", "dir-sell" + if t.Type == model.BuyType { + dir, cls = "BUY", "dir-buy" + } + label := productLabels[t.Product] + fmt.Fprintf(w, ` + %s + %s + %s + %s + %s + %s + %s + `, t.Symbol, label, t.CurrencyCode, + t.Date.Format("2006-01-02"), cls, dir, + fmtNum(float64(t.Shares)), fmtNum(t.Price)) + } + } +} +func TradeCount(svc *service.Service) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + trades, err := svc.GetTrades() + if err != nil { + fmt.Fprint(w, "—") + return + } + count := len(trades) + if count == 1 { + fmt.Fprint(w, "1 trade") + } else { + fmt.Fprintf(w, "%d trades", count) + } + } +} diff --git a/internal/website/static/index.html b/internal/website/static/index.html new file mode 100644 index 0000000..e591dc9 --- /dev/null +++ b/internal/website/static/index.html @@ -0,0 +1,137 @@ + + + + + + Portfolio — Samantha Friis + + + + + +
+ +
+ ← back +

// portfolio

+

Investment
Portfolio

+

Equity positions  ·  Personal research

+
+ + +
+ + connecting to backend… + + +
+ + +
+

Positions

+

Total Shares

+

Total Trades

+

Currencies

+
+ + +

// positions

+
+ + + + + + + + + + +
SymbolCurrencySharesCost BasisWeight
loading…
+
+ + +

// companies — tracked universe

+
+ + + + + + + + + + +
SymbolCurrencyPriceShares OutstandingMarket Cap
loading…
+
+ + +
+ + +
+
+ + +
+ +
+ +

+ +
+ + + + + + + + + + +
SymbolAssetCurrencyDateDirQtyPrice
expand to load trades
+
+
+
+
+ + + +
+ + \ No newline at end of file diff --git a/internal/website/static/styles.css b/internal/website/static/styles.css new file mode 100644 index 0000000..7e38c14 --- /dev/null +++ b/internal/website/static/styles.css @@ -0,0 +1,138 @@ + :root { + --bg: #f4f4f4; + --surface: #efefef; + --surface2:#e0e0e0; + --border: #8c8c8c; + --border2: #acacac; + --text: #000000; + --muted: #5a5a68; + --muted2: #3d3d4a; + --accent: #c8a96e; + --up: #5aad85; + --down: #c96b6b; + --mono: 'Space Mono', monospace; + --serif: 'Fraunces', Georgia, serif; + } + + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + html { font-size: 15px; } + body { + background: var(--bg); + color: var(--text); + font-family: var(--mono); + font-size: 0.8rem; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + } + + .container { max-width: 1080px; margin: 0 auto; padding: 3rem 2rem 5rem; } + + /* ── Header ── */ + header { margin-bottom: 3rem; border-bottom: 1px solid var(--border); padding-bottom: 2rem; } + .back { display: inline-block; color: var(--muted); text-decoration: none; font-size: 0.72rem; letter-spacing: 0.05em; margin-bottom: 1.8rem; transition: color 0.15s; } + .back:hover { color: var(--accent); } + .tag { font-size: 0.68rem; color: var(--accent); letter-spacing: 0.12em; margin-bottom: 0.6rem; } + h1 { font-family: var(--serif); font-size: clamp(2.4rem, 5vw, 3.6rem); font-weight: 300; line-height: 1.1; color: var(--text); margin-bottom: 0.75rem; } + h1 span { font-style: italic; color: var(--accent); } + .header-sub { font-size: 0.7rem; color: var(--muted); letter-spacing: 0.08em; } + + /* ── Status bar ── */ + .status-bar { + display: flex; align-items: center; gap: 0.6rem; + font-size: 0.65rem; color: var(--muted); + margin-bottom: 2rem; padding: 0.6rem 1rem; + background: var(--surface); border: 1px solid var(--border); border-radius: 5px; + } + .status-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--muted2); } + .status-dot.ok { background: var(--up); } + .status-dot.loading { background: var(--accent); animation: blink 1s ease-in-out infinite; } + .status-dot.error { background: var(--down); } + @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.2} } + .status-sep { color: var(--muted2); margin: 0 0.25rem; } + .status-spacer { flex: 1; } + .refresh-btn { + background: none; border: 1px solid var(--border2); border-radius: 3px; + color: var(--muted); font-family: var(--mono); font-size: 0.62rem; + padding: 2px 9px; cursor: pointer; letter-spacing: 0.05em; transition: all 0.15s; + } + .refresh-btn:hover { border-color: var(--accent); color: var(--accent); } + + /* ── Summary cards ── */ + .cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; margin-bottom: 2.5rem; } + .card { background: var(--surface); padding: 1.1rem 1.25rem; } + .card-label { font-size: 0.62rem; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.45rem; } + .card-value { font-family: var(--serif); font-size: 1.65rem; font-weight: 300; color: var(--text); } + .card-value.dim { color: var(--muted2); font-size: 1rem; } + + /* ── Section label ── */ + .section-label { font-size: 0.62rem; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.75rem; } + .section-label span { color: var(--accent); } + + /* ── Tables ── */ + .table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 2.5rem; } + table { width: 100%; border-collapse: collapse; font-size: 0.75rem; } + thead tr { border-bottom: 1px solid var(--border2); background: var(--surface); } + th { color: var(--muted); font-weight: 400; font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.75rem 1rem; text-align: left; white-space: nowrap; } + td { padding: 0.7rem 1rem; border-bottom: 1px solid var(--border); color: var(--text); white-space: nowrap; } + tbody tr:last-child td { border-bottom: none; } + tbody tr:hover td { background: rgba(200,169,110,0.03); } + + .ticker { font-weight: 700; font-size: 0.72rem; letter-spacing: 0.04em; color: var(--text); margin-right: 6px; } + .currency-badge { display: inline-block; font-size: 0.58rem; padding: 1px 5px; background: var(--surface2); border: 1px solid var(--border2); border-radius: 3px; color: var(--muted); letter-spacing: 0.06em; vertical-align: middle; } + + .weight-cell { min-width: 100px; } + .weight-bar-track { height: 2px; background: var(--border2); border-radius: 1px; margin-top: 5px; } + .weight-bar-fill { height: 2px; background: var(--accent); border-radius: 1px; } + + .state-row td { color: var(--muted2); font-size: 0.7rem; text-align: center; padding: 2rem; border-bottom: none; } + + /* ── Trades accordion ── */ + .trades-section { margin-top: 0; } + .trades-toggle { + display: flex; align-items: center; justify-content: space-between; + cursor: pointer; background: none; border: none; + border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); + width: 100%; padding: 0.85rem 0; color: inherit; + font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em; + text-align: left; user-select: none; text-transform: uppercase; + } + .trades-toggle:hover .toggle-label { color: var(--accent); } + .toggle-label { color: var(--muted); transition: color 0.15s; } + .toggle-label span { color: var(--accent); } + .toggle-icon { color: var(--muted2); font-size: 0.9rem; transition: transform 0.25s ease; } + .trades-toggle[aria-expanded="true"] .toggle-icon { transform: rotate(180deg); } + .trades-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s ease; } + .trades-body.open { grid-template-rows: 1fr; } + .trades-inner { overflow: hidden; } + + .trades-filter { display: flex; gap: 0.4rem; flex-wrap: wrap; padding: 1rem 0 0.75rem; } + .filter-btn { + background: none; border: 1px solid var(--border2); border-radius: 3px; + color: var(--muted); font-family: var(--mono); font-size: 0.62rem; + letter-spacing: 0.08em; padding: 3px 10px; cursor: pointer; + transition: all 0.15s; text-transform: uppercase; + } + .filter-btn:hover, .filter-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(200,169,110,0.06); } + + .trades-count { font-size: 0.62rem; color: var(--muted2); letter-spacing: 0.06em; padding-bottom: 0.5rem; } + + .trades-table-wrap { overflow-x: auto; padding-bottom: 1.5rem; border: 1px solid var(--border); border-radius: 6px; } + .trades-table { width: 100%; border-collapse: collapse; font-size: 0.73rem; } + .trades-table thead tr { border-bottom: 1px solid var(--border2); background: var(--surface); } + .trades-table th { color: var(--muted); font-weight: 400; letter-spacing: 0.1em; font-size: 0.6rem; text-transform: uppercase; padding: 0.6rem 0.9rem; text-align: left; white-space: nowrap; } + .trades-table td { padding: 0.6rem 0.9rem; border-bottom: 1px solid var(--border); white-space: nowrap; color: var(--text); } + .trades-table tbody tr:last-child td { border-bottom: none; } + .trades-table tbody tr:hover td { background: rgba(200,169,110,0.03); } + .trades-table tr.hidden-row { display: none; } + + .dir-buy { color: var(--up); } + .dir-sell { color: var(--down); } + + .trade-code { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 0.6rem; background: var(--surface2); border: 1px solid var(--border2); color: var(--muted); letter-spacing: 0.05em; } + .trade-ticker { font-weight: 700; color: var(--text); letter-spacing: 0.03em; } + + /* ── Footer ── */ + footer { margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; } + footer a { color: var(--muted); font-size: 0.68rem; text-decoration: none; letter-spacing: 0.04em; transition: color 0.15s; } + footer a:hover { color: var(--accent); } + .footer-copy { font-size: 0.65rem; color: var(--muted2); letter-spacing: 0.06em; } diff --git a/main.go b/main.go index 18dbc2d..38c07ed 100644 --- a/main.go +++ b/main.go @@ -2,15 +2,12 @@ package main import ( "Portifolio/internal/database" - "Portifolio/internal/handlers" - "Portifolio/internal/shell" - "bufio" + "Portifolio/internal/service" + "Portifolio/internal/website" "database/sql" "fmt" "log" "net/http" - "os" - "strings" _ "github.com/mattn/go-sqlite3" ) @@ -28,6 +25,42 @@ func corsMiddleware(next http.Handler) http.Handler { }) } +func RegisterRoutes(mux *http.ServeMux, db *sql.DB) { + svc := service.New(db) // ← create it here, once + + // ── Website (HTMX fragments) ────────────────────────────── + mux.HandleFunc("/", website.Getsite()) + mux.HandleFunc("/style", website.Getstylesheet()) + mux.HandleFunc("/health/fragment", website.HealthFragment(svc)) + mux.HandleFunc("/positions/fragment", website.PositionsFragment(svc)) + mux.HandleFunc("/company/fragment", website.CompanyFragment(svc)) + mux.HandleFunc("/summary/fragment", website.SummaryFragment(svc)) + mux.HandleFunc("/trade/fragment", website.TradeFragment(svc)) + mux.HandleFunc("/trade/count", website.TradeCount(svc)) + + // ── API (JSON) ──────────────────────────────────────────── + mux.HandleFunc("/health", svc.HealthHandler()) + // Trades + mux.HandleFunc("POST /trade/add", svc.AddTradeHandler()) + mux.HandleFunc("GET /trade/list", svc.GetTradeListHandler()) + //mux.HandleFunc("GET /trade/search", svc.SearchTradeHandler(svc)) + // Positions + mux.HandleFunc("GET /positions/list", svc.GetPositionListHandler()) + //mux.HandleFunc("GET /positions/closed/list", svc.GetClosedPositionListHandler(svc)) + //mux.HandleFunc("GET /positions/closed/search", svc.SearchClosedPositionsHandler(svc)) + // Company + mux.HandleFunc("POST /company/add", svc.AddCompanyHandler()) + mux.HandleFunc("GET /company/list", svc.GetCompaniesHandler()) + mux.HandleFunc("GET /company/revenue/categories", svc.GetCompanyRevenueCategories()) + //mux.HandleFunc("POST /company/S-O/add", svc.AddSharesOutstandingHandler(svc)) + //mux.HandleFunc("GET /company/S-O/list", svc.GetSharesOutstandingHandler(svc)) + // Currency + mux.HandleFunc("GET /currency/list", svc.GetCurrenciesHandler()) + mux.HandleFunc("POST /currency/add", svc.AddCurrencyHandler()) + // Revenue + mux.HandleFunc("POST /add/revenue/entry", svc.AddRevenueEntryHandler()) + mux.HandleFunc("POST /api/v1/revenue/add", svc.AddRevenueEntryHandler()) +} func main() { var err error db, err = sql.Open("sqlite3", "./app.db?_foreign_keys=on") @@ -44,97 +77,8 @@ func main() { fmt.Println("Connected to SQLite database") mux := http.NewServeMux() - - mux.HandleFunc("/health", handlers.HealthHandler(db)) - - //Trades - mux.HandleFunc("POST /trade/add", handlers.AddTradeHandler(db)) - mux.HandleFunc("GET /trade/list", handlers.GetTradeListHandler(db)) - mux.HandleFunc("GET /trade/search", handlers.GetTradeListHandler(db)) // new - //Positions - mux.HandleFunc("GET /positions/list", handlers.GetPositionListHandler(db)) - mux.HandleFunc("GET /positions/closed/list", handlers.GetPositionListHandler(db)) // new - mux.HandleFunc("GET /positions/closed/search", handlers.GetTradeListHandler(db)) // new - // Company - mux.HandleFunc("POST /company/add", handlers.AddCompanyHandler(db)) - mux.HandleFunc("GET /company/list", handlers.GetCompaniesHandler(db)) - mux.HandleFunc("GET /company/revenue/categories", handlers.GetCompanyRevenueCategories(db)) - mux.HandleFunc("POST /company/S-O/add", handlers.GetCompaniesHandler(db)) // new - mux.HandleFunc("GET /company/S-O/list", handlers.GetCompaniesHandler(db)) // new - - // Currency - mux.HandleFunc("GET /currency/list", handlers.GetCurrenciesHandler(db)) - mux.HandleFunc("POST /currency/add", handlers.AddCurrencyHandler(db)) - - // Revenue - mux.HandleFunc("POST /add/revenue/entry", handlers.AddRevenueEntryHandler(db)) - mux.HandleFunc("POST /api/v1/revenue/add", handlers.AddRevenueEntryHandler(db)) - - //http.HandleFunc("GET /revenue/report", handlers.GetRevenueReportHandler(db)) + RegisterRoutes(mux, db) fmt.Println("Server running on :8080") - go func() { - log.Fatal(http.ListenAndServe(":8080", corsMiddleware(mux))) - }() - - runShell(db) -} - -func runShell(db *sql.DB) { - scanner := bufio.NewScanner(os.Stdin) - fmt.Println("\nShell ready. Type 'help' for commands.") - - for { - fmt.Print("> ") - if !scanner.Scan() { - break - } - - parts := strings.Fields(scanner.Text()) - if len(parts) == 0 { - continue - } - - switch parts[0] { - // Company - case "add-company": - shell.AddCompany(scanner, db) - case "list-companies": - shell.ListCompanies(db) - - // Currency - case "add-currency": - shell.AddCurrency(scanner, db) - case "list-currency": - shell.ListCurrencies(db) - - // Revenue - case "add-revenue": - shell.AddRevenue(scanner, db) - case "list-revenue": - shell.ListRevenue(scanner, db) - - case "help": - fmt.Println("\nCommands:") - fmt.Println(" --- Company ---") - fmt.Println(" add-company add a new company") - fmt.Println(" list-companies list all companies") - fmt.Println(" --- Currency ---") - fmt.Println(" add-currency add a new currency") - fmt.Println(" list-currency list all currencies") - fmt.Println(" --- Revenue ---") - fmt.Println(" add-revenue add a revenue entry interactively") - fmt.Println(" list-revenue list revenue for a company/period") - fmt.Println(" sum-revenue sum revenue across periods") - fmt.Println(" --- Other ---") - fmt.Println(" exit quit") - - case "exit": - fmt.Println("Bye!") - os.Exit(0) - - default: - fmt.Printf("Unknown command: %s. Type 'help' for commands.\n", parts[0]) - } - } + http.ListenAndServe(":8080", corsMiddleware(mux)) }