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
// portfolio
+Equity positions · Personal research
+Positions
…
Total Shares
…
Total Trades
…
Currencies
…
// positions
+| Symbol | Currency | Shares | +Cost Basis | Weight | +
|---|---|---|---|---|
| loading… | ||||
// companies — tracked universe
+| Symbol | Currency | Price | +Shares Outstanding | Market Cap | +
|---|---|---|---|---|
| loading… | ||||
—
+ +| Symbol | Asset | Currency | +Date | Dir | Qty | Price | +
|---|---|---|---|---|---|---|
| expand to load trades | ||||||