diff --git a/apps/finance/services/api/main/handler.go b/apps/finance/services/api/main/handler.go index 863ba40..2820ed1 100644 --- a/apps/finance/services/api/main/handler.go +++ b/apps/finance/services/api/main/handler.go @@ -151,6 +151,7 @@ func parseTmpl(files ...string) *template.Template { } var ( + homepageTmpl = parseTmpl("templates/homepage.html") baseTmpl = parseTmpl("templates/base.html") dashboardTmpl = parseTmpl("templates/base.html", "templates/dashboard.html") txnsTmpl = parseTmpl("templates/base.html", "templates/transactions.html") @@ -170,22 +171,23 @@ var ( peopleTmpl = parseTmpl("templates/base.html", "templates/people.html") settingsTmpl = parseTmpl("templates/base.html", "templates/settings.html") - // Org - orgListTmpl = parseTmpl("templates/base.html", "templates/org_list.html") - orgCreateTmpl = parseTmpl("templates/base.html", "templates/org_create.html") - orgHomeTmpl = parseTmpl("templates/base.html", "templates/org_home.html") - orgTeamsTmpl = parseTmpl("templates/base.html", "templates/org_teams.html") - orgMembersTmpl = parseTmpl("templates/base.html", "templates/org_members.html") - orgInviteTmpl = parseTmpl("templates/base.html", "templates/org_invite.html") - orgJoinTmpl = parseTmpl("templates/base.html", "templates/org_join.html") - orgEventsTmpl = parseTmpl("templates/base.html", "templates/org_events.html") - orgEventDetailTmpl = parseTmpl("templates/base.html", "templates/org_event_detail.html") - orgRequestsTmpl = parseTmpl("templates/base.html", "templates/org_requests.html") - orgRequestDetailTmpl = parseTmpl("templates/base.html", "templates/org_request_detail.html") - orgLedgerTmpl = parseTmpl("templates/base.html", "templates/org_ledger.html") - orgBankImportTmpl = parseTmpl("templates/base.html", "templates/org_bank_import.html") - orgAnalysisTmpl = parseTmpl("templates/base.html", "templates/org_analysis.html") - orgReportTmpl = parseTmpl("templates/base.html", "templates/org_report.html") + // Org — list/create/join stay on personal base; inner org pages use business base + orgListTmpl = parseTmpl("templates/base.html", "templates/org_list.html") + orgCreateTmpl = parseTmpl("templates/base.html", "templates/org_create.html") + orgJoinTmpl = parseTmpl("templates/base.html", "templates/org_join.html") + + orgHomeTmpl = parseTmpl("templates/base_org.html", "templates/org_home.html") + orgTeamsTmpl = parseTmpl("templates/base_org.html", "templates/org_teams.html") + orgMembersTmpl = parseTmpl("templates/base_org.html", "templates/org_members.html") + orgInviteTmpl = parseTmpl("templates/base_org.html", "templates/org_invite.html") + orgEventsTmpl = parseTmpl("templates/base_org.html", "templates/org_events.html") + orgEventDetailTmpl = parseTmpl("templates/base_org.html", "templates/org_event_detail.html") + orgRequestsTmpl = parseTmpl("templates/base_org.html", "templates/org_requests.html") + orgRequestDetailTmpl = parseTmpl("templates/base_org.html", "templates/org_request_detail.html") + orgLedgerTmpl = parseTmpl("templates/base_org.html", "templates/org_ledger.html") + orgBankImportTmpl = parseTmpl("templates/base_org.html", "templates/org_bank_import.html") + orgAnalysisTmpl = parseTmpl("templates/base_org.html", "templates/org_analysis.html") + orgReportTmpl = parseTmpl("templates/base_org.html", "templates/org_report.html") ) type authInfo struct { @@ -218,6 +220,20 @@ func render(w http.ResponseWriter, tmpl *template.Template, data interface{}) { } } +func renderOrg(w http.ResponseWriter, tmpl *template.Template, data interface{}) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.ExecuteTemplate(w, "base_org.html", data); err != nil { + slog.Error("template error", "err", err) + } +} + +func renderRaw(w http.ResponseWriter, tmpl *template.Template, data interface{}) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.Execute(w, data); err != nil { + slog.Error("template error", "err", err) + } +} + type storeIface interface { getAccounts(ctx context.Context, userID string) ([]Account, error) getAccount(ctx context.Context, id string) (*Account, error) @@ -354,6 +370,13 @@ func (h *Handler) ownerOrViewerMW(next http.HandlerFunc) http.HandlerFunc { }) } +func (h *Handler) Homepage(w http.ResponseWriter, r *http.Request) { + a := getAuth(r) + renderRaw(w, homepageTmpl, map[string]interface{}{ + "Email": a.Email, + }) +} + func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { ctx := r.Context() a := getAuth(r) @@ -2551,7 +2574,8 @@ func (h *Handler) Settings(w http.ResponseWriter, r *http.Request) { } func (h *Handler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET /{$}", h.Dashboard) + mux.HandleFunc("GET /{$}", h.Homepage) + mux.HandleFunc("GET /dashboard", h.authMW(h.Dashboard)) mux.HandleFunc("GET /transactions", h.Transactions) mux.HandleFunc("GET /import", h.ImportPage) mux.HandleFunc("POST /import/preview", h.ImportPreview) diff --git a/apps/finance/services/api/main/handler_org.go b/apps/finance/services/api/main/handler_org.go index eb71fe2..b3ba3b4 100644 --- a/apps/finance/services/api/main/handler_org.go +++ b/apps/finance/services/api/main/handler_org.go @@ -180,11 +180,11 @@ func (h *Handler) OrgHome(w http.ResponseWriter, r *http.Request) { } } - render(w, orgHomeTmpl, &OrgHomeData{ + d := &OrgHomeData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: org.Name, - Route: "orgs", + Route: "org-home", Org: *org, MyRole: me.Role, MyTeamIDs: me.TeamIDs, @@ -192,7 +192,11 @@ func (h *Handler) OrgHome(w http.ResponseWriter, r *http.Request) { ActiveYear: active, Teams: teams, Members: members, - }) + } + if active != nil { + d.FiscalYear = *active + } + renderOrg(w, orgHomeTmpl, d) })(w, r) } @@ -204,11 +208,11 @@ func (h *Handler) OrgTeams(w http.ResponseWriter, r *http.Request) { teams, _ := h.store.getTeams(ctx, org.ID) members, _ := h.store.getMembers(ctx, org.ID) - render(w, orgTeamsTmpl, &OrgTeamsData{ + renderOrg(w, orgTeamsTmpl, &OrgTeamsData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: org.Name + " — Teams", - Route: "orgs", + Route: "org-teams", Org: *org, MyRole: me.Role, Teams: teams, @@ -269,11 +273,11 @@ func (h *Handler) OrgMembers(w http.ResponseWriter, r *http.Request) { teams, _ := h.store.getTeams(ctx, org.ID) invites, _ := h.store.getInvites(ctx, org.ID) - render(w, orgMembersTmpl, &OrgMembersData{ + renderOrg(w, orgMembersTmpl, &OrgMembersData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: org.Name + " — Members", - Route: "orgs", + Route: "org-members", Org: *org, MyRole: me.Role, Members: members, @@ -325,11 +329,11 @@ func (h *Handler) OrgInviteNew(w http.ResponseWriter, r *http.Request) { teams, _ := h.store.getTeams(ctx, org.ID) if r.Method == http.MethodGet { - render(w, orgInviteTmpl, &OrgInviteData{ + renderOrg(w, orgInviteTmpl, &OrgInviteData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: "Invite to " + org.Name, - Route: "orgs", + Route: "org-invite", Org: *org, MyRole: me.Role, Teams: teams, @@ -350,11 +354,11 @@ func (h *Handler) OrgInviteNew(w http.ResponseWriter, r *http.Request) { } if errMsg != "" { - render(w, orgInviteTmpl, &OrgInviteData{ + renderOrg(w, orgInviteTmpl, &OrgInviteData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: "Invite to " + org.Name, - Route: "orgs", + Route: "org-invite", Org: *org, MyRole: me.Role, Teams: teams, @@ -387,11 +391,11 @@ func (h *Handler) OrgInviteNew(w http.ResponseWriter, r *http.Request) { } link := fmt.Sprintf("%s://%s/join/%s", scheme, r.Host, token) - render(w, orgInviteTmpl, &OrgInviteData{ + renderOrg(w, orgInviteTmpl, &OrgInviteData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: "Invite to " + org.Name, - Route: "orgs", + Route: "org-invite", Org: *org, MyRole: me.Role, Teams: teams, @@ -581,11 +585,11 @@ func (h *Handler) OrgEventList(w http.ResponseWriter, r *http.Request) { }) } - render(w, orgEventsTmpl, &OrgEventsData{ + renderOrg(w, orgEventsTmpl, &OrgEventsData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: org.Name + " — Events", - Route: "orgs", + Route: "org-events", Org: *org, MyRole: me.Role, FiscalYear: *year, @@ -608,11 +612,11 @@ func (h *Handler) OrgEventNew(w http.ResponseWriter, r *http.Request) { teams, _ := h.store.getTeams(ctx, org.ID) if r.Method == http.MethodGet { - render(w, orgEventDetailTmpl, &OrgEventDetailData{ + renderOrg(w, orgEventDetailTmpl, &OrgEventDetailData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: "New Event", - Route: "orgs", + Route: "org-event-detail", Org: *org, MyRole: me.Role, FiscalYear: *year, @@ -699,11 +703,11 @@ func (h *Handler) OrgEventDetail(w http.ResponseWriter, r *http.Request) { } } - render(w, orgEventDetailTmpl, &OrgEventDetailData{ + renderOrg(w, orgEventDetailTmpl, &OrgEventDetailData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: ev.Name, - Route: "orgs", + Route: "org-event-detail", Org: *org, MyRole: me.Role, FiscalYear: *year, @@ -1066,11 +1070,11 @@ func (h *Handler) OrgRequestList(w http.ResponseWriter, r *http.Request) { events, _ := h.store.getEvents(ctx, org.ID, "") teams, _ := h.store.getTeams(ctx, org.ID) - render(w, orgRequestsTmpl, &OrgRequestsData{ + renderOrg(w, orgRequestsTmpl, &OrgRequestsData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: org.Name + " — Requests", - Route: "orgs", + Route: "org-requests", Org: *org, MyRole: me.Role, Requests: requests, @@ -1096,11 +1100,11 @@ func (h *Handler) OrgRequestNew(w http.ResponseWriter, r *http.Request) { teams, _ := h.store.getTeams(ctx, org.ID) if r.Method == http.MethodGet { - render(w, orgRequestDetailTmpl, &OrgRequestDetailData{ + renderOrg(w, orgRequestDetailTmpl, &OrgRequestDetailData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: "New Request", - Route: "orgs", + Route: "org-requests", Org: *org, MyRole: me.Role, FiscalYear: activeYear, @@ -1203,11 +1207,11 @@ func (h *Handler) OrgRequestDetail(w http.ResponseWriter, r *http.Request) { } attachments, _ := h.store.getAttachments(ctx, reqID, org.ID) - render(w, orgRequestDetailTmpl, &OrgRequestDetailData{ + renderOrg(w, orgRequestDetailTmpl, &OrgRequestDetailData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: string(req.Type) + " Request", - Route: "orgs", + Route: "org-requests", Org: *org, MyRole: me.Role, Request: *req, @@ -1578,11 +1582,11 @@ func (h *Handler) OrgLedger(w http.ResponseWriter, r *http.Request) { } } - render(w, orgLedgerTmpl, &OrgLedgerData{ + renderOrg(w, orgLedgerTmpl, &OrgLedgerData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: org.Name + " — Ledger", - Route: "orgs", + Route: "org-ledger", Org: *org, MyRole: me.Role, FiscalYear: fy, @@ -1603,11 +1607,11 @@ func (h *Handler) OrgBankImport(w http.ResponseWriter, r *http.Request) { activeYear, _ := h.store.getActiveFiscalYear(ctx, org.ID) if r.Method == http.MethodGet { - render(w, orgBankImportTmpl, &OrgBankImportData{ + renderOrg(w, orgBankImportTmpl, &OrgBankImportData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), Title: org.Name + " — Bank Import", - Route: "orgs", + Route: "org-ledger", Org: *org, MyRole: me.Role, FiscalYear: activeYear, @@ -1628,9 +1632,9 @@ func (h *Handler) OrgBankImport(w http.ResponseWriter, r *http.Request) { rows, err := parseBankCSV(file) if err != nil { - render(w, orgBankImportTmpl, &OrgBankImportData{ + renderOrg(w, orgBankImportTmpl, &OrgBankImportData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), - Title: org.Name + " — Bank Import", Route: "orgs", + Title: org.Name + " — Bank Import", Route: "org-ledger", Org: *org, MyRole: me.Role, FiscalYear: activeYear, Error: "could not parse CSV: " + err.Error(), }) @@ -1639,9 +1643,9 @@ func (h *Handler) OrgBankImport(w http.ResponseWriter, r *http.Request) { if r.FormValue("confirm") != "1" { // preview mode - render(w, orgBankImportTmpl, &OrgBankImportData{ + renderOrg(w, orgBankImportTmpl, &OrgBankImportData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), - Title: org.Name + " — Bank Import", Route: "orgs", + Title: org.Name + " — Bank Import", Route: "org-ledger", Org: *org, MyRole: me.Role, FiscalYear: activeYear, Rows: rows, }) @@ -1674,9 +1678,9 @@ func (h *Handler) OrgBankImport(w http.ResponseWriter, r *http.Request) { imported++ } } - render(w, orgBankImportTmpl, &OrgBankImportData{ + renderOrg(w, orgBankImportTmpl, &OrgBankImportData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), - Title: org.Name + " — Bank Import", Route: "orgs", + Title: org.Name + " — Bank Import", Route: "org-ledger", Org: *org, MyRole: me.Role, FiscalYear: fy, Imported: imported, }) @@ -1780,9 +1784,9 @@ func (h *Handler) OrgAnalysis(w http.ResponseWriter, r *http.Request) { }) } - render(w, orgAnalysisTmpl, &OrgAnalysisData{ + renderOrg(w, orgAnalysisTmpl, &OrgAnalysisData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), - Title: org.Name + " — Analysis", Route: "orgs", + Title: org.Name + " — Analysis", Route: "org-analysis", Org: *org, MyRole: me.Role, FiscalYear: *year, FiscalYears: years, EventRows: eventRows, TeamRows: teamRows, TotalPlannedIncome: totalPI, TotalActualIncome: totalAI, @@ -1864,9 +1868,9 @@ func (h *Handler) OrgReport(w http.ResponseWriter, r *http.Request) { }) } - render(w, orgReportTmpl, &OrgReportData{ + renderOrg(w, orgReportTmpl, &OrgReportData{ UserID: r.Header.Get("X-Auth-User-Id"), Email: r.Header.Get("X-Auth-Email"), - Title: org.Name + " — " + year.Label + " Report", Route: "orgs", + Title: org.Name + " — " + year.Label + " Report", Route: "org-report", Org: *org, MyRole: me.Role, FiscalYear: *year, FiscalYears: years, EventReports: eventReports, TotalPlannedIncome: totalPI, TotalActualIncome: totalAI, diff --git a/apps/finance/services/api/main/models_org.go b/apps/finance/services/api/main/models_org.go index 1a076d3..e1431aa 100644 --- a/apps/finance/services/api/main/models_org.go +++ b/apps/finance/services/api/main/models_org.go @@ -319,17 +319,18 @@ type OrgWithRole struct { } type OrgHomeData struct { - UserID string - Email string - Title string - Route string - Org Org - MyRole OrgRole - MyTeamIDs []string + UserID string + Email string + Title string + Route string + Org Org + MyRole OrgRole + MyTeamIDs []string FiscalYears []FiscalYear - ActiveYear *FiscalYear - Teams []OrgTeam - Members []OrgMember + ActiveYear *FiscalYear + FiscalYear FiscalYear // populated from ActiveYear for base_org.html nav + Teams []OrgTeam + Members []OrgMember } type OrgTeamsData struct { diff --git a/apps/finance/services/api/main/templates/base.html b/apps/finance/services/api/main/templates/base.html index e8ecec2..de58781 100644 --- a/apps/finance/services/api/main/templates/base.html +++ b/apps/finance/services/api/main/templates/base.html @@ -570,11 +570,11 @@ + + + + +
+ {{block "content" .}}{{end}} +
+ + + + diff --git a/apps/finance/services/api/main/templates/homepage.html b/apps/finance/services/api/main/templates/homepage.html new file mode 100644 index 0000000..5e9c79b --- /dev/null +++ b/apps/finance/services/api/main/templates/homepage.html @@ -0,0 +1,734 @@ + + + + + + Finance Hub + + + + + + + +
+
+
+ +
+ + +
+ + {{if .Email}}{{.Email}}{{end}} +
+ + +
+
Your Financial Universe
+

Finance Hub

+

One platform for managing personal wealth and business finances — beautifully unified.

+
+ + +
+
+
2
+
Products
+
+
+
+
Organisations
+
+
+
100%
+
Self-hosted
+
+
+ + +
+ + +
+
+
🏦
+
+
Personal
+
My Finance
+
+
+

Track spending, grow wealth, plan your future with full visibility into your personal finances.

+
    +
  • Dashboard & spending analytics
  • +
  • Transactions & auto-import
  • +
  • Investment portfolio
  • +
  • Financial goals
  • +
  • Net worth & projections
  • +
  • Tax reports
  • +
+ Open Personal +
+ + +
+
+
🏢
+
+
Business
+
Organisations
+
+
+

Manage organisations, plan events, control budgets, and keep your teams aligned financially.

+
    +
  • Multi-org & teams
  • +
  • Events & budget planning
  • +
  • Purchase requests & approvals
  • +
  • Ledger & bank imports
  • +
  • Variance analysis
  • +
  • End-of-year reports
  • +
+ Open Business +
+ +
+ +
+ + +
+
🔒 Self-hosted & private
+
🌙 Dark & light themes
+
📊 Real-time analytics
+
🏦 Multi-bank support
+
👥 Role-based access
+
📅 Fiscal year lifecycle
+
+ + +
+ + + +