diff --git a/cmd/web/web.go b/cmd/web/web.go index 4b3a58e..ff71904 100644 --- a/cmd/web/web.go +++ b/cmd/web/web.go @@ -1,6 +1,7 @@ package main import ( + "github.com/datasektionen/GOrdian/internal/config" "github.com/datasektionen/GOrdian/internal/database" "log" "net/http" @@ -9,11 +10,18 @@ import ( ) func main() { - db, err := database.Connect() + envVar := config.GetEnv() + dbGO, err := database.Connect(envVar.PsqlconnStringGOrdian) if err != nil { - log.Printf("error accessing database: %v", err) + log.Printf("error accessing GOrdian database: %v", err) } - if err := web.Mount(http.DefaultServeMux, db); err != nil { + + dbCF, err := database.Connect(envVar.PsqlconnStringCashflow) + if err != nil { + log.Printf("error accessing Cashflow database: %v", err) + } + + if err := web.Mount(http.DefaultServeMux, web.Databases{DBCF: dbCF, DBGO: dbGO}); err != nil { panic(err) } panic(http.ListenAndServe("0.0.0.0:3000", nil)) diff --git a/internal/config/env.go b/internal/config/env.go index 9f45b3b..ac5ec8a 100644 --- a/internal/config/env.go +++ b/internal/config/env.go @@ -3,47 +3,39 @@ package config import "os" type EnvVar struct { - DBHost string - DBPort string - DBUser string - DBPass string - DBName string - LoginURL string - LoginToken string - PlsURL string - PlsSystem string - ServerPort string - ServerURL string + PsqlconnStringGOrdian string + PsqlconnStringCashflow string + LoginURL string + LoginToken string + PlsURL string + PlsSystem string + ServerPort string + ServerURL string } func GetEnv() EnvVar { envConfig := EnvVar{ - DBHost: os.Getenv("DB_HOST"), - DBPort: os.Getenv("DB_PORT"), - DBUser: os.Getenv("DB_USER"), - DBPass: os.Getenv("DB_PASS"), - DBName: os.Getenv("DB_NAME"), - LoginURL: os.Getenv("LOGIN_URL"), - LoginToken: os.Getenv("LOGIN_TOKEN"), - PlsURL: os.Getenv("PLS_URL"), - PlsSystem: os.Getenv("PLS_SYSTEM"), - ServerPort: os.Getenv("SERVER_PORT"), - ServerURL: os.Getenv("SERVER_URL"), + //prod + PsqlconnStringGOrdian: os.Getenv("GO_CONN"), + PsqlconnStringCashflow: os.Getenv("CF_CONN"), + LoginURL: os.Getenv("LOGIN_URL"), + LoginToken: os.Getenv("LOGIN_TOKEN"), + PlsURL: os.Getenv("PLS_URL"), + PlsSystem: os.Getenv("PLS_SYSTEM"), + ServerPort: os.Getenv("SERVER_PORT"), + ServerURL: os.Getenv("SERVER_URL"), + + // local + // PsqlconnStringGOrdian: "host=localhost port=5432 user=alexander password=kopis dbname=budget_local sslmode=disable", + // PsqlconnStringCashflow: "host=localhost port=54321 user=cashflow password=cashflow dbname=cashflow sslmode=disable", + // LoginURL: "https://login.datasektionen.se", + // PlsURL: "https://pls.datasektionen.se", + // PlsSystem: "gordian", + // ServerPort: "3000", + // ServerURL: "http://localhost:3000", + // LoginToken: "this is secret fuck you", } return envConfig } - -//example -// DBHost: localhost, -// DBPort: "5432", -// DBUser: "alexander", -// DBPass: "kopis", -// DBName: "budget_local", -// LoginURL: "https://login.datasektionen.se", -// LoginToken: "this is secret fuck you", -// PlsURL: "https://pls.datasektionen.se", -// PlsSystem: "gordian", -// ServerPort: "3000", -// ServerURL: "http://localhost:3000", diff --git a/internal/database/loader.go b/internal/database/loader.go index 1c46f83..af674fd 100644 --- a/internal/database/loader.go +++ b/internal/database/loader.go @@ -1,22 +1,19 @@ package database import ( + "database/sql" "fmt" "github.com/datasektionen/GOrdian/internal/excel" "io" ) -func SaveBudget(fileReader io.Reader) error { +func SaveBudget(fileReader io.Reader, db *sql.DB) error { fmt.Println("You have very many money") //testBudget := "test/Budget_2024.xlsx" costCentres, secondaryCostCentres, budgetLines, err := excel.ReadExcel(fileReader) - if err != nil { - return fmt.Errorf("error parsing Excel file: %v", err) - } - db, err := Connect() if err != nil { - return fmt.Errorf("error accessing database: %v", err) + return fmt.Errorf("error parsing Excel file: %v", err) } err = WipeDatabase(db) diff --git a/internal/database/postgres.go b/internal/database/postgres.go index 6b7132d..fc94971 100644 --- a/internal/database/postgres.go +++ b/internal/database/postgres.go @@ -3,16 +3,10 @@ package database import ( "database/sql" "fmt" - "github.com/datasektionen/GOrdian/internal/config" _ "github.com/lib/pq" ) -func Connect() (*sql.DB, error) { - - envVar := config.GetEnv() - - psqlconnString := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - envVar.DBHost, envVar.DBPort, envVar.DBUser, envVar.DBPass, envVar.DBName) +func Connect(psqlconnString string) (*sql.DB, error) { db, err := sql.Open("postgres", psqlconnString) if err != nil { diff --git a/internal/web/DBgetters.go b/internal/web/DBgetters.go new file mode 100644 index 0000000..ce0c6d5 --- /dev/null +++ b/internal/web/DBgetters.go @@ -0,0 +1,149 @@ +package web + +import ( + "database/sql" + "fmt" + "github.com/datasektionen/GOrdian/internal/excel" +) + +func getBudgetLinesByCostCentreID(db *sql.DB, costCentreID int) ([]excel.BudgetLine, error) { + var budgetLinesGetStatementStatic = ` + SELECT + budget_lines.id, + budget_lines.name, + income, + expense, + comment, + account, + secondary_cost_centres.id, + secondary_cost_centres.name + FROM budget_lines + JOIN secondary_cost_centres ON secondary_cost_centres.id = secondary_cost_centre_id + WHERE cost_centre_id = $1 + ORDER BY secondary_cost_centre_id + ` + result, err := db.Query(budgetLinesGetStatementStatic, costCentreID) + if err != nil { + return nil, fmt.Errorf("failed to get budget lines from database: %v", err) + } + var budgetLines []excel.BudgetLine + for result.Next() { + var budgetLine excel.BudgetLine + + err := result.Scan( + &budgetLine.BudgetLineID, + &budgetLine.BudgetLineName, + &budgetLine.BudgetLineIncome, + &budgetLine.BudgetLineExpense, + &budgetLine.BudgetLineComment, + &budgetLine.BudgetLineAccount, + &budgetLine.SecondaryCostCentreID, + &budgetLine.SecondaryCostCentreName, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan budget line from query result: %v", err) + } + budgetLines = append(budgetLines, budgetLine) + } + return budgetLines, nil +} + +func getCostCentres(db *sql.DB) ([]excel.CostCentre, error) { + var costCentresGetStatementStatic = `SELECT id, name, type FROM cost_centres ORDER BY name` + result, err := db.Query(costCentresGetStatementStatic) + if err != nil { + return nil, fmt.Errorf("failed to get cost centres from database: %v", err) + } + var costCentres []excel.CostCentre + for result.Next() { + var costCentre excel.CostCentre + + err := result.Scan(&costCentre.CostCentreID, &costCentre.CostCentreName, &costCentre.CostCentreType) + if err != nil { + return nil, fmt.Errorf("failed to scan cost centre from query result: %v", err) + } + costCentres = append(costCentres, costCentre) + } + return costCentres, nil +} + +func getCostCentreByID(db *sql.DB, costCentreID int) (excel.CostCentre, error) { + var costCentreGetStatementStatic = `SELECT id, name, type FROM cost_centres WHERE id = $1` + result := db.QueryRow(costCentreGetStatementStatic, costCentreID) + var costCentre excel.CostCentre + err := result.Scan(&costCentre.CostCentreID, &costCentre.CostCentreName, &costCentre.CostCentreType) + if err != nil { + return excel.CostCentre{}, fmt.Errorf("failed to scan cost centre from query result: %v", err) + } + return costCentre, nil +} + +func getSecondaryCostCentresByCostCentreID(db *sql.DB, costCentreID int) ([]excel.SecondaryCostCentre, error) { + var SecondaryCostCentresGetStatementStatic = ` + SELECT + id, + name, + cost_centre_id + FROM secondary_cost_centres + WHERE cost_centre_id = $1 + ORDER BY id + ` + result, err := db.Query(SecondaryCostCentresGetStatementStatic, costCentreID) + if err != nil { + return nil, fmt.Errorf("failed to get secondary cost centres from database: %v", err) + } + var secondaryCostCentres []excel.SecondaryCostCentre + for result.Next() { + var secondaryCostCentre excel.SecondaryCostCentre + + err := result.Scan( + &secondaryCostCentre.SecondaryCostCentreID, + &secondaryCostCentre.SecondaryCostCentreName, + &secondaryCostCentre.CostCentreID, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan secondary cost centre from query result: %v", err) + } + secondaryCostCentres = append(secondaryCostCentres, secondaryCostCentre) + } + return secondaryCostCentres, nil +} + +func getBudgetLinesBySecondaryCostCentreID(db *sql.DB, secondaryCostCentreID int) ([]excel.BudgetLine, error) { + var budgetLinesGetStatementStatic = ` + SELECT + id, + name, + income, + expense, + comment, + account, + secondary_cost_centre_id + FROM budget_lines + WHERE secondary_cost_centre_id = $1 + ORDER BY id + ` + result, err := db.Query(budgetLinesGetStatementStatic, secondaryCostCentreID) + if err != nil { + return nil, fmt.Errorf("failed to get budgetlines from database: %v", err) + } + var budgetLines []excel.BudgetLine + for result.Next() { + var budgetLine excel.BudgetLine + + err := result.Scan( + &budgetLine.BudgetLineID, + &budgetLine.BudgetLineName, + &budgetLine.BudgetLineIncome, + &budgetLine.BudgetLineExpense, + &budgetLine.BudgetLineComment, + &budgetLine.BudgetLineAccount, + &budgetLine.SecondaryCostCentreID, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan budget line from query result: %v", err) + } + budgetLines = append(budgetLines, budgetLine) + } + return budgetLines, nil +} diff --git a/internal/web/api.go b/internal/web/api.go new file mode 100644 index 0000000..ef8ddc6 --- /dev/null +++ b/internal/web/api.go @@ -0,0 +1,53 @@ +package web + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" + "strconv" +) + +func apiCostCentres(w http.ResponseWriter, r *http.Request, db *sql.DB) error { + costCentres, err := getCostCentres(db) + if err != nil { + return fmt.Errorf("failed get scan cost centres information from database: %v", err) + } + err = json.NewEncoder(w).Encode(costCentres) + if err != nil { + return fmt.Errorf("failed to encode cost centres to json: %v", err) + } + return nil +} + +func apiSecondaryCostCentre(w http.ResponseWriter, r *http.Request, db *sql.DB) error { + idCC, err := strconv.Atoi(r.FormValue("id")) + if err != nil { + return fmt.Errorf("failed to convert secondary cost centre id to int: %v", err) + } + secondaryCostCentres, err := getSecondaryCostCentresByCostCentreID(db, idCC) + if err != nil { + return fmt.Errorf("failed get scan sendondary cost centres information from database: %v", err) + } + err = json.NewEncoder(w).Encode(secondaryCostCentres) + if err != nil { + return fmt.Errorf("failed to encode secondary cost centres to json: %v", err) + } + return nil +} + +func apiBudgetLine(w http.ResponseWriter, r *http.Request, db *sql.DB) error { + idSCC, err := strconv.Atoi(r.FormValue("id")) + if err != nil { + return fmt.Errorf("failed to convert SCC id fromstring to int: %v", err) + } + budgetLines, err := getBudgetLinesBySecondaryCostCentreID(db, idSCC) + if err != nil { + return fmt.Errorf("failed get scan budget lines information from database: %v", err) + } + err = json.NewEncoder(w).Encode(budgetLines) + if err != nil { + return fmt.Errorf("failed to encode budget lines to json: %v", err) + } + return nil +} diff --git a/internal/web/costcentre.go b/internal/web/costcentre.go new file mode 100644 index 0000000..64eb3f4 --- /dev/null +++ b/internal/web/costcentre.go @@ -0,0 +1,102 @@ +package web + +import ( + "database/sql" + "fmt" + "github.com/datasektionen/GOrdian/internal/excel" + "net/http" + "strconv" +) + +func costCentrePage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []string, loggedIn bool) error { + costCentreIDString := r.PathValue("costCentreIDPath") + costCentreIDInt, err := strconv.Atoi(costCentreIDString) + if err != nil { + return fmt.Errorf("failed to convert cost centre id from string to int: %v", err) + } + + budgetLines, err := getBudgetLinesByCostCentreID(db, costCentreIDInt) + if err != nil { + return fmt.Errorf("failed get scan budget line information from database: %v", err) + } + + //omg + secondaryCostCentresWithBudgetLinesList := make([]secondaryCostCentresWithBudgetLines, 1) + currentSecondaryCostCentre := &secondaryCostCentresWithBudgetLinesList[0] + for _, budgetLine := range budgetLines { + if currentSecondaryCostCentre.SecondaryCostCentreName != budgetLine.SecondaryCostCentreName { + secondaryCostCentresWithBudgetLinesList = append(secondaryCostCentresWithBudgetLinesList, secondaryCostCentresWithBudgetLines{ + SecondaryCostCentreName: budgetLine.SecondaryCostCentreName, + BudgetLines: []excel.BudgetLine{}, + }) + currentSecondaryCostCentre = &secondaryCostCentresWithBudgetLinesList[len(secondaryCostCentresWithBudgetLinesList)-1] + } + currentSecondaryCostCentre.BudgetLines = append(currentSecondaryCostCentre.BudgetLines, budgetLine) + } + secondaryCostCentresWithBudgetLinesList = secondaryCostCentresWithBudgetLinesList[1:] + + costCentre, err := getCostCentreByID(db, costCentreIDInt) + if err != nil { + return fmt.Errorf("failed get scan cost centre information from database: %v", err) + } + + //calc the total incomes, expenses and results of all cost centres in the list + secondaryCostCentresWithBudgetLinesList, err = calculateSecondaryCostCentres(secondaryCostCentresWithBudgetLinesList) + if err != nil { + return fmt.Errorf("failed calculate secondary cost centre values: %v", err) + } + + costCentreTotalIncome, costCentreTotalExpense, costCentreTotalResult, err := calculateCostCentre(secondaryCostCentresWithBudgetLinesList) + if err != nil { + return fmt.Errorf("failed calculate cost centre values: %v", err) + } + + if err := templates.ExecuteTemplate(w, "costcentre.gohtml", map[string]any{ + "motd": motdGenerator(), + "secondaryCostCentresWithBudgetLinesList": secondaryCostCentresWithBudgetLinesList, + "costCentre": costCentre, + "costCentreTotalIncome": costCentreTotalIncome, + "costCentreTotalExpense": costCentreTotalExpense, + "costCentreTotalResult": costCentreTotalResult, + "permissions": perms, + "loggedIn": loggedIn, + }); err != nil { + return fmt.Errorf("could not render template: %w", err) + } + return nil +} + +func calculateCostCentre(secondaryCostCentresWithBudgetLinesList []secondaryCostCentresWithBudgetLines) (int, int, int, error) { + var totalIncome int + var totalExpense int + for _, sCCWithBudgetLines := range secondaryCostCentresWithBudgetLinesList { + totalIncome = totalIncome + sCCWithBudgetLines.SecondaryCostCentreTotalIncome + totalExpense = totalExpense + sCCWithBudgetLines.SecondaryCostCentreTotalExpense + } + totalResult := totalIncome + totalExpense + + return totalIncome, totalExpense, totalResult, nil +} + +func calculateSecondaryCostCentres(secondaryCostCentresWithBudgetLinesList []secondaryCostCentresWithBudgetLines) ([]secondaryCostCentresWithBudgetLines, error) { + for index, sCCWithBudgetLines := range secondaryCostCentresWithBudgetLinesList { + var totalIncome int + var totalExpense int + for _, budgetLine := range sCCWithBudgetLines.BudgetLines { + totalIncome = totalIncome + budgetLine.BudgetLineIncome + totalExpense = totalExpense + budgetLine.BudgetLineExpense + } + secondaryCostCentresWithBudgetLinesList[index].SecondaryCostCentreTotalIncome = totalIncome + secondaryCostCentresWithBudgetLinesList[index].SecondaryCostCentreTotalExpense = totalExpense + secondaryCostCentresWithBudgetLinesList[index].SecondaryCostCentreTotalResult = totalIncome + totalExpense + } + return secondaryCostCentresWithBudgetLinesList, nil +} + +type secondaryCostCentresWithBudgetLines struct { + SecondaryCostCentreName string + SecondaryCostCentreTotalIncome int + SecondaryCostCentreTotalExpense int + SecondaryCostCentreTotalResult int + BudgetLines []excel.BudgetLine +} diff --git a/internal/web/frame.go b/internal/web/frame.go new file mode 100644 index 0000000..01770a5 --- /dev/null +++ b/internal/web/frame.go @@ -0,0 +1,163 @@ +package web + +import ( + "database/sql" + "fmt" + "net/http" + + "github.com/datasektionen/GOrdian/internal/excel" +) + +type FrameLine struct { + FrameLineName string + FrameLineIncome int + FrameLineExpense int + FrameLineInternal int + FrameLineResult int +} + +func framePage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []string, loggedIn bool) error { + budgetLines, err := getFrameLines(db) + if err != nil { + return fmt.Errorf("failed get scan budget lines information from database: %v", err) + } + committeeFrameLines, projectFrameLines, otherFrameLines, totalFrameLine, sumCommitteeFrameLine, sumProjectFrameLine, sumOtherFrameLine, err := generateFrameLines(budgetLines) + if err != nil { + return fmt.Errorf("failed to generate frame budget lines: %v", err) + } + if err := templates.ExecuteTemplate(w, "frame.gohtml", map[string]any{ + "motd": motdGenerator(), + "committeeframelines": committeeFrameLines, + "projectframelines": projectFrameLines, + "otherframelines": otherFrameLines, + "totalframeline": totalFrameLine, + "sumcommitteeframeline": sumCommitteeFrameLine, + "sumprojectframeline": sumProjectFrameLine, + "sumotherframeline": sumOtherFrameLine, + "permissions": perms, + "loggedIn": loggedIn, + }); err != nil { + return fmt.Errorf("could not render template: %w", err) + } + return nil +} + +func getFrameLines(db *sql.DB) ([]excel.BudgetLine, error) { + var frameLinesGetStatementStatic = ` + SELECT + SUM(income), + SUM(expense), + secondary_cost_centres.name ILIKE '%Internt%', + cost_centres.id, + cost_centres.name, + cost_centres.type + FROM budget_lines + JOIN secondary_cost_centres ON secondary_cost_centres.id = secondary_cost_centre_id + JOIN cost_centres ON secondary_cost_centres.cost_centre_id = cost_centres.id + GROUP BY cost_centres.id, cost_centres.name, cost_centres.type, secondary_cost_centres.name ILIKE '%Internt%' + ORDER BY cost_centres.name, secondary_cost_centres.name ILIKE '%Internt%' + ` + result, err := db.Query(frameLinesGetStatementStatic) + if err != nil { + return nil, fmt.Errorf("failed to get framelines from database: %v", err) + } + var frameLines []excel.BudgetLine + for result.Next() { + var frameLine excel.BudgetLine + + err := result.Scan( + &frameLine.BudgetLineIncome, + &frameLine.BudgetLineExpense, + &frameLine.SecondaryCostCentreName, + &frameLine.CostCentreID, + &frameLine.CostCentreName, + &frameLine.CostCentreType, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan budget line from query result: %v", err) + } + frameLines = append(frameLines, frameLine) + } + return frameLines, nil +} + +func generateFrameLines(frameLines []excel.BudgetLine) ([]FrameLine, []FrameLine, []FrameLine, FrameLine, FrameLine, FrameLine, FrameLine, error) { + var committeeFrameLines []FrameLine + var projectFrameLines []FrameLine + var otherFrameLines []FrameLine + var totalFrameLine FrameLine + var sumCommitteeFrameLine FrameLine + var sumProjectFrameLine FrameLine + var sumOtherFrameLine FrameLine + + totalFrameLine.FrameLineName = "Totalt" + sumCommitteeFrameLine.FrameLineName = "Summa nämnder" + sumProjectFrameLine.FrameLineName = "Summa projekt" + sumOtherFrameLine.FrameLineName = "Summa övrigt" + + var skippidi bool + for i, frameLine := range frameLines { + if skippidi { + skippidi = false + continue + } + frameLineIncome := frameLine.BudgetLineIncome + frameLineExpense := frameLine.BudgetLineExpense + frameLineName := frameLine.CostCentreName + frameLineInternal := 0 + frameLineResult := 0 + + // each CC appears twice, once for internal costs, once for the rest + // both are handled i and i+1 + // skippidi makes sure that the loop incements by two + if i+1 < len(frameLines) && frameLines[i+1].CostCentreName == frameLineName { + frameLineIncome += frameLines[i+1].BudgetLineIncome + frameLineExpense += frameLines[i+1].BudgetLineExpense + frameLineInternal = frameLines[i+1].BudgetLineIncome + frameLines[i+1].BudgetLineExpense + skippidi = true + } + + frameLineResult = frameLineIncome + frameLineExpense + + reconstructedFrameLine := FrameLine{frameLineName, frameLineIncome, frameLineExpense, frameLineInternal, frameLineResult} + + totalFrameLine.FrameLineIncome += frameLineIncome + totalFrameLine.FrameLineExpense += frameLineExpense + totalFrameLine.FrameLineInternal += frameLineInternal + totalFrameLine.FrameLineResult += frameLineResult + + switch frameLine.CostCentreType { + case "committee": + committeeFrameLines = append(committeeFrameLines, reconstructedFrameLine) + case "project": + projectFrameLines = append(projectFrameLines, reconstructedFrameLine) + case "other": + otherFrameLines = append(otherFrameLines, reconstructedFrameLine) + default: + return nil, nil, nil, FrameLine{}, FrameLine{}, FrameLine{}, FrameLine{}, fmt.Errorf("faulty cost centre type found when splitting") + } + } + + for _, committeeFrameLine := range committeeFrameLines { + sumCommitteeFrameLine.FrameLineIncome += committeeFrameLine.FrameLineIncome + sumCommitteeFrameLine.FrameLineExpense += committeeFrameLine.FrameLineExpense + sumCommitteeFrameLine.FrameLineInternal += committeeFrameLine.FrameLineInternal + sumCommitteeFrameLine.FrameLineResult += committeeFrameLine.FrameLineResult + } + + for _, ProjectFrameLine := range projectFrameLines { + sumProjectFrameLine.FrameLineIncome += ProjectFrameLine.FrameLineIncome + sumProjectFrameLine.FrameLineExpense += ProjectFrameLine.FrameLineExpense + sumProjectFrameLine.FrameLineInternal += ProjectFrameLine.FrameLineInternal + sumProjectFrameLine.FrameLineResult += ProjectFrameLine.FrameLineResult + } + + for _, OtherFrameLine := range otherFrameLines { + sumOtherFrameLine.FrameLineIncome += OtherFrameLine.FrameLineIncome + sumOtherFrameLine.FrameLineExpense += OtherFrameLine.FrameLineExpense + sumOtherFrameLine.FrameLineInternal += OtherFrameLine.FrameLineInternal + sumOtherFrameLine.FrameLineResult += OtherFrameLine.FrameLineResult + } + + return committeeFrameLines, projectFrameLines, otherFrameLines, totalFrameLine, sumCommitteeFrameLine, sumProjectFrameLine, sumOtherFrameLine, nil +} diff --git a/internal/web/index.go b/internal/web/index.go new file mode 100644 index 0000000..1332816 --- /dev/null +++ b/internal/web/index.go @@ -0,0 +1,86 @@ +package web + +import ( + "database/sql" + "fmt" + "io" + "log/slog" + "net/http" + "strconv" + + "github.com/datasektionen/GOrdian/internal/excel" +) + +func indexPage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []string, loggedIn bool) error { + costCentres, err := getCostCentres(db) + if err != nil { + return fmt.Errorf("failed get scan cost centre information from database: %v", err) + } + committeeCostCentres, projectCostCentres, otherCostCentres, err := splitCostCentresOnType(costCentres) + if err != nil { + return fmt.Errorf("failed to split cost centres on type: %v", err) + } + + //Mörkläggning av mottagningens budget + darkeningResp, err := http.Get("https://darkmode.datasektionen.se/") + if err != nil { + slog.Error("Failed to get status from darkmode", "error", err) + return fmt.Errorf(": %v", err) + } + defer darkeningResp.Body.Close() + + if darkeningResp.StatusCode != http.StatusOK { + slog.Error("Status error from darkmode", "error", darkeningResp.StatusCode) + } + + darkeningBody, err := io.ReadAll(darkeningResp.Body) + if err != nil { + slog.Error("Failed to read body", "error", err) + } + + darkeningValue, err := strconv.ParseBool(string(darkeningBody)) + if err != nil { + slog.Error("Failed to parse bool", "error", err) + } + + if darkeningValue { + for index, committeeCostCentre := range committeeCostCentres { + if committeeCostCentre.CostCentreName == "Mottagningen" { + committeeCostCentres = append(committeeCostCentres[:index], committeeCostCentres[index+1:]...) + break + } + } + } + //end of mörkläggning + + if err := templates.ExecuteTemplate(w, "index.gohtml", map[string]any{ + "motd": motdGenerator(), + "committees": committeeCostCentres, + "projects": projectCostCentres, + "others": otherCostCentres, + "permissions": perms, + "loggedIn": loggedIn, + }); err != nil { + return fmt.Errorf("could not render template: %w", err) + } + return nil +} + +func splitCostCentresOnType(costCentres []excel.CostCentre) ([]excel.CostCentre, []excel.CostCentre, []excel.CostCentre, error) { + var committeeCostCentres []excel.CostCentre + var projectCostCentres []excel.CostCentre + var otherCostCentres []excel.CostCentre + for _, costCentre := range costCentres { + switch costCentre.CostCentreType { + case "committee": + committeeCostCentres = append(committeeCostCentres, costCentre) + case "project": + projectCostCentres = append(projectCostCentres, costCentre) + case "other": + otherCostCentres = append(otherCostCentres, costCentre) + default: + return nil, nil, nil, fmt.Errorf("faulty cost centre type found when splitting") + } + } + return committeeCostCentres, projectCostCentres, otherCostCentres, nil +} diff --git a/internal/web/queries.go b/internal/web/queries.go new file mode 100644 index 0000000..86d5595 --- /dev/null +++ b/internal/web/queries.go @@ -0,0 +1,182 @@ +package web + +var CombinedReportLinesGetStatementStatic = ` + SELECT + cost_centre, + secondary_cost_centre, + budget_line, + SUM(amount) AS total_amount + FROM ( + SELECT + ep.cost_centre, + ep.secondary_cost_centre, + ep.budget_line, + ep.amount, + EXTRACT(YEAR FROM e.expense_date)::text AS date, -- Cast to text + e.description + FROM expenses_expensepart AS ep + INNER JOIN expenses_expense AS e ON ep.expense_id = e.id + UNION ALL + SELECT + ip.cost_centre, + ip.secondary_cost_centre, + ip.budget_line, + ip.amount, + EXTRACT(YEAR FROM COALESCE(i.invoice_date, i.payed_at))::text AS date, -- Cast to text + i.description + FROM invoices_invoicepart AS ip + INNER JOIN invoices_invoice AS i ON ip.invoice_id = i.id + ) AS combined + WHERE (COALESCE($1, '') = '' OR $1 = 'Alla' OR date = $1) -- Filter by year or allow 'Alla' as wildcard + AND (COALESCE($2, '') = '' OR $2 = 'Alla' OR cost_centre::text = $2) -- Filter by cost_centre or allow 'Alla' as wildcard + GROUP BY cost_centre, secondary_cost_centre, budget_line + ORDER BY cost_centre, secondary_cost_centre, budget_line; +` + +var uniqueCCGetStatementStatic = ` + SELECT DISTINCT cost_centre + FROM ( + SELECT + ep.cost_centre + FROM expenses_expensepart AS ep + INNER JOIN expenses_expense AS e ON ep.expense_id = e.id + UNION + SELECT + ip.cost_centre + FROM invoices_invoicepart AS ip + INNER JOIN invoices_invoice AS i ON ip.invoice_id = i.id + ) AS combined + ORDER BY cost_centre; + ` + +// var ReportLinesByYearAllCCGetStatementStatic = ` +// SELECT +// cost_centre, +// secondary_cost_centre, +// budget_line, +// SUM(amount) AS total_amount +// FROM ( +// SELECT +// ep.cost_centre, +// ep.secondary_cost_centre, +// ep.budget_line, +// ep.amount, +// EXTRACT(YEAR FROM e.expense_date) AS date, +// e.description +// FROM expenses_expensepart AS ep +// INNER JOIN expenses_expense AS e ON ep.expense_id = e.id +// UNION ALL +// SELECT +// ip.cost_centre, +// ip.secondary_cost_centre, +// ip.budget_line, +// ip.amount, +// EXTRACT(YEAR FROM (COALESCE(i.invoice_date, i.payed_at))) AS invoice_date, +// i.description +// FROM invoices_invoicepart AS ip +// INNER JOIN invoices_invoice AS i ON ip.invoice_id = i.id +// ) AS combined +// WHERE COALESCE(date = $1, true) +// GROUP BY cost_centre, secondary_cost_centre, budget_line +// ORDER BY cost_centre, secondary_cost_centre, budget_line; +// ` + +// var ReportLinesAllYearAllCCGetStatementStatic = ` +// SELECT +// cost_centre, +// secondary_cost_centre, +// budget_line, +// SUM(amount) AS total_amount +// FROM ( +// SELECT +// ep.cost_centre, +// ep.secondary_cost_centre, +// ep.budget_line, +// ep.amount, +// EXTRACT(YEAR FROM e.expense_date) AS date, +// e.description +// FROM expenses_expensepart AS ep +// INNER JOIN expenses_expense AS e ON ep.expense_id = e.id +// UNION ALL +// SELECT +// ip.cost_centre, +// ip.secondary_cost_centre, +// ip.budget_line, +// ip.amount, +// EXTRACT(YEAR FROM (COALESCE(i.invoice_date, i.payed_at))) AS invoice_date, +// i.description +// FROM invoices_invoicepart AS ip +// INNER JOIN invoices_invoice AS i ON ip.invoice_id = i.id +// ) AS combined +// WHERE COALESCE(date = null, true) +// GROUP BY cost_centre, secondary_cost_centre, budget_line +// ORDER BY cost_centre, secondary_cost_centre, budget_line; +// ` + +// var ReportLinesAllYearByCCGetStatementStatic = ` +// SELECT +// cost_centre, +// secondary_cost_centre, +// budget_line, +// SUM(amount) AS total_amount +// FROM ( +// SELECT +// ep.cost_centre, +// ep.secondary_cost_centre, +// ep.budget_line, +// ep.amount, +// EXTRACT(YEAR FROM e.expense_date) AS date, +// e.description +// FROM expenses_expensepart AS ep +// INNER JOIN expenses_expense AS e ON ep.expense_id = e.id +// UNION ALL +// SELECT +// ip.cost_centre, +// ip.secondary_cost_centre, +// ip.budget_line, +// ip.amount, +// EXTRACT(YEAR FROM (COALESCE(i.invoice_date, i.payed_at))) AS invoice_date, +// i.description +// FROM invoices_invoicepart AS ip +// INNER JOIN invoices_invoice AS i ON ip.invoice_id = i.id +// ) AS combined +// WHERE COALESCE(date = null, true) +// AND COALESCE(cost_centre = $1, true) +// GROUP BY cost_centre, secondary_cost_centre, budget_line +// ORDER BY cost_centre, secondary_cost_centre, budget_line; +// ` + +// var ReportLinesByYearByCCGetStatementStatic = ` +// SELECT +// cost_centre, +// secondary_cost_centre, +// budget_line, +// SUM(amount) AS total_amount +// FROM ( +// SELECT +// ep.cost_centre, +// ep.secondary_cost_centre, +// ep.budget_line, +// ep.amount, +// EXTRACT(YEAR FROM e.expense_date) AS date, +// e.description +// FROM expenses_expensepart AS ep +// INNER JOIN expenses_expense AS e ON ep.expense_id = e.id +// UNION ALL +// SELECT +// ip.cost_centre, +// ip.secondary_cost_centre, +// ip.budget_line, +// ip.amount, +// EXTRACT(YEAR FROM (COALESCE(i.invoice_date, i.payed_at))) AS invoice_date, +// i.description +// FROM invoices_invoicepart AS ip +// INNER JOIN invoices_invoice AS i ON ip.invoice_id = i.id +// ) AS combined +// WHERE COALESCE(date = $1, true) +// AND COALESCE(cost_centre = $2, true) +// GROUP BY cost_centre, secondary_cost_centre, budget_line +// ORDER BY cost_centre, secondary_cost_centre, budget_line; +// ` + + diff --git a/internal/web/report.go b/internal/web/report.go new file mode 100644 index 0000000..ac169de --- /dev/null +++ b/internal/web/report.go @@ -0,0 +1,220 @@ +package web + +import ( + "database/sql" + "fmt" + "net/http" + "strconv" + "strings" + "time" +) + +type ReportLine struct { + ReportLineCostCentre string + ReportLineSecondaryCostCentre string + ReportLineBudgetLine string + ReportLineTotal string +} + +type ReportBudgetLine struct { + BudgetLineName string + Total string +} + +type ReportSecondaryCostCentreLine struct { + SecondaryCostCentreName string + BudgetLinesList []ReportBudgetLine + Total string +} + +type ReportCostCentreLine struct { + CostCentreName string + SecondaryCostCentresList []ReportSecondaryCostCentreLine + Total string +} + +func getYearsSince2017() []string { + startYear := 2017 + currentYear := time.Now().Year() + var years []string + + for year := startYear; year <= currentYear; year++ { + years = append(years, strconv.Itoa(year)) + } + + return years +} + +func reportPage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []string, loggedIn bool) error { + + currentYear := strconv.Itoa(time.Now().Year()) + + selectedYear := r.FormValue("year") + if selectedYear == "" { + selectedYear = currentYear + } + + CCList, err := getCCList(db) + if err != nil { + return fmt.Errorf("failed get scan CCList information from database: %v", err) + } + + reportLines, err := getReportLines(db, selectedYear, r.FormValue("cc")) + if err != nil { + return fmt.Errorf("failed to get scan report lines information from database: %v", err) + } + + structuredReport, err := StructureReportLines(reportLines) + if err != nil { + return fmt.Errorf("failed to structure report lines: %v", err) + } + years := getYearsSince2017() + + if err := templates.ExecuteTemplate(w, "report.gohtml", map[string]any{ + "motd": motdGenerator(), + "reportLines": reportLines, + "permissions": perms, + "loggedIn": loggedIn, + "report": structuredReport, + "CCList": CCList, + "years": years, + "SelectedCC": r.FormValue("cc"), + "SelectedYear": selectedYear, + }); err != nil { + return fmt.Errorf("could not render template: %w", err) + } + return nil +} + +func getCCList(db *sql.DB) ([]string, error) { + var result *sql.Rows + var err error + + result, err = db.Query(uniqueCCGetStatementStatic) + if err != nil { + return nil, fmt.Errorf("failed to get CCList from database: %v", err) + } + defer result.Close() + + var CCList []string + + for result.Next() { + var CC string + + err := result.Scan( + &CC, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan CC from query result: %v", err) + } + CCList = append(CCList, CC) + } + return CCList, nil +} + +func getReportLines(db *sql.DB, year string, cc string) ([]ReportLine, error) { + + var result *sql.Rows + var err error + + result, err = db.Query(CombinedReportLinesGetStatementStatic, year, cc) + if err != nil { + return nil, fmt.Errorf("failed to get reportlines from database: %v", err) + } + defer result.Close() + + var reportLines []ReportLine + + for result.Next() { + var reportLine ReportLine + + err := result.Scan( + &reportLine.ReportLineCostCentre, + &reportLine.ReportLineSecondaryCostCentre, + &reportLine.ReportLineBudgetLine, + &reportLine.ReportLineTotal, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan report line from query result: %v", err) + } + reportLines = append(reportLines, reportLine) + } + return reportLines, nil +} + +// Add cost centre or return existing +func findOrAddCostCentre(costCentres *[]ReportCostCentreLine, costCentreName string) *ReportCostCentreLine { + for i := range *costCentres { + if (*costCentres)[i].CostCentreName == costCentreName { + return &(*costCentres)[i] + } + } + // Add new cost centre if not found + newCostCentre := ReportCostCentreLine{ + CostCentreName: costCentreName, + SecondaryCostCentresList: []ReportSecondaryCostCentreLine{}, + Total: "0 kr", + } + *costCentres = append(*costCentres, newCostCentre) + return &(*costCentres)[len(*costCentres)-1] +} + +// Add secondary cost centre or return existing +func findOrAddSecondaryCostCentre(secCostCentres *[]ReportSecondaryCostCentreLine, secCostCentreName string) *ReportSecondaryCostCentreLine { + for i := range *secCostCentres { + if (*secCostCentres)[i].SecondaryCostCentreName == secCostCentreName { + return &(*secCostCentres)[i] + } + } + // Add new secondary cost centre if not found + newSecCostCentre := ReportSecondaryCostCentreLine{ + SecondaryCostCentreName: secCostCentreName, + BudgetLinesList: []ReportBudgetLine{}, + Total: "0 kr", + } + *secCostCentres = append(*secCostCentres, newSecCostCentre) + return &(*secCostCentres)[len(*secCostCentres)-1] +} + +// Function to organize ReportLines into structured data +func StructureReportLines(reportLines []ReportLine) ([]ReportCostCentreLine, error) { + var costCentres []ReportCostCentreLine + + // Loop through each ReportLine and structure it + for _, line := range reportLines { + // Find or add the CostCentre + costCentre := findOrAddCostCentre(&costCentres, line.ReportLineCostCentre) + + // Find or add the SecondaryCostCentre within the CostCentre + secCostCentre := findOrAddSecondaryCostCentre(&costCentre.SecondaryCostCentresList, line.ReportLineSecondaryCostCentre) + + // Add the BudgetLine to the SecondaryCostCentre + secCostCentre.BudgetLinesList = append(secCostCentre.BudgetLinesList, ReportBudgetLine{ + BudgetLineName: line.ReportLineBudgetLine, + Total: line.ReportLineTotal, + }) + var err1, err2 error + + // Update totals for SecondaryCostCentre and CostCentre + secCostCentre.Total, err1 = addTotals(secCostCentre.Total, line.ReportLineTotal) + costCentre.Total, err2 = addTotals(costCentre.Total, line.ReportLineTotal) + + if err1 != nil || err2 != nil { + return nil, fmt.Errorf("failed to update totals for SCC or CC: %v%v", err1, err2) + } + } + + return costCentres, nil +} + +// Helper function to add totals (handling currency and decimal values) +func addTotals(total1, total2 string) (string, error) { + t1, err1 := strconv.ParseFloat(strings.ReplaceAll(total1, " kr", ""), 64) + t2, err2 := strconv.ParseFloat(strings.ReplaceAll(total2, " kr", ""), 64) + + if err1 != nil || err2 != nil { + return "0 kr", fmt.Errorf("failed operate on total float: %v%v", err1, err2) + } + + return fmt.Sprintf("%.2f kr", t1+t2), nil +} diff --git a/internal/web/static/budgets/budget_2024_upd.xlsx b/internal/web/static/budgets/budget_2024_upd.xlsx new file mode 100644 index 0000000..fd06fe7 Binary files /dev/null and b/internal/web/static/budgets/budget_2024_upd.xlsx differ diff --git a/internal/web/templates/fippel.gohtml b/internal/web/templates/fippel.gohtml new file mode 100644 index 0000000..221697b --- /dev/null +++ b/internal/web/templates/fippel.gohtml @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/internal/web/templates/frame.gohtml b/internal/web/templates/frame.gohtml index d6e0117..462c18b 100644 --- a/internal/web/templates/frame.gohtml +++ b/internal/web/templates/frame.gohtml @@ -24,21 +24,21 @@ {{range $_, $committeeframeline := .committeeframelines}} - - {{$committeeframeline.FrameLineName}} - - {{formatMoney $committeeframeline.FrameLineIncome}} kr - - - {{formatMoney $committeeframeline.FrameLineExpense}} kr - - - {{formatMoney $committeeframeline.FrameLineInternal}} kr - - - {{formatMoney $committeeframeline.FrameLineResult}} kr - - + + {{$committeeframeline.FrameLineName}} + + {{formatMoney $committeeframeline.FrameLineIncome}} kr + + + {{formatMoney $committeeframeline.FrameLineExpense}} kr + + + {{formatMoney $committeeframeline.FrameLineInternal}} kr + + + {{formatMoney $committeeframeline.FrameLineResult}} kr + + {{end}} Projekt diff --git a/internal/web/templates/head.gohtml b/internal/web/templates/head.gohtml index 81ff332..97f7b3f 100644 --- a/internal/web/templates/head.gohtml +++ b/internal/web/templates/head.gohtml @@ -33,6 +33,10 @@ str: "Rambudget", href: "/framebudget", }, + { + str: "Resultatrapport", + href: "/resultreport", + }, {{if sliceContains .permissions "admin" "view-all" }} { str: "Administrera", href: "/admin", diff --git a/internal/web/templates/report.gohtml b/internal/web/templates/report.gohtml new file mode 100644 index 0000000..57f343f --- /dev/null +++ b/internal/web/templates/report.gohtml @@ -0,0 +1,85 @@ + + +{{template "head.gohtml" .}} + +
+
+

Resultatrapport

+
+
+

Vad har spenderats på min budgetpost?

+
+
+OBS! Räknar endast med kvitton och fakturor i Cashflow +
+
+ +
+

Nedan kan du välja en nämnd eller ett projekt som du vill se

+
+
+

Du kan även välja ett specifikt år för att se vad som spenderades då

+
+
+ +
+ + +
+ + +
+
+ + {{range $_, $ReportCostCentreLine := .report }} + + + + + {{range $_, $ReportSecondaryCostCentreLine := $ReportCostCentreLine.SecondaryCostCentresList}} + + + + + {{range $_, $ReportBudgetLine := $ReportSecondaryCostCentreLine.BudgetLinesList}} + + + + + {{end}} + + {{end}} + + {{end}} +
{{$ReportCostCentreLine.CostCentreName}} + {{$ReportCostCentreLine.Total}} +
+ {{$ReportSecondaryCostCentreLine.SecondaryCostCentreName}} + + {{$ReportSecondaryCostCentreLine.Total}} +
+ {{$ReportBudgetLine.BudgetLineName}} + + {{$ReportBudgetLine.Total}} kr +
+
+
+






+ + + \ No newline at end of file diff --git a/internal/web/urls.go b/internal/web/urls.go index f422a06..ce639bd 100644 --- a/internal/web/urls.go +++ b/internal/web/urls.go @@ -5,23 +5,19 @@ import ( "embed" "encoding/json" "fmt" - "github.com/datasektionen/GOrdian/internal/config" - "github.com/datasektionen/GOrdian/internal/database" - "github.com/datasektionen/GOrdian/internal/excel" "html/template" - "io" "log/slog" "math/rand" "net/http" "strconv" + + "github.com/datasektionen/GOrdian/internal/config" + "github.com/datasektionen/GOrdian/internal/database" ) -type FrameLine struct { - FrameLineName string - FrameLineIncome int - FrameLineExpense int - FrameLineInternal int - FrameLineResult int +type Databases struct { + DBCF *sql.DB + DBGO *sql.DB } const ( @@ -36,7 +32,7 @@ var staticFiles embed.FS var templates *template.Template -func Mount(mux *http.ServeMux, db *sql.DB) error { +func Mount(mux *http.ServeMux, databases Databases) error { var err error tokenURL := config.GetEnv().LoginURL + "/login?callback=" + config.GetEnv().ServerURL + "/token?token=" templates, err = template.New("").Funcs(map[string]any{"formatMoney": formatMoney, "add": add, "sliceContains": sliceContains}).ParseFS(templatesFS, "templates/*.gohtml") @@ -44,17 +40,18 @@ func Mount(mux *http.ServeMux, db *sql.DB) error { return err } mux.Handle("/static/", http.FileServerFS(staticFiles)) - mux.Handle("/{$}", authRoute(db, indexPage, []string{})) - mux.Handle("/costcentre/{costCentreIDPath}", authRoute(db, costCentrePage, []string{})) + mux.Handle("/{$}", authRoute(databases.DBGO, indexPage, []string{})) + mux.Handle("/costcentre/{costCentreIDPath}", authRoute(databases.DBGO, costCentrePage, []string{})) mux.Handle("/login", http.RedirectHandler(tokenURL, http.StatusSeeOther)) - mux.Handle("/token", route(db, tokenPage)) - mux.Handle("/logout", route(db, logoutPage)) - mux.Handle("/admin", authRoute(db, adminPage, []string{"admin", "view-all"})) - mux.Handle("/admin/upload", authRoute(db, uploadPage, []string{"admin"})) - mux.Handle("/api/CostCentres", cors(route(db, apiCostCentres))) - mux.Handle("/api/SecondaryCostCentres", cors(route(db, apiSecondaryCostCentre))) - mux.Handle("/api/BudgetLines", cors(route(db, apiBudgetLine))) - mux.Handle("/framebudget", authRoute(db, framePage, []string{})) + mux.Handle("/token", route(databases.DBGO, tokenPage)) + mux.Handle("/logout", route(databases.DBGO, logoutPage)) + mux.Handle("/admin", authRoute(databases.DBGO, adminPage, []string{"admin", "view-all"})) + mux.Handle("/admin/upload", authRoute(databases.DBGO, uploadPage, []string{"admin"})) + mux.Handle("/api/CostCentres", cors(route(databases.DBGO, apiCostCentres))) + mux.Handle("/api/SecondaryCostCentres", cors(route(databases.DBGO, apiSecondaryCostCentre))) + mux.Handle("/api/BudgetLines", cors(route(databases.DBGO, apiBudgetLine))) + mux.Handle("/framebudget", authRoute(databases.DBGO, framePage, []string{})) + mux.Handle("/resultreport", authRoute(databases.DBCF, reportPage, []string{})) return nil } @@ -66,25 +63,6 @@ func cors(h http.Handler) http.Handler { }) } -func add(x int, y int) int { - return x + y -} - -func formatMoney(value int) string { - numStr := strconv.Itoa(value) - length := len(numStr) - var result string - - for i := 0; i < length; i++ { - if i > 0 && (length-i)%3 == 0 { - result += " " - } - result += string(numStr[i]) - } - - return result -} - func route(db *sql.DB, handler func(w http.ResponseWriter, r *http.Request, db *sql.DB) error) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := handler(w, r, db) @@ -124,6 +102,9 @@ func authRoute(db *sql.DB, handler func(w http.ResponseWriter, r *http.Request, return fmt.Errorf("failed to decode user body from json: %v", err) } userPerms, err := http.Get(config.GetEnv().PlsURL + "/api/user/" + loginBody.User + "/" + config.GetEnv().PlsSystem) + if err != nil { + return fmt.Errorf("no response from pls: %v", err) + } var perms []string err = json.NewDecoder(userPerms.Body).Decode(&perms) @@ -154,45 +135,23 @@ func sliceContains(list1 []string, list2 ...string) bool { return false } -func apiCostCentres(w http.ResponseWriter, r *http.Request, db *sql.DB) error { - costCentres, err := getCostCentres(db) - if err != nil { - return fmt.Errorf("failed get scan cost centres information from database: %v", err) - } - err = json.NewEncoder(w).Encode(costCentres) - if err != nil { - return fmt.Errorf("failed to encode cost centres to json: %v", err) - } - return nil +func add(x int, y int) int { + return x + y } -func apiSecondaryCostCentre(w http.ResponseWriter, r *http.Request, db *sql.DB) error { - idCC, err := strconv.Atoi(r.FormValue("id")) - if err != nil { - return fmt.Errorf("failed to convert secondary cost centre id to int: %v", err) - } - secondaryCostCentres, err := getSecondaryCostCentresByCostCentreID(db, idCC) - if err != nil { - return fmt.Errorf("failed get scan sendondary cost centres information from database: %v", err) - } - err = json.NewEncoder(w).Encode(secondaryCostCentres) - if err != nil { - return fmt.Errorf("failed to encode secondary cost centres to json: %v", err) - } - return nil -} +func formatMoney(value int) string { + numStr := strconv.Itoa(value) + length := len(numStr) + var result string -func apiBudgetLine(w http.ResponseWriter, r *http.Request, db *sql.DB) error { - idSCC, err := strconv.Atoi(r.FormValue("id")) - budgetLines, err := getBudgetLinesBySecondaryCostCentreID(db, idSCC) - if err != nil { - return fmt.Errorf("failed get scan budget lines information from database: %v", err) - } - err = json.NewEncoder(w).Encode(budgetLines) - if err != nil { - return fmt.Errorf("failed to encode budget lines to json: %v", err) + for i := 0; i < length; i++ { + if i > 0 && (length-i)%3 == 0 { + result += " " + } + result += string(numStr[i]) } - return nil + + return result } func adminPage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []string, loggedIn bool) error { @@ -211,7 +170,7 @@ func uploadPage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []stri if err != nil { return fmt.Errorf("could not read file from form: %w", err) } - err = database.SaveBudget(file) + err = database.SaveBudget(file, db) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } @@ -234,461 +193,6 @@ func logoutPage(w http.ResponseWriter, r *http.Request, db *sql.DB) error { return nil } -func indexPage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []string, loggedIn bool) error { - costCentres, err := getCostCentres(db) - if err != nil { - return fmt.Errorf("failed get scan cost centre information from database: %v", err) - } - committeeCostCentres, projectCostCentres, otherCostCentres, err := splitCostCentresOnType(costCentres) - if err != nil { - return fmt.Errorf("failed to split cost centres on type: %v", err) - } - - //Mörkläggning av mottagningens budget - darkeningResp, err := http.Get("https://darkmode.datasektionen.se/") - if err != nil { - slog.Error("Failed to get status from darkmode", "error", err) - return fmt.Errorf(": %v", err) - } - defer darkeningResp.Body.Close() - - if darkeningResp.StatusCode != http.StatusOK { - slog.Error("Status error from darkmode", "error", darkeningResp.StatusCode) - } - - darkeningBody, err := io.ReadAll(darkeningResp.Body) - if err != nil { - slog.Error("Failed to read body", "error", err) - } - - darkeningValue, err := strconv.ParseBool(string(darkeningBody)) - if err != nil { - slog.Error("Failed to parse bool", "error", err) - } - - if darkeningValue { - for index, committeeCostCentre := range committeeCostCentres { - if committeeCostCentre.CostCentreName == "Mottagningen" { - committeeCostCentres = append(committeeCostCentres[:index], committeeCostCentres[index+1:]...) - break - } - } - } - //end of mörkläggning - - if err := templates.ExecuteTemplate(w, "index.gohtml", map[string]any{ - "motd": motdGenerator(), - "committees": committeeCostCentres, - "projects": projectCostCentres, - "others": otherCostCentres, - "permissions": perms, - "loggedIn": loggedIn, - }); err != nil { - return fmt.Errorf("Could not render template: %w", err) - } - return nil -} - -func framePage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []string, loggedIn bool) error { - budgetLines, err := getFrameLines(db) - if err != nil { - return fmt.Errorf("failed get scan budget lines information from database: %v", err) - } - committeeFrameLines, projectFrameLines, otherFrameLines, totalFrameLine, sumCommitteeFrameLine, sumProjectFrameLine, sumOtherFrameLine, err := generateFrameLines(budgetLines) - if err != nil { - return fmt.Errorf("failed to generate frame budget lines: %v", err) - } - if err := templates.ExecuteTemplate(w, "frame.gohtml", map[string]any{ - "motd": motdGenerator(), - "committeeframelines": committeeFrameLines, - "projectframelines": projectFrameLines, - "otherframelines": otherFrameLines, - "totalframeline": totalFrameLine, - "sumcommitteeframeline": sumCommitteeFrameLine, - "sumprojectframeline": sumProjectFrameLine, - "sumotherframeline": sumOtherFrameLine, - "permissions": perms, - "loggedIn": loggedIn, - }); err != nil { - return fmt.Errorf("Could not render template: %w", err) - } - return nil -} - -func costCentrePage(w http.ResponseWriter, r *http.Request, db *sql.DB, perms []string, loggedIn bool) error { - costCentreIDString := r.PathValue("costCentreIDPath") - costCentreIDInt, err := strconv.Atoi(costCentreIDString) - if err != nil { - return fmt.Errorf("failed to convert cost centre id from string to int: %v", err) - } - - budgetLines, err := getBudgetLinesByCostCentreID(db, costCentreIDInt) - if err != nil { - return fmt.Errorf("failed get scan budget line information from database: %v", err) - } - - //omg - secondaryCostCentresWithBudgetLinesList := make([]secondaryCostCentresWithBudgetLines, 1) - currentSecondaryCostCentre := &secondaryCostCentresWithBudgetLinesList[0] - for _, budgetLine := range budgetLines { - if currentSecondaryCostCentre.SecondaryCostCentreName != budgetLine.SecondaryCostCentreName { - secondaryCostCentresWithBudgetLinesList = append(secondaryCostCentresWithBudgetLinesList, secondaryCostCentresWithBudgetLines{ - SecondaryCostCentreName: budgetLine.SecondaryCostCentreName, - BudgetLines: []excel.BudgetLine{}, - }) - currentSecondaryCostCentre = &secondaryCostCentresWithBudgetLinesList[len(secondaryCostCentresWithBudgetLinesList)-1] - } - currentSecondaryCostCentre.BudgetLines = append(currentSecondaryCostCentre.BudgetLines, budgetLine) - } - secondaryCostCentresWithBudgetLinesList = secondaryCostCentresWithBudgetLinesList[1:] - - costCentre, err := getCostCentreByID(db, costCentreIDInt) - if err != nil { - return fmt.Errorf("failed get scan cost centre information from database: %v", err) - } - - //calc the total incomes, expenses and results of all cost centres in the list - secondaryCostCentresWithBudgetLinesList, err = calculateSecondaryCostCentres(secondaryCostCentresWithBudgetLinesList) - if err != nil { - return fmt.Errorf("failed calculate secondary cost centre values: %v", err) - } - - costCentreTotalIncome, costCentreTotalExpense, costCentreTotalResult, err := calculateCostCentre(secondaryCostCentresWithBudgetLinesList) - if err != nil { - return fmt.Errorf("failed calculate cost centre values: %v", err) - } - - if err := templates.ExecuteTemplate(w, "costcentre.gohtml", map[string]any{ - "motd": motdGenerator(), - "secondaryCostCentresWithBudgetLinesList": secondaryCostCentresWithBudgetLinesList, - "costCentre": costCentre, - "costCentreTotalIncome": costCentreTotalIncome, - "costCentreTotalExpense": costCentreTotalExpense, - "costCentreTotalResult": costCentreTotalResult, - "permissions": perms, - "loggedIn": loggedIn, - }); err != nil { - return fmt.Errorf("could not render template: %w", err) - } - return nil -} - -func calculateCostCentre(secondaryCostCentresWithBudgetLinesList []secondaryCostCentresWithBudgetLines) (int, int, int, error) { - var totalIncome int - var totalExpense int - for _, sCCWithBudgetLines := range secondaryCostCentresWithBudgetLinesList { - totalIncome = totalIncome + sCCWithBudgetLines.SecondaryCostCentreTotalIncome - totalExpense = totalExpense + sCCWithBudgetLines.SecondaryCostCentreTotalExpense - } - totalResult := totalIncome + totalExpense - - return totalIncome, totalExpense, totalResult, nil -} - -func calculateSecondaryCostCentres(secondaryCostCentresWithBudgetLinesList []secondaryCostCentresWithBudgetLines) ([]secondaryCostCentresWithBudgetLines, error) { - for index, sCCWithBudgetLines := range secondaryCostCentresWithBudgetLinesList { - var totalIncome int - var totalExpense int - for _, budgetLine := range sCCWithBudgetLines.BudgetLines { - totalIncome = totalIncome + budgetLine.BudgetLineIncome - totalExpense = totalExpense + budgetLine.BudgetLineExpense - } - secondaryCostCentresWithBudgetLinesList[index].SecondaryCostCentreTotalIncome = totalIncome - secondaryCostCentresWithBudgetLinesList[index].SecondaryCostCentreTotalExpense = totalExpense - secondaryCostCentresWithBudgetLinesList[index].SecondaryCostCentreTotalResult = totalIncome + totalExpense - } - return secondaryCostCentresWithBudgetLinesList, nil -} - -type secondaryCostCentresWithBudgetLines struct { - SecondaryCostCentreName string - SecondaryCostCentreTotalIncome int - SecondaryCostCentreTotalExpense int - SecondaryCostCentreTotalResult int - BudgetLines []excel.BudgetLine -} - -func getBudgetLinesByCostCentreID(db *sql.DB, costCentreID int) ([]excel.BudgetLine, error) { - var budgetLinesGetStatementStatic = ` - SELECT - budget_lines.id, - budget_lines.name, - income, - expense, - comment, - account, - secondary_cost_centres.id, - secondary_cost_centres.name - FROM budget_lines - JOIN secondary_cost_centres ON secondary_cost_centres.id = secondary_cost_centre_id - WHERE cost_centre_id = $1 - ORDER BY secondary_cost_centre_id - ` - result, err := db.Query(budgetLinesGetStatementStatic, costCentreID) - if err != nil { - return nil, fmt.Errorf("failed to get budget lines from database: %v", err) - } - var budgetLines []excel.BudgetLine - for result.Next() { - var budgetLine excel.BudgetLine - - err := result.Scan( - &budgetLine.BudgetLineID, - &budgetLine.BudgetLineName, - &budgetLine.BudgetLineIncome, - &budgetLine.BudgetLineExpense, - &budgetLine.BudgetLineComment, - &budgetLine.BudgetLineAccount, - &budgetLine.SecondaryCostCentreID, - &budgetLine.SecondaryCostCentreName, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan budget line from query result: %v", err) - } - budgetLines = append(budgetLines, budgetLine) - } - return budgetLines, nil -} - -func getSecondaryCostCentresByCostCentreID(db *sql.DB, costCentreID int) ([]excel.SecondaryCostCentre, error) { - var SecondaryCostCentresGetStatementStatic = ` - SELECT - id, - name, - cost_centre_id - FROM secondary_cost_centres - WHERE cost_centre_id = $1 - ORDER BY id - ` - result, err := db.Query(SecondaryCostCentresGetStatementStatic, costCentreID) - if err != nil { - return nil, fmt.Errorf("failed to get secondary cost centres from database: %v", err) - } - var secondaryCostCentres []excel.SecondaryCostCentre - for result.Next() { - var secondaryCostCentre excel.SecondaryCostCentre - - err := result.Scan( - &secondaryCostCentre.SecondaryCostCentreID, - &secondaryCostCentre.SecondaryCostCentreName, - &secondaryCostCentre.CostCentreID, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan secondary cost centre from query result: %v", err) - } - secondaryCostCentres = append(secondaryCostCentres, secondaryCostCentre) - } - return secondaryCostCentres, nil -} - -func getBudgetLinesBySecondaryCostCentreID(db *sql.DB, secondaryCostCentreID int) ([]excel.BudgetLine, error) { - var budgetLinesGetStatementStatic = ` - SELECT - id, - name, - income, - expense, - comment, - account, - secondary_cost_centre_id - FROM budget_lines - WHERE secondary_cost_centre_id = $1 - ORDER BY id - ` - result, err := db.Query(budgetLinesGetStatementStatic, secondaryCostCentreID) - if err != nil { - return nil, fmt.Errorf("failed to get budgetlines from database: %v", err) - } - var budgetLines []excel.BudgetLine - for result.Next() { - var budgetLine excel.BudgetLine - - err := result.Scan( - &budgetLine.BudgetLineID, - &budgetLine.BudgetLineName, - &budgetLine.BudgetLineIncome, - &budgetLine.BudgetLineExpense, - &budgetLine.BudgetLineComment, - &budgetLine.BudgetLineAccount, - &budgetLine.SecondaryCostCentreID, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan budget line from query result: %v", err) - } - budgetLines = append(budgetLines, budgetLine) - } - return budgetLines, nil -} - -func getFrameLines(db *sql.DB) ([]excel.BudgetLine, error) { - var frameLinesGetStatementStatic = ` - SELECT - SUM(income), - SUM(expense), - secondary_cost_centres.name ILIKE '%Internt%', - cost_centres.id, - cost_centres.name, - cost_centres.type - FROM budget_lines - JOIN secondary_cost_centres ON secondary_cost_centres.id = secondary_cost_centre_id - JOIN cost_centres ON secondary_cost_centres.cost_centre_id = cost_centres.id - GROUP BY cost_centres.id, cost_centres.name, cost_centres.type, secondary_cost_centres.name ILIKE '%Internt%' - ORDER BY cost_centres.name, secondary_cost_centres.name ILIKE '%Internt%' - ` - result, err := db.Query(frameLinesGetStatementStatic) - if err != nil { - return nil, fmt.Errorf("failed to get framelines from database: %v", err) - } - var frameLines []excel.BudgetLine - for result.Next() { - var frameLine excel.BudgetLine - - err := result.Scan( - &frameLine.BudgetLineIncome, - &frameLine.BudgetLineExpense, - &frameLine.SecondaryCostCentreName, - &frameLine.CostCentreID, - &frameLine.CostCentreName, - &frameLine.CostCentreType, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan budget line from query result: %v", err) - } - frameLines = append(frameLines, frameLine) - } - return frameLines, nil -} - -func getCostCentres(db *sql.DB) ([]excel.CostCentre, error) { - var costCentresGetStatementStatic = `SELECT id, name, type FROM cost_centres ORDER BY name` - result, err := db.Query(costCentresGetStatementStatic) - if err != nil { - return nil, fmt.Errorf("failed to get cost centres from database: %v", err) - } - var costCentres []excel.CostCentre - for result.Next() { - var costCentre excel.CostCentre - - err := result.Scan(&costCentre.CostCentreID, &costCentre.CostCentreName, &costCentre.CostCentreType) - if err != nil { - return nil, fmt.Errorf("failed to scan cost centre from query result: %v", err) - } - costCentres = append(costCentres, costCentre) - } - return costCentres, nil -} - -func getCostCentreByID(db *sql.DB, costCentreID int) (excel.CostCentre, error) { - var costCentreGetStatementStatic = `SELECT id, name, type FROM cost_centres WHERE id = $1` - result := db.QueryRow(costCentreGetStatementStatic, costCentreID) - var costCentre excel.CostCentre - err := result.Scan(&costCentre.CostCentreID, &costCentre.CostCentreName, &costCentre.CostCentreType) - if err != nil { - return excel.CostCentre{}, fmt.Errorf("failed to scan cost centre from query result: %v", err) - } - return costCentre, nil -} - -func splitCostCentresOnType(costCentres []excel.CostCentre) ([]excel.CostCentre, []excel.CostCentre, []excel.CostCentre, error) { - var committeeCostCentres []excel.CostCentre - var projectCostCentres []excel.CostCentre - var otherCostCentres []excel.CostCentre - for _, costCentre := range costCentres { - switch costCentre.CostCentreType { - case "committee": - committeeCostCentres = append(committeeCostCentres, costCentre) - case "project": - projectCostCentres = append(projectCostCentres, costCentre) - case "other": - otherCostCentres = append(otherCostCentres, costCentre) - default: - return nil, nil, nil, fmt.Errorf("faulty cost centre type found when splitting") - } - } - return committeeCostCentres, projectCostCentres, otherCostCentres, nil -} - -func generateFrameLines(frameLines []excel.BudgetLine) ([]FrameLine, []FrameLine, []FrameLine, FrameLine, FrameLine, FrameLine, FrameLine, error) { - var committeeFrameLines []FrameLine - var projectFrameLines []FrameLine - var otherFrameLines []FrameLine - var totalFrameLine FrameLine - var sumCommitteeFrameLine FrameLine - var sumProjectFrameLine FrameLine - var sumOtherFrameLine FrameLine - - totalFrameLine.FrameLineName = "Totalt" - sumCommitteeFrameLine.FrameLineName = "Summa nämnder" - sumProjectFrameLine.FrameLineName = "Summa projekt" - sumOtherFrameLine.FrameLineName = "Summa övrigt" - - var skippidi bool - for i, frameLine := range frameLines { - if skippidi == true { - skippidi = false - continue - } - frameLineIncome := frameLine.BudgetLineIncome - frameLineExpense := frameLine.BudgetLineExpense - frameLineName := frameLine.CostCentreName - frameLineInternal := 0 - frameLineResult := 0 - - // each CC appears twice, once for internal costs, once for the rest - // both are handled i and i+1 - // skippidi makes sure that the loop incements by two - if i+1 < len(frameLines) && frameLines[i+1].CostCentreName == frameLineName { - frameLineIncome += frameLines[i+1].BudgetLineIncome - frameLineExpense += frameLines[i+1].BudgetLineExpense - frameLineInternal = frameLines[i+1].BudgetLineIncome + frameLines[i+1].BudgetLineExpense - skippidi = true - } - - frameLineResult = frameLineIncome + frameLineExpense - - reconstructedFrameLine := FrameLine{frameLineName, frameLineIncome, frameLineExpense, frameLineInternal, frameLineResult} - - totalFrameLine.FrameLineIncome += frameLineIncome - totalFrameLine.FrameLineExpense += frameLineExpense - totalFrameLine.FrameLineInternal += frameLineInternal - totalFrameLine.FrameLineResult += frameLineResult - - switch frameLine.CostCentreType { - case "committee": - committeeFrameLines = append(committeeFrameLines, reconstructedFrameLine) - case "project": - projectFrameLines = append(projectFrameLines, reconstructedFrameLine) - case "other": - otherFrameLines = append(otherFrameLines, reconstructedFrameLine) - default: - return nil, nil, nil, FrameLine{}, FrameLine{}, FrameLine{}, FrameLine{}, fmt.Errorf("faulty cost centre type found when splitting") - } - } - - for _, committeeFrameLine := range committeeFrameLines { - sumCommitteeFrameLine.FrameLineIncome += committeeFrameLine.FrameLineIncome - sumCommitteeFrameLine.FrameLineExpense += committeeFrameLine.FrameLineExpense - sumCommitteeFrameLine.FrameLineInternal += committeeFrameLine.FrameLineInternal - sumCommitteeFrameLine.FrameLineResult += committeeFrameLine.FrameLineResult - } - - for _, ProjectFrameLine := range projectFrameLines { - sumProjectFrameLine.FrameLineIncome += ProjectFrameLine.FrameLineIncome - sumProjectFrameLine.FrameLineExpense += ProjectFrameLine.FrameLineExpense - sumProjectFrameLine.FrameLineInternal += ProjectFrameLine.FrameLineInternal - sumProjectFrameLine.FrameLineResult += ProjectFrameLine.FrameLineResult - } - - for _, OtherFrameLine := range otherFrameLines { - sumOtherFrameLine.FrameLineIncome += OtherFrameLine.FrameLineIncome - sumOtherFrameLine.FrameLineExpense += OtherFrameLine.FrameLineExpense - sumOtherFrameLine.FrameLineInternal += OtherFrameLine.FrameLineInternal - sumOtherFrameLine.FrameLineResult += OtherFrameLine.FrameLineResult - } - - return committeeFrameLines, projectFrameLines, otherFrameLines, totalFrameLine, sumCommitteeFrameLine, sumProjectFrameLine, sumOtherFrameLine, nil -} - func motdGenerator() string { options := []string{ "You have very many money:",