diff --git a/Portifolio b/Portifolio index 88c07f3..27919fc 100755 Binary files a/Portifolio and b/Portifolio differ diff --git a/app.db b/app.db index d5dd44d..23f7672 100644 Binary files a/app.db and b/app.db differ diff --git a/internal/database/main.go b/internal/database/main.go index 4942c8a..b261c64 100644 --- a/internal/database/main.go +++ b/internal/database/main.go @@ -11,9 +11,9 @@ import ( func InitDB(db *sql.DB) { schema := ` CREATE TABLE IF NOT EXISTS currencies ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - code TEXT NOT NULL UNIQUE, - name TEXT NOT NULL + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL UNIQUE, + name TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS companies ( @@ -35,46 +35,28 @@ func InitDB(db *sql.DB) { UNIQUE(type, year, idx) ); - CREATE TABLE IF NOT EXISTS revenue_reports ( + CREATE TABLE IF NOT EXISTS category ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_id INTEGER NOT NULL, - period_id INTEGER NOT NULL, + parent_id INTEGER, + name TEXT NOT NULL, FOREIGN KEY (company_id) REFERENCES companies(id), - FOREIGN KEY (period_id) REFERENCES periods(id), - UNIQUE(company_id, period_id) + FOREIGN KEY (parent_id) REFERENCES category(id), + UNIQUE(company_id, name) ); CREATE TABLE IF NOT EXISTS revenue_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, - report_id INTEGER NOT NULL, + company_id INTEGER NOT NULL, currency_id INTEGER NOT NULL, - category TEXT NOT NULL CHECK(category IN ('product', 'location', 'total')), - label TEXT NOT NULL, + category_id INTEGER NOT NULL, + period_id INTEGER NOT NULL, value REAL NOT NULL, - FOREIGN KEY (report_id) REFERENCES revenue_reports(id), - FOREIGN KEY (currency_id) REFERENCES currencies(id) - ); - - -- one row per category type per company - -- e.g. Apple has "product" and "location" - CREATE TABLE IF NOT EXISTS category_defs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - company_id INTEGER NOT NULL, - name TEXT NOT NULL, - FOREIGN KEY (company_id) REFERENCES companies(id), - UNIQUE(company_id, name) - ); - - -- the actual labels belonging to each category - -- e.g. category_def "product" → "iPhone", "Mac", "Services" - CREATE TABLE IF NOT EXISTS category_labels ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - category_def_id INTEGER NOT NULL, - label TEXT NOT NULL, - FOREIGN KEY (category_def_id) REFERENCES category_defs(id), - UNIQUE(category_def_id, label) - ); - ` + FOREIGN KEY (company_id) REFERENCES companies(id), + FOREIGN KEY (currency_id) REFERENCES currencies(id), + FOREIGN KEY (category_id) REFERENCES category(id), + FOREIGN KEY (period_id) REFERENCES periods(id) + );` if _, err := db.Exec(schema); err != nil { log.Fatal("Failed to create tables:", err) diff --git a/internal/database/revenue.go b/internal/database/revenue.go new file mode 100644 index 0000000..fd59555 --- /dev/null +++ b/internal/database/revenue.go @@ -0,0 +1,43 @@ +package database + +import ( + "Portifolio/internal/model" + "database/sql" + "fmt" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +func GetCategoryByName(db *sql.DB, companyID int, name string) (model.RevenueCategory, error) { + var rc model.RevenueCategory + err := db.QueryRow( + `SELECT id, company_id, parent_id, name FROM category WHERE company_id = ? AND name = ?`, + companyID, name, + ).Scan(&rc.ID, &rc.CompanyID, &rc.ParentID, &rc.Name) + if err == sql.ErrNoRows { + return rc, fmt.Errorf("category %q not found for company %d", name, companyID) + } + if err != nil { + return rc, fmt.Errorf("get category by name: %w", err) + } + return rc, nil +} + +func GetPeriodByID(db *sql.DB, periodID int) (model.Period, error) { + var p model.Period + var start, end string + err := db.QueryRow( + `SELECT type, year, idx, start_date, end_date FROM periods WHERE id = ?`, + periodID, + ).Scan(&p.Type, &p.Year, &p.Index, &start, &end) + if err == sql.ErrNoRows { + return p, fmt.Errorf("period %d not found", periodID) + } + if err != nil { + return p, fmt.Errorf("get period by id: %w", err) + } + p.Start, _ = time.Parse("2006-01-02", start) + p.End, _ = time.Parse("2006-01-02", end) + return p, nil +} diff --git a/internal/handlers/revenue.go b/internal/handlers/revenue.go index b68a816..9e75b44 100644 --- a/internal/handlers/revenue.go +++ b/internal/handlers/revenue.go @@ -11,48 +11,6 @@ import ( _ "github.com/mattn/go-sqlite3" ) -func AddRevenueReportHandler(db *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var input struct { - CompanyID int `json:"company_id"` - PeriodType string `json:"period_type"` - Year int `json:"year"` - Index int `json:"index"` - } - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - http.Error(w, "invalid json", http.StatusBadRequest) - return - } - - var period model.Period - switch input.PeriodType { - case "Q": - period = model.QuarterPeriod(input.Year, input.Index) - case "H": - period = model.HalfYearPeriod(input.Year, input.Index) - case "Y": - period = model.FullYearPeriod(input.Year) - default: - http.Error(w, "invalid period_type (Q/H/Y)", http.StatusBadRequest) - return - } - - id, err := service.InsertPeriod(db, period) - 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", - "period_id": id, - "period": period.String(), - }) - } -} - func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var input struct { @@ -62,7 +20,6 @@ func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc { Year int `json:"year"` Index int `json:"index"` Category string `json:"category"` - Label string `json:"label"` Value float64 `json:"value"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { @@ -83,7 +40,7 @@ func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc { return } - if err := service.InsertRevenue(db, input.CompanyID, input.CurrencyID, input.Category, input.Label, input.Value, period); err != nil { + if err := service.InsertRevenue(db, input.CompanyID, input.CurrencyID, input.Category, nil, input.Value, period); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -111,20 +68,3 @@ func GetRevenueReportHandler(db *sql.DB) http.HandlerFunc { json.NewEncoder(w).Encode(entries) } } - -func GetRevenueSumHandler(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")) - - rs, err := service.SumRevenue(db, companyID, model.PeriodType(periodType), year) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(rs) - } -} diff --git a/internal/model/revenue.go b/internal/model/revenue.go index f9f83cd..70cc388 100644 --- a/internal/model/revenue.go +++ b/internal/model/revenue.go @@ -1,6 +1,10 @@ package model -import "time" +import ( + "database/sql" + "fmt" + "time" +) // PeriodType defines the granularity of the revenue entry type PeriodType string @@ -13,6 +17,7 @@ const ( // Period holds the actual time range for a revenue entry type Period struct { + ID int Type PeriodType Year int Index int // Q1=1 Q2=2 Q3=3 Q4=4 | H1=1 H2=2 | FY=1 @@ -20,22 +25,69 @@ type Period struct { End time.Time } -// RevenueCategory is what the revenue is broken down by -// RevenueCategory is now just a string — categories are dynamic per company -type RevenueCategory = string +func (p *Period) Insert(db *sql.DB) error { + _, err := db.Exec( + `INSERT INTO periods (type, year, idx, start_date, end_date) VALUES (?, ?, ?, ?, ?) + ON CONFLICT(type, year, idx) DO UPDATE SET start_date=excluded.start_date, end_date=excluded.end_date`, + string(p.Type), p.Year, p.Index, p.Start.Format("2006-01-02"), p.End.Format("2006-01-02"), + ) + if err != nil { + return fmt.Errorf("upsert period: %w", err) + } -// Built-in reserved categories -const ( - CategoryTotal RevenueCategory = "total" // always required, never custom -) + var id int + err = db.QueryRow( + `SELECT id FROM periods WHERE type = ? AND year = ? AND idx = ?`, + string(p.Type), p.Year, p.Index, + ).Scan(&id) + if err != nil { + return fmt.Errorf("select period: %w", err) + } + p.ID = id + return nil +} -// CategoryDef defines a custom category for a specific company -// e.g. Company: Apple, Name: "product", Labels: ["iPhone", "Mac", "Services"] -type CategoryDef struct { - ID int - Company *Company - Name string // e.g. "product", "location", "segment" - Labels []string // populated from category_labels table +type RevenueCategory struct { + ID int + CompanyID int + ParentID *int + Name string // e.g. "product", "location", "segment" +} + +func (rc *RevenueCategory) Validate() error { + if rc.ID == 0 { + return fmt.Errorf("No ID Set") + } else if rc.Name == "" { + return fmt.Errorf("No Name found") + } else if rc.CompanyID == 0 { + return fmt.Errorf("No Company Set") + } + return nil +} + +func (rc *RevenueCategory) Insert(db *sql.DB) error { + if err := rc.Validate(); err != nil { + return fmt.Errorf("failed to insert: %w", err) + } + + _, err := db.Exec( + `INSERT INTO category (company_id, parent_id, name) VALUES (?, ?, ?) + ON CONFLICT(company_id, name) DO UPDATE SET parent_id=excluded.parent_id`, + rc.CompanyID, rc.ParentID, rc.Name, + ) + if err != nil { + return fmt.Errorf("upsert category: %w", err) + } + + err = db.QueryRow( + `SELECT id FROM category WHERE company_id = ? AND name = ?`, + rc.CompanyID, rc.Name, + ).Scan(&rc.ID) + if err != nil { + return fmt.Errorf("select category id: %w", err) + } + + return nil } // Revenue is a single line in a financial report @@ -43,82 +95,7 @@ type Revenue struct { ID int Company *Company Currency *Currency - Category RevenueCategory - Label string // e.g. "North America", "iPhone", "Total" + Category *RevenueCategory + Period *Period Value float64 - Period Period -} - -// RevenueReport groups revenue lines for a company/period -// and verifies that sub-categories sum to total -type RevenueReport struct { - Company *Company - Period Period - Entries []Revenue -} - -// Total returns the sum of all entries matching a category -func (r *RevenueReport) Total(category RevenueCategory) float64 { - var sum float64 - for _, e := range r.Entries { - if e.Category == category { - sum += e.Value - } - } - return sum -} - -// SumByCategory returns a map of category → sum for all entries -func (r *RevenueReport) SumByCategory() map[RevenueCategory]float64 { - sums := make(map[RevenueCategory]float64) - for _, e := range r.Entries { - sums[e.Category] += e.Value - } - return sums -} - -// Validate checks every category independently sums to total -func (r *RevenueReport) Validate() (bool, map[RevenueCategory]float64) { - total := r.Total(CategoryTotal) - sums := r.SumByCategory() - delete(sums, CategoryTotal) // don't compare total to itself - - allMatch := true - for cat, sum := range sums { - if sum != total { - allMatch = false - _ = cat // caller can inspect the map to see which failed - } - } - return allMatch, sums -} - -// RevSum aggregates revenue across multiple reports or periods -// Use this when you want e.g. FY = Q1+Q2+Q3+Q4 -type RevSum struct { - Company *Company - Categories map[RevenueCategory]float64 // category → total across all periods - Labels map[string]float64 // label → total (e.g. "iPhone" across quarters) - Total float64 - Periods []Period // which periods were summed -} - -func NewRevSum(company *Company, reports []RevenueReport) *RevSum { - rs := &RevSum{ - Company: company, - Categories: make(map[RevenueCategory]float64), - Labels: make(map[string]float64), - } - for _, report := range reports { - rs.Periods = append(rs.Periods, report.Period) - for _, e := range report.Entries { - if e.Category == CategoryTotal { - rs.Total += e.Value - continue - } - rs.Categories[e.Category] += e.Value - rs.Labels[e.Label] += e.Value - } - } - return rs } diff --git a/internal/service/revenue.go b/internal/service/revenue.go index d3ef5fd..a103d87 100644 --- a/internal/service/revenue.go +++ b/internal/service/revenue.go @@ -1,6 +1,7 @@ package service import ( + "Portifolio/internal/database" "Portifolio/internal/model" "database/sql" "fmt" @@ -9,69 +10,64 @@ import ( _ "github.com/mattn/go-sqlite3" ) -func InsertRevenue(db *sql.DB, companyID, currencyID int, category, label string, value float64, period model.Period) error { - periodID, err := InsertPeriod(db, period) - if err != nil { - return err - } +func InsertRevenue(db *sql.DB, companyID, currencyID int, categoryName string, parentID *int, value float64, period model.Period) error { - reportID, err := getOrCreateReport(db, companyID, periodID) + period, err := database.GetPeriodByID(db, period.ID) if err != nil { - return err + err = period.Insert(db) + if err != nil { + return err + } + } + // Getting Category, if error, trying to insert the category with the company. + category, err := database.GetCategoryByName(db, companyID, categoryName) + if err != nil { + category = model.RevenueCategory{ + CompanyID: companyID, + ParentID: parentID, + Name: categoryName, + } + err := category.Insert(db) + if err != nil { + return err + } } _, err = db.Exec( - `INSERT INTO revenue_entries (report_id, currency_id, category, label, value) VALUES (?, ?, ?, ?, ?)`, - reportID, currencyID, category, label, value, - ) - return err -} - -func InsertPeriod(db *sql.DB, p model.Period) (int, error) { - res, err := db.Exec( - `INSERT INTO periods (type, year, idx, start_date, end_date) VALUES (?, ?, ?, ?, ?) - ON CONFLICT(type, year, idx) DO UPDATE SET start_date=excluded.start_date`, - string(p.Type), p.Year, p.Index, p.Start.Format("2006-01-02"), p.End.Format("2006-01-02"), + `INSERT INTO revenue_entries (company_id, currency_id, category_id, period_id, value) + VALUES (?, ?, ?, ?, ?)`, + companyID, currencyID, category.ID, period.ID, value, ) if err != nil { - return 0, err + return fmt.Errorf("insert revenue_entries: %w", err) } - id, err := res.LastInsertId() - return int(id), err -} -func getOrCreateReport(db *sql.DB, companyID, periodID int) (int, error) { - var id int - err := db.QueryRow( - `SELECT id FROM revenue_reports WHERE company_id = ? AND period_id = ?`, - companyID, periodID, - ).Scan(&id) - if err == sql.ErrNoRows { - res, err := db.Exec( - `INSERT INTO revenue_reports (company_id, period_id) VALUES (?, ?)`, - companyID, periodID, - ) - if err != nil { - return 0, err - } - lid, _ := res.LastInsertId() - return int(lid), nil - } - return id, err + return nil } func GetRevenue(db *sql.DB, companyID int, periodType model.PeriodType, year, idx int) ([]model.Revenue, error) { rows, err := db.Query(` - SELECT e.id, e.category, e.label, e.value, + WITH RECURSIVE tree AS ( + SELECT id, name, parent_id + FROM category + WHERE company_id = ? AND parent_id IS NULL + + UNION ALL + + SELECT c.id, c.name, c.parent_id + FROM category c + JOIN tree t ON c.parent_id = t.id + ) + SELECT e.id, t.name, e.value, p.type, p.year, p.idx, p.start_date, p.end_date FROM revenue_entries e - JOIN revenue_reports r ON e.report_id = r.id - JOIN periods p ON r.period_id = p.id - WHERE r.company_id = ? AND p.type = ? AND p.year = ? AND p.idx = ?`, - companyID, string(periodType), year, idx, + JOIN tree t ON e.category_id = t.id + JOIN periods p ON e.period_id = p.id + WHERE e.company_id = ? AND p.type = ? AND p.year = ? AND p.idx = ?`, + companyID, companyID, string(periodType), year, idx, ) if err != nil { - return nil, err + return nil, fmt.Errorf("query revenue: %w", err) } defer rows.Close() @@ -80,56 +76,14 @@ func GetRevenue(db *sql.DB, companyID int, periodType model.PeriodType, year, id var e model.Revenue var p model.Period var start, end string - if err := rows.Scan(&e.ID, &e.Category, &e.Label, &e.Value, + if err := rows.Scan(&e.ID, &e.Category, &e.Value, &p.Type, &p.Year, &p.Index, &start, &end); err != nil { - return nil, err + return nil, fmt.Errorf("scan revenue row: %w", err) } p.Start, _ = time.Parse("2006-01-02", start) p.End, _ = time.Parse("2006-01-02", end) - e.Period = p + e.Period = &p entries = append(entries, e) } return entries, rows.Err() } - -func SumRevenue(db *sql.DB, companyID int, periodType model.PeriodType, year int) (*model.RevSum, error) { - rows, err := db.Query(` - SELECT e.category, e.label, e.value, p.type, p.year, p.idx - FROM revenue_entries e - JOIN revenue_reports r ON e.report_id = r.id - JOIN periods p ON r.period_id = p.id - WHERE r.company_id = ? AND p.type = ? AND p.year = ?`, - companyID, string(periodType), year, - ) - if err != nil { - return nil, err - } - defer rows.Close() - - rs := &model.RevSum{ - Categories: make(map[model.RevenueCategory]float64), - Labels: make(map[string]float64), - } - seen := map[string]bool{} - for rows.Next() { - var category, label string - var value float64 - var pType model.PeriodType - var pYear, pIdx int - if err := rows.Scan(&category, &label, &value, &pType, &pYear, &pIdx); err != nil { - return nil, err - } - key := fmt.Sprintf("%s-%d-%d", pType, pYear, pIdx) - if !seen[key] { - rs.Periods = append(rs.Periods, model.Period{Type: pType, Year: pYear, Index: pIdx}) - seen[key] = true - } - if category == model.CategoryTotal { - rs.Total += value - } else { - rs.Categories[category] += value - rs.Labels[label] += value - } - } - return rs, rows.Err() -} diff --git a/internal/shell/revenue.go b/internal/shell/revenue.go index b37065c..b50649e 100644 --- a/internal/shell/revenue.go +++ b/internal/shell/revenue.go @@ -10,144 +10,120 @@ import ( "strings" ) -func AddRevenue(scanner *bufio.Scanner, db *sql.DB) { - fmt.Print(" Company ID: ") +func promptInt(scanner *bufio.Scanner, label string) (int, error) { + fmt.Print(label) scanner.Scan() - companyID, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) + v, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) if err != nil { - fmt.Println(" ✗ Invalid company ID") - return + 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 } - fmt.Print(" Currency ID: ") - scanner.Scan() - currencyID, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) - if err != nil { - fmt.Println(" ✗ Invalid currency ID") - return - } - - fmt.Print(" Period type (Q/H/Y): ") - scanner.Scan() - periodType := strings.ToUpper(strings.TrimSpace(scanner.Text())) - - fmt.Print(" Year: ") - scanner.Scan() - year, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) - if err != nil { - fmt.Println(" ✗ Invalid year") - return - } - - fmt.Print(" Index (Q: 1-4 | H: 1-2 | Y: 1): ") - scanner.Scan() - idx, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) - if err != nil { - fmt.Println(" ✗ Invalid index") - return - } - - fmt.Print(" Category (product/location/total): ") - scanner.Scan() - category := strings.TrimSpace(scanner.Text()) - - fmt.Print(" Label (e.g. iPhone, Americas): ") - scanner.Scan() - label := strings.TrimSpace(scanner.Text()) - - fmt.Print(" Value: ") - scanner.Scan() - value, err := strconv.ParseFloat(strings.TrimSpace(scanner.Text()), 64) - if err != nil { - fmt.Println(" ✗ Invalid value") - return - } - - var period model.Period switch periodType { - case "Q": - period = model.QuarterPeriod(year, idx) - case "H": - period = model.HalfYearPeriod(year, idx) case "Y": - period = model.FullYearPeriod(year) + 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: - fmt.Println(" ✗ Invalid period type") + 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 + } + 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 } - if err := service.InsertRevenue(db, companyID, currencyID, category, label, value, period); err != nil { + if err := service.InsertRevenue(db, companyID, currencyID, category, parentID, value, period); err != nil { fmt.Println(" ✗ Error:", err) return } - fmt.Printf(" ✓ Revenue entry added: %s / %s = %.2f (%s)\n", category, label, value, period.String()) + fmt.Printf(" ✓ Revenue added: %s = %.2f (%s)\n", category, value, period.String()) } func ListRevenue(scanner *bufio.Scanner, db *sql.DB) { - fmt.Print(" Company ID: ") - scanner.Scan() - companyID, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) + companyID, err := promptInt(scanner, " Company ID: ") if err != nil { - fmt.Println(" ✗ Invalid company ID") + fmt.Println(" ✗", err) + return + } + period, err := promptPeriod(scanner) + if err != nil { + fmt.Println(" ✗", err) return } - fmt.Print(" Period type (Q/H/Y): ") - scanner.Scan() - periodType := strings.ToUpper(strings.TrimSpace(scanner.Text())) - - fmt.Print(" Year: ") - scanner.Scan() - year, _ := strconv.Atoi(strings.TrimSpace(scanner.Text())) - - fmt.Print(" Index: ") - scanner.Scan() - idx, _ := strconv.Atoi(strings.TrimSpace(scanner.Text())) - - entries, err := service.GetRevenue(db, companyID, model.PeriodType(periodType), year, idx) + entries, err := service.GetRevenue(db, companyID, period.Type, period.Year, period.Index) if err != nil { fmt.Println(" ✗ Error:", err) return } - fmt.Printf("\n %-12s %-20s %12s\n", "CATEGORY", "LABEL", "VALUE") - fmt.Println(" " + strings.Repeat("-", 46)) + fmt.Printf("\n %-20s %12s\n", "CATEGORY", "VALUE") + fmt.Println(" " + strings.Repeat("-", 34)) for _, e := range entries { - fmt.Printf(" %-12s %-20s %12.2f\n", e.Category, e.Label, e.Value) - } -} - -func SumRevenue(scanner *bufio.Scanner, db *sql.DB) { - fmt.Print(" Company ID: ") - scanner.Scan() - companyID, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) - if err != nil { - fmt.Println(" ✗ Invalid company ID") - return - } - - fmt.Print(" Period type to sum (Q/H/Y): ") - scanner.Scan() - periodType := strings.ToUpper(strings.TrimSpace(scanner.Text())) - - fmt.Print(" Year: ") - scanner.Scan() - year, _ := strconv.Atoi(strings.TrimSpace(scanner.Text())) - - rs, err := service.SumRevenue(db, companyID, model.PeriodType(periodType), year) - if err != nil { - fmt.Println(" ✗ Error:", err) - return - } - - fmt.Printf("\n Revenue Sum — FY%d\n", year) - fmt.Printf(" Total: %.2f\n\n", rs.Total) - fmt.Println(" By Category:") - for cat, sum := range rs.Categories { - fmt.Printf(" %-15s %.2f\n", cat, sum) - } - fmt.Println("\n By Label:") - for label, sum := range rs.Labels { - fmt.Printf(" %-20s %.2f\n", label, sum) + fmt.Printf(" %-20s %12.2f\n", e.Category, e.Value) } } diff --git a/main.go b/main.go index f4fd955..9cc4110 100644 --- a/main.go +++ b/main.go @@ -42,10 +42,8 @@ func main() { http.HandleFunc("GET /currencies", handlers.GetCurrenciesHandler(db)) // Revenue - http.HandleFunc("POST /add/revenue/report", handlers.AddRevenueReportHandler(db)) http.HandleFunc("POST /add/revenue/entry", handlers.AddRevenueEntryHandler(db)) http.HandleFunc("GET /revenue/report", handlers.GetRevenueReportHandler(db)) - http.HandleFunc("GET /revenue/sum", handlers.GetRevenueSumHandler(db)) fmt.Println("Server running on :8080") go func() { @@ -88,8 +86,6 @@ func runShell(db *sql.DB) { shell.AddRevenue(scanner, db) case "list-revenue": shell.ListRevenue(scanner, db) - case "sum-revenue": - shell.SumRevenue(scanner, db) case "help": fmt.Println("\nCommands:")