feat: replace overview with reports page
This commit is contained in:
parent
e5941e1329
commit
a7d5f21fe8
5 changed files with 12 additions and 200 deletions
|
|
@ -97,40 +97,24 @@ func (h *SpaceHandler) DashboardPage(w http.ResponseWriter, r *http.Request) {
|
||||||
space, err := h.spaceService.GetSpace(spaceID)
|
space, err := h.spaceService.GetSpace(spaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to get space", "error", err, "space_id", spaceID)
|
slog.Error("failed to get space", "error", err, "space_id", spaceID)
|
||||||
// The RequireSpaceAccess middleware should prevent this, but as a fallback.
|
|
||||||
http.Error(w, "Space not found.", http.StatusNotFound)
|
http.Error(w, "Space not found.", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lists, err := h.listService.GetListsForSpace(spaceID)
|
// Default to this month
|
||||||
|
now := time.Now()
|
||||||
|
presets := service.GetPresetDateRanges(now)
|
||||||
|
from := presets[0].From
|
||||||
|
to := presets[0].To
|
||||||
|
|
||||||
|
report, err := h.reportService.GetSpendingReport(spaceID, from, to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to get shopping lists for space", "error", err, "space_id", spaceID)
|
slog.Error("failed to get spending report", "error", err, "space_id", spaceID)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tags, err := h.tagService.GetTagsForSpace(spaceID)
|
ui.Render(w, r, pages.SpaceReportsPage(space, report, presets, "this_month"))
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to get tags for space", "error", err, "space_id", spaceID)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
listsWithItems, err := h.listService.GetListsWithUncheckedItems(spaceID)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to get lists with unchecked items", "error", err, "space_id", spaceID)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts, err := h.accountService.GetAccountsForSpace(spaceID)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to get accounts for space", "error", err, "space_id", spaceID)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.Render(w, r, pages.SpaceOverviewPage(space, lists, tags, listsWithItems, accounts))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SpaceHandler) ListsPage(w http.ResponseWriter, r *http.Request) {
|
func (h *SpaceHandler) ListsPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -644,12 +628,6 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
balance -= totalAllocated
|
balance -= totalAllocated
|
||||||
|
|
||||||
if r.URL.Query().Get("from") == "overview" {
|
|
||||||
w.Header().Set("HX-Redirect", "/app/spaces/"+spaceID+"/expenses?created=true")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the full paginated list for page 1 so the new expense appears
|
// Return the full paginated list for page 1 so the new expense appears
|
||||||
expenses, totalPages, err := h.expenseService.GetExpensesWithTagsAndMethodsForSpacePaginated(spaceID, 1)
|
expenses, totalPages, err := h.expenseService.GetExpensesWithTagsAndMethodsForSpacePaginated(spaceID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -2127,30 +2105,6 @@ func (h *SpaceHandler) GetBudgetsList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// --- Reports ---
|
// --- Reports ---
|
||||||
|
|
||||||
func (h *SpaceHandler) ReportsPage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
spaceID := r.PathValue("spaceID")
|
|
||||||
space, err := h.spaceService.GetSpace(spaceID)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Space not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to this month
|
|
||||||
now := time.Now()
|
|
||||||
presets := service.GetPresetDateRanges(now)
|
|
||||||
from := presets[0].From
|
|
||||||
to := presets[0].To
|
|
||||||
|
|
||||||
report, err := h.reportService.GetSpendingReport(spaceID, from, to)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to get spending report", "error", err, "space_id", spaceID)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.Render(w, r, pages.SpaceReportsPage(space, report, presets, "this_month"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SpaceHandler) GetReportCharts(w http.ResponseWriter, r *http.Request) {
|
func (h *SpaceHandler) GetReportCharts(w http.ResponseWriter, r *http.Request) {
|
||||||
spaceID := r.PathValue("spaceID")
|
spaceID := r.PathValue("spaceID")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,10 +212,6 @@ func SetupRoutes(a *app.App) http.Handler {
|
||||||
mux.Handle("GET /app/spaces/{spaceID}/components/budgets", budgetsListWithAccess)
|
mux.Handle("GET /app/spaces/{spaceID}/components/budgets", budgetsListWithAccess)
|
||||||
|
|
||||||
// Report routes
|
// Report routes
|
||||||
reportsPageHandler := middleware.RequireAuth(space.ReportsPage)
|
|
||||||
reportsPageWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(reportsPageHandler)
|
|
||||||
mux.Handle("GET /app/spaces/{spaceID}/reports", reportsPageWithAccess)
|
|
||||||
|
|
||||||
reportChartsHandler := middleware.RequireAuth(space.GetReportCharts)
|
reportChartsHandler := middleware.RequireAuth(space.GetReportCharts)
|
||||||
reportChartsWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(reportChartsHandler)
|
reportChartsWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(reportChartsHandler)
|
||||||
mux.Handle("GET /app/spaces/{spaceID}/components/report-charts", reportChartsWithAccess)
|
mux.Handle("GET /app/spaces/{spaceID}/components/report-charts", reportChartsWithAccess)
|
||||||
|
|
|
||||||
|
|
@ -22,19 +22,10 @@ type AddExpenseFormProps struct {
|
||||||
ListsWithItems []model.ListWithUncheckedItems
|
ListsWithItems []model.ListWithUncheckedItems
|
||||||
PaymentMethods []*model.PaymentMethod
|
PaymentMethods []*model.PaymentMethod
|
||||||
DialogID string // which dialog to close on success
|
DialogID string // which dialog to close on success
|
||||||
FromOverview bool // if true, POSTs with ?from=overview; server redirects to expenses page
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p AddExpenseFormProps) formAttrs() templ.Attributes {
|
func (p AddExpenseFormProps) formAttrs() templ.Attributes {
|
||||||
closeScript := "on htmx:afterOnLoad if event.detail.xhr.status == 200 then call window.tui.dialog.close('" + p.DialogID + "') then reset() me then show #item-selector-section end"
|
closeScript := "on htmx:afterOnLoad if event.detail.xhr.status == 200 then call window.tui.dialog.close('" + p.DialogID + "') then reset() me then show #item-selector-section end"
|
||||||
if p.FromOverview {
|
|
||||||
return templ.Attributes{
|
|
||||||
"hx-post": "/app/spaces/" + p.Space.ID + "/expenses?from=overview",
|
|
||||||
"hx-target": "body",
|
|
||||||
"hx-swap": "beforeend",
|
|
||||||
"_": closeScript,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return templ.Attributes{
|
return templ.Attributes{
|
||||||
"hx-post": "/app/spaces/" + p.Space.ID + "/expenses",
|
"hx-post": "/app/spaces/" + p.Space.ID + "/expenses",
|
||||||
"hx-target": "#expenses-list-wrapper",
|
"hx-target": "#expenses-list-wrapper",
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,10 @@ templ Space(title string, space *model.Space) {
|
||||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
||||||
Href: "/app/spaces/" + space.ID,
|
Href: "/app/spaces/" + space.ID,
|
||||||
IsActive: ctxkeys.URLPath(ctx) == "/app/spaces/"+space.ID,
|
IsActive: ctxkeys.URLPath(ctx) == "/app/spaces/"+space.ID,
|
||||||
Tooltip: "Space Dashboard",
|
Tooltip: "Reports",
|
||||||
}) {
|
}) {
|
||||||
@icon.House(icon.Props{Class: "size-4"})
|
@icon.ChartPie(icon.Props{Class: "size-4"})
|
||||||
<span>Overview</span>
|
<span>Reports</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@sidebar.MenuItem() {
|
@sidebar.MenuItem() {
|
||||||
|
|
@ -80,16 +80,6 @@ templ Space(title string, space *model.Space) {
|
||||||
<span>Budgets</span>
|
<span>Budgets</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@sidebar.MenuItem() {
|
|
||||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
|
||||||
Href: "/app/spaces/" + space.ID + "/reports",
|
|
||||||
IsActive: ctxkeys.URLPath(ctx) == "/app/spaces/"+space.ID+"/reports",
|
|
||||||
Tooltip: "Reports",
|
|
||||||
}) {
|
|
||||||
@icon.ChartPie(icon.Props{Class: "size-4"})
|
|
||||||
<span>Reports</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@sidebar.MenuItem() {
|
@sidebar.MenuItem() {
|
||||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
||||||
Href: "/app/spaces/" + space.ID + "/accounts",
|
Href: "/app/spaces/" + space.ID + "/accounts",
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
package pages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
|
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog"
|
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/expense"
|
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
|
||||||
)
|
|
||||||
|
|
||||||
templ SpaceOverviewPage(space *model.Space, lists []*model.ShoppingList, tags []*model.Tag, listsWithItems []model.ListWithUncheckedItems, accounts []model.MoneyAccountWithBalance) {
|
|
||||||
@layouts.Space("Overview", space) {
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<h1 class="text-2xl font-bold">Welcome to { space.Name }!</h1>
|
|
||||||
@dialog.Dialog(dialog.Props{ID: "add-expense-overview-dialog"}) {
|
|
||||||
@dialog.Trigger() {
|
|
||||||
@button.Button() {
|
|
||||||
Add Expense
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@dialog.Content() {
|
|
||||||
@dialog.Header() {
|
|
||||||
@dialog.Title() {
|
|
||||||
Add Transaction
|
|
||||||
}
|
|
||||||
@dialog.Description() {
|
|
||||||
Add a new expense or top-up to your space.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@expense.AddExpenseForm(expense.AddExpenseFormProps{
|
|
||||||
Space: space,
|
|
||||||
Tags: tags,
|
|
||||||
ListsWithItems: listsWithItems,
|
|
||||||
DialogID: "add-expense-overview-dialog",
|
|
||||||
FromOverview: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
// Shopping Lists section
|
|
||||||
<div class="border rounded-lg p-4">
|
|
||||||
<h2 class="text-lg font-semibold mb-2">Shopping Lists</h2>
|
|
||||||
if len(lists) > 0 {
|
|
||||||
<ul class="space-y-1">
|
|
||||||
for i, list := range lists {
|
|
||||||
if i < 5 {
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={ templ.URL("/app/spaces/" + space.ID + "/lists/" + list.ID) }
|
|
||||||
class="block px-3 py-2 rounded-md hover:bg-muted transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{ list.Name }
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<a
|
|
||||||
href={ templ.URL("/app/spaces/" + space.ID + "/lists") }
|
|
||||||
class="block text-sm text-muted-foreground hover:text-foreground transition-colors mt-2 px-3"
|
|
||||||
>
|
|
||||||
View all shopping lists
|
|
||||||
</a>
|
|
||||||
} else {
|
|
||||||
<p class="text-sm text-muted-foreground">No shopping lists yet.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
// Tags section
|
|
||||||
<div class="border rounded-lg p-4">
|
|
||||||
<h2 class="text-lg font-semibold mb-2">Tags</h2>
|
|
||||||
if len(tags) > 0 {
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
for _, tag := range tags {
|
|
||||||
@badge.Badge(badge.Props{Variant: badge.VariantSecondary}) {
|
|
||||||
{ tag.Name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<p class="text-sm text-muted-foreground">No tags yet.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
// Money Accounts section
|
|
||||||
<div class="border rounded-lg p-4">
|
|
||||||
<h2 class="text-lg font-semibold mb-2">Money Accounts</h2>
|
|
||||||
if len(accounts) > 0 {
|
|
||||||
<ul class="space-y-1">
|
|
||||||
for i, account := range accounts {
|
|
||||||
if i < 5 {
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={ templ.URL("/app/spaces/" + space.ID + "/accounts") }
|
|
||||||
class="block px-3 py-2 rounded-md hover:bg-muted transition-colors text-sm flex justify-between"
|
|
||||||
>
|
|
||||||
<span>{ account.Name }</span>
|
|
||||||
<span class="text-muted-foreground">{ fmt.Sprintf("$%.2f", float64(account.BalanceCents)/100) }</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<a
|
|
||||||
href={ templ.URL("/app/spaces/" + space.ID + "/accounts") }
|
|
||||||
class="block text-sm text-muted-foreground hover:text-foreground transition-colors mt-2 px-3"
|
|
||||||
>
|
|
||||||
View all accounts
|
|
||||||
</a>
|
|
||||||
} else {
|
|
||||||
<p class="text-sm text-muted-foreground">No accounts yet.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue