feat(finance): org Phases 2-5 — events, requests, ledger, analysis, report (#20)

Phase 2 — Event planning:
- Create/edit/delete/submit events per fiscal year
- Budget lines (income + expense) with planned amounts
- Admin review flow: approve / reject / comment
- Post-mortem feedback comments on closed years

Phase 3 — Transaction requests:
- 5 types: reimbursement, purchase_order, cash_advance, income, budget_transfer
- Full status machine with append-only StatusLog audit trail
- PO delivery sub-form (actual vendor, amount, invoice note)
- Cash advance settlement sub-form (spent + returned)
- Ledger view per fiscal year with income/expense summary
- Bank CSV import: parse → preview → confirm → ledger entries

Phase 4 — Plan vs actual analysis:
- Variance table by event (planned vs actual income/expense)
- Variance table by team

Phase 5 — Year-end report:
- Executive summary with net variance
- Per-event section: budget lines + team feedback comments
- Feedback form available on closed fiscal years

Supporting changes:
- New store methods: getLedgerEntries, createLedgerEntry, updateLedgerEntry,
  getAttachments, createAttachment
- New template funcs: dateInput, statusColor, varColor
- parseEuroAmount + parseBankCSV helpers in handler_org.go

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gonçalo Rodrigues 2026-06-14 12:58:47 +01:00 committed by GitHub
parent 1a935aa8ff
commit 0f58a51c6d
13 changed files with 2917 additions and 1 deletions

View File

@ -98,6 +98,35 @@ func parseTmpl(files ...string) *template.Template {
"isOver": func(spent, budget int64) bool {
return budget > 0 && spent > budget
},
"dateInput": func(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("2006-01-02")
},
"statusColor": func(s string) string {
switch s {
case "approved", "paid", "delivered", "settled", "received", "reconciled", "done":
return "rgba(74,222,128,0.12); color:var(--green)"
case "submitted", "under_review", "review", "ordered", "disbursed", "pending_payment":
return "rgba(99,179,237,0.12); color:#63b3ed"
case "rejected", "cancelled", "disputed":
return "rgba(239,68,68,0.12); color:var(--red)"
case "info_requested", "settlement_due", "partial_settlement":
return "rgba(251,191,36,0.1); color:#fbbf24"
default:
return "var(--bg3); color:var(--text3)"
}
},
"varColor": func(planned, actual int64) string {
if planned == 0 {
return "var(--text2)"
}
if actual > planned {
return "var(--red)"
}
return "var(--green)"
},
"jsonVals": func(m map[string]int64) template.JS {
var vals []string
for _, v := range m {
@ -135,7 +164,15 @@ var (
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")
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")
)
type authInfo struct {
@ -244,6 +281,11 @@ type storeIface interface {
createTxRequest(ctx context.Context, r *TxRequest) error
appendStatusLog(ctx context.Context, reqID, orgID string, entry StatusLogEntry) error
updateTxRequest(ctx context.Context, reqID, orgID string, update bson.M) error
getLedgerEntries(ctx context.Context, orgID, fiscalYearID string, extra bson.M) ([]OrgLedgerEntry, error)
createLedgerEntry(ctx context.Context, e *OrgLedgerEntry) error
updateLedgerEntry(ctx context.Context, id, orgID string, update bson.M) error
getAttachments(ctx context.Context, requestID, orgID string) ([]OrgAttachment, error)
createAttachment(ctx context.Context, a *OrgAttachment) error
}
type Handler struct {

File diff suppressed because it is too large Load Diff

View File

@ -217,6 +217,15 @@ func (m *mockStore) getTxRequest(_ context.Context, _, _ string) (*TxRequest, er
func (m *mockStore) createTxRequest(_ context.Context, _ *TxRequest) error { return nil }
func (m *mockStore) appendStatusLog(_ context.Context, _, _ string, _ StatusLogEntry) error { return nil }
func (m *mockStore) updateTxRequest(_ context.Context, _, _ string, _ bson.M) error { return nil }
func (m *mockStore) getLedgerEntries(_ context.Context, _, _ string, _ bson.M) ([]OrgLedgerEntry, error) {
return nil, nil
}
func (m *mockStore) createLedgerEntry(_ context.Context, _ *OrgLedgerEntry) error { return nil }
func (m *mockStore) updateLedgerEntry(_ context.Context, _, _ string, _ bson.M) error { return nil }
func (m *mockStore) getAttachments(_ context.Context, _, _ string) ([]OrgAttachment, error) {
return nil, nil
}
func (m *mockStore) createAttachment(_ context.Context, _ *OrgAttachment) error { return nil }
// ── helpers ───────────────────────────────────────────────────────────────────

View File

@ -354,3 +354,173 @@ type OrgInviteData struct {
Error string
Link string // generated invite link shown after creation
}
type OrgEventsData struct {
UserID string
Email string
Title string
Route string
Org Org
MyRole OrgRole
FiscalYear FiscalYear
Events []OrgEventSummary
Teams []OrgTeam
}
type OrgEventSummary struct {
Event OrgEvent
TotalIncome int64
TotalExpense int64
Teams []OrgTeam
}
type OrgEventDetailData struct {
UserID string
Email string
Title string
Route string
Org Org
MyRole OrgRole
FiscalYear FiscalYear
Event OrgEvent
BudgetLines []BudgetLine
Comments []EventComment
Teams []OrgTeam
EventTeams []OrgTeam
TotalIncome int64
TotalExpense int64
Error string
}
// ── Phase 3 page data ────────────────────────────────────────────────────────
type OrgRequestsData struct {
UserID string
Email string
Title string
Route string
Org Org
MyRole OrgRole
Requests []TxRequest
Events []OrgEvent
Teams []OrgTeam
StatusFilter string
}
type OrgRequestDetailData struct {
UserID string
Email string
Title string
Route string
Org Org
MyRole OrgRole
Request TxRequest
Event *OrgEvent
BudgetLine *BudgetLine
Team *OrgTeam
FiscalYear *FiscalYear
Attachments []OrgAttachment
Error string
}
type OrgLedgerData struct {
UserID string
Email string
Title string
Route string
Org Org
MyRole OrgRole
FiscalYear *FiscalYear
FiscalYears []FiscalYear
Entries []OrgLedgerEntry
Events map[string]OrgEvent
Teams map[string]OrgTeam
TotalIncome int64
TotalExpense int64
}
type OrgBankImportData struct {
UserID string
Email string
Title string
Route string
Org Org
MyRole OrgRole
FiscalYear *FiscalYear
Rows []BankImportRow
Error string
Imported int
}
type BankImportRow struct {
Date string
Description string
AmountCents int64
Reference string
Matched bool
MatchedID string
}
// ── Phase 4 page data ────────────────────────────────────────────────────────
type OrgAnalysisData struct {
UserID string
Email string
Title string
Route string
Org Org
MyRole OrgRole
FiscalYear FiscalYear
FiscalYears []FiscalYear
EventRows []AnalysisEventRow
TeamRows []AnalysisTeamRow
TotalPlannedIncome int64
TotalActualIncome int64
TotalPlannedExpense int64
TotalActualExpense int64
}
type AnalysisEventRow struct {
Event OrgEvent
PlannedIncome int64
ActualIncome int64
PlannedExpense int64
ActualExpense int64
}
type AnalysisTeamRow struct {
Team OrgTeam
PlannedIncome int64
ActualIncome int64
PlannedExpense int64
ActualExpense int64
}
// ── Phase 5 page data ────────────────────────────────────────────────────────
type OrgReportData struct {
UserID string
Email string
Title string
Route string
Org Org
MyRole OrgRole
FiscalYear FiscalYear
FiscalYears []FiscalYear
EventReports []EventReport
TotalPlannedIncome int64
TotalActualIncome int64
TotalPlannedExpense int64
TotalActualExpense int64
}
type EventReport struct {
Event OrgEvent
BudgetLines []BudgetLine
Comments []EventComment // kind=feedback only
PlannedIncome int64
ActualIncome int64
PlannedExpense int64
ActualExpense int64
Teams []OrgTeam
}

View File

@ -527,3 +527,64 @@ func (s *Store) updateTxRequest(ctx context.Context, reqID, orgID string, update
)
return err
}
// ── Ledger ────────────────────────────────────────────────────────────────────
func (s *Store) getLedgerEntries(ctx context.Context, orgID, fiscalYearID string, extra bson.M) ([]OrgLedgerEntry, error) {
ctx, span := mongo.StartSpan(ctx, "Store.getLedgerEntries")
defer span.End()
filter := bson.M{"org_id": orgID}
if fiscalYearID != "" {
filter["fiscal_year_id"] = fiscalYearID
}
for k, v := range extra {
filter[k] = v
}
cur, err := s.orgLedger().Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "date", Value: -1}}))
if err != nil {
return nil, err
}
var entries []OrgLedgerEntry
if err := cur.All(ctx, &entries); err != nil {
return nil, err
}
return entries, nil
}
func (s *Store) createLedgerEntry(ctx context.Context, e *OrgLedgerEntry) error {
ctx, span := mongo.StartSpan(ctx, "Store.createLedgerEntry")
defer span.End()
_, err := s.orgLedger().InsertOne(ctx, e)
return err
}
func (s *Store) updateLedgerEntry(ctx context.Context, id, orgID string, update bson.M) error {
ctx, span := mongo.StartSpan(ctx, "Store.updateLedgerEntry")
defer span.End()
_, err := s.orgLedger().UpdateOne(ctx, bson.M{"_id": id, "org_id": orgID}, bson.M{"$set": update})
return err
}
// ── Attachments ───────────────────────────────────────────────────────────────
func (s *Store) getAttachments(ctx context.Context, requestID, orgID string) ([]OrgAttachment, error) {
ctx, span := mongo.StartSpan(ctx, "Store.getAttachments")
defer span.End()
cur, err := s.orgAttachments().Find(ctx, bson.M{"request_id": requestID, "org_id": orgID})
if err != nil {
return nil, err
}
var attachments []OrgAttachment
if err := cur.All(ctx, &attachments); err != nil {
return nil, err
}
return attachments, nil
}
func (s *Store) createAttachment(ctx context.Context, a *OrgAttachment) error {
ctx, span := mongo.StartSpan(ctx, "Store.createAttachment")
defer span.End()
_, err := s.orgAttachments().InsertOne(ctx, a)
return err
}

View File

@ -0,0 +1,123 @@
{{define "content"}}
{{$d := .}}
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div>
<div style="font-size:12px; color:var(--text3); margin-bottom:4px;">
<a href="/orgs" style="color:var(--text3); text-decoration:none;">Orgs</a> /
<a href="/orgs/{{$d.Org.Slug}}" style="color:var(--text3); text-decoration:none;">{{$d.Org.Name}}</a> /
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events" style="color:var(--text3); text-decoration:none;">{{$d.FiscalYear.Label}}</a> / Analysis
</div>
<h1>Plan vs Actual</h1>
</div>
<div style="display:flex; gap:8px;">
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/report" class="btn btn-outline btn-sm">Year-end report</a>
</div>
</div>
<!-- Year selector -->
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:20px;">
{{range $d.FiscalYears}}
<a href="/orgs/{{$d.Org.Slug}}/years/{{.ID}}/analysis" style="padding:5px 12px; border-radius:20px; text-decoration:none; font-size:12px; font-weight:600;
{{if eq $d.FiscalYear.ID .ID}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">
{{.Label}}
</a>
{{end}}
</div>
<!-- Summary totals -->
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:28px;">
<div class="card value-card animate-on-scroll">
<h2>Planned income</h2>
<div class="value" style="color:var(--green); font-size:22px;">{{cents $d.TotalPlannedIncome}}</div>
</div>
<div class="card value-card animate-on-scroll">
<h2>Actual income</h2>
<div class="value" style="font-size:22px; color:{{varColor $d.TotalPlannedIncome $d.TotalActualIncome}};">{{cents $d.TotalActualIncome}}</div>
<div style="font-size:11px; color:var(--text3); margin-top:4px;">
{{if $d.TotalPlannedIncome}}{{cents (sub $d.TotalActualIncome $d.TotalPlannedIncome)}} vs plan{{end}}
</div>
</div>
<div class="card value-card animate-on-scroll">
<h2>Planned expense</h2>
<div class="value" style="color:var(--red); font-size:22px;">{{cents $d.TotalPlannedExpense}}</div>
</div>
<div class="card value-card animate-on-scroll">
<h2>Actual expense</h2>
<div class="value" style="font-size:22px; color:{{varColor $d.TotalPlannedExpense $d.TotalActualExpense}};">{{cents $d.TotalActualExpense}}</div>
<div style="font-size:11px; color:var(--text3); margin-top:4px;">
{{if $d.TotalPlannedExpense}}{{cents (sub $d.TotalActualExpense $d.TotalPlannedExpense)}} vs plan{{end}}
</div>
</div>
</div>
<!-- By event -->
<h2 style="margin-bottom:12px;">By event</h2>
{{if $d.EventRows}}
<div class="card animate-on-scroll" style="padding:0; overflow:hidden; margin-bottom:28px;">
<table>
<thead>
<tr>
<th>Event</th>
<th style="text-align:right;">Planned income</th>
<th style="text-align:right;">Actual income</th>
<th style="text-align:right;">Planned expense</th>
<th style="text-align:right;">Actual expense</th>
<th style="text-align:right;">Variance</th>
</tr>
</thead>
<tbody>
{{range $d.EventRows}}
{{$netPlan := sub .PlannedIncome .PlannedExpense}}
{{$netActual := sub .ActualIncome .ActualExpense}}
<tr>
<td style="font-weight:600;">
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{.Event.ID}}" style="color:var(--text); text-decoration:none;">{{.Event.Name}}</a>
</td>
<td style="text-align:right; color:var(--green); font-size:13px;">{{cents .PlannedIncome}}</td>
<td style="text-align:right; font-size:13px; color:{{varColor .PlannedIncome .ActualIncome}};">{{cents .ActualIncome}}</td>
<td style="text-align:right; color:var(--red); font-size:13px;">{{cents .PlannedExpense}}</td>
<td style="text-align:right; font-size:13px; color:{{varColor .PlannedExpense .ActualExpense}};">{{cents .ActualExpense}}</td>
<td style="text-align:right; font-size:13px; font-weight:600; color:{{if ge $netActual $netPlan}}var(--green){{else}}var(--red){{end}};">
{{cents (sub $netActual $netPlan)}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p style="color:var(--text3); font-size:13px; margin-bottom:28px;">No events in this fiscal year.</p>
{{end}}
<!-- By team -->
<h2 style="margin-bottom:12px;">By team</h2>
{{if $d.TeamRows}}
<div class="card animate-on-scroll" style="padding:0; overflow:hidden;">
<table>
<thead>
<tr>
<th>Team</th>
<th style="text-align:right;">Planned income</th>
<th style="text-align:right;">Actual income</th>
<th style="text-align:right;">Planned expense</th>
<th style="text-align:right;">Actual expense</th>
</tr>
</thead>
<tbody>
{{range $d.TeamRows}}
<tr>
<td style="font-weight:600;">{{.Team.Name}}</td>
<td style="text-align:right; color:var(--green); font-size:13px;">{{cents .PlannedIncome}}</td>
<td style="text-align:right; font-size:13px; color:{{varColor .PlannedIncome .ActualIncome}};">{{cents .ActualIncome}}</td>
<td style="text-align:right; color:var(--red); font-size:13px;">{{cents .PlannedExpense}}</td>
<td style="text-align:right; font-size:13px; color:{{varColor .PlannedExpense .ActualExpense}};">{{cents .ActualExpense}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p style="color:var(--text3); font-size:13px;">No teams configured.</p>
{{end}}
{{end}}

View File

@ -0,0 +1,89 @@
{{define "content"}}
{{$d := .}}
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div>
<div style="font-size:12px; color:var(--text3); margin-bottom:4px;">
<a href="/orgs" style="color:var(--text3); text-decoration:none;">Orgs</a> /
<a href="/orgs/{{$d.Org.Slug}}" style="color:var(--text3); text-decoration:none;">{{$d.Org.Name}}</a> /
<a href="/orgs/{{$d.Org.Slug}}/ledger" style="color:var(--text3); text-decoration:none;">Ledger</a> / Import
</div>
<h1>Bank CSV Import</h1>
</div>
</div>
{{if $d.Imported}}
<div class="card animate-on-scroll" style="border-color:var(--green); max-width:480px;">
<div style="font-size:36px; margin-bottom:12px;"></div>
<h2 style="color:var(--green); margin-bottom:8px;">Import complete</h2>
<p style="color:var(--text2); margin-bottom:16px;">{{$d.Imported}} entries imported into the ledger.</p>
<a href="/orgs/{{$d.Org.Slug}}/ledger" class="btn btn-primary">View ledger</a>
</div>
{{else if $d.Rows}}
<!-- Preview before confirming -->
<div class="card animate-on-scroll" style="margin-bottom:20px;">
<h2 style="margin-bottom:4px;">Preview — {{len $d.Rows}} rows</h2>
<p style="font-size:13px; color:var(--text3); margin-bottom:16px;">Review the parsed rows below, then confirm import.</p>
<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th>Date</th>
<th>Description</th>
<th style="text-align:right;">Amount</th>
<th>Reference</th>
</tr>
</thead>
<tbody>
{{range $d.Rows}}
<tr>
<td style="font-size:12px; white-space:nowrap;">{{.Date}}</td>
<td style="font-size:13px; max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{.Description}}</td>
<td style="text-align:right; font-size:13px; font-weight:600; {{if ge .AmountCents 0}}color:var(--green);{{else}}color:var(--red);{{end}}">{{cents .AmountCents}}</td>
<td style="font-size:11px; color:var(--text3); font-family:monospace;">{{if .Reference}}{{.Reference}}{{else}}—{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<form method="post" action="/orgs/{{$d.Org.Slug}}/ledger/import" enctype="multipart/form-data" style="display:flex; gap:10px;">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-primary">Confirm import</button>
<a href="/orgs/{{$d.Org.Slug}}/ledger/import" class="btn btn-outline">Start over</a>
</form>
{{else}}
<!-- Upload form -->
{{if not $d.FiscalYear}}
<div class="card animate-on-scroll" style="border-color:#fbbf24; max-width:480px;">
<p style="color:#fbbf24; font-size:13px;">⚠ No active fiscal year. Activate a fiscal year before importing bank transactions.</p>
</div>
{{end}}
<div class="card animate-on-scroll" style="max-width:480px; {{if not $d.FiscalYear}}opacity:0.5; pointer-events:none;{{end}}">
<h2 style="margin-bottom:6px;">Upload bank statement</h2>
<p style="font-size:13px; color:var(--text3); margin-bottom:20px;">
CSV must have columns: <code>date</code>, <code>description</code>, <code>amount</code>.
Optionally: <code>reference</code>. Amounts use decimal notation (e.g. <code>-50.00</code>).
</p>
{{if $d.Error}}
<div style="padding:10px 14px; border-radius:var(--radius-sm); background:rgba(239,68,68,0.1); color:var(--red); font-size:13px; margin-bottom:16px;">{{$d.Error}}</div>
{{end}}
<form method="post" action="/orgs/{{$d.Org.Slug}}/ledger/import" enctype="multipart/form-data">
<div style="margin-bottom:20px;">
<label class="form-label">CSV file</label>
<input class="form-input" type="file" name="csv" accept=".csv,text/csv" required>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;">Parse & preview</button>
</form>
</div>
<div class="card animate-on-scroll" style="max-width:480px; margin-top:16px; background:var(--bg3);">
<h3 style="font-size:13px; margin-bottom:8px; color:var(--text2);">Expected CSV format</h3>
<pre style="font-size:11px; color:var(--text3); line-height:1.6; overflow-x:auto;">date,description,amount,reference
2025-01-15,Supermarket purchase,-45.20,REF001
2025-01-16,Client payment received,1200.00,REF002
2025-01-17,Office supplies,-89.99,REF003</pre>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,332 @@
{{define "content"}}
{{$d := .}}
{{$isNew := eq $d.Event.ID ""}}
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div>
<div style="font-size:12px; color:var(--text3); margin-bottom:4px;">
<a href="/orgs" style="color:var(--text3); text-decoration:none;">Orgs</a> /
<a href="/orgs/{{$d.Org.Slug}}" style="color:var(--text3); text-decoration:none;">{{$d.Org.Name}}</a> /
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events" style="color:var(--text3); text-decoration:none;">Events</a> /
{{if $isNew}}New event{{else}}{{$d.Event.Name}}{{end}}
</div>
<h1>{{if $isNew}}New event{{else}}{{$d.Event.Name}}{{end}}</h1>
</div>
{{if not $isNew}}
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
{{if eq (print $d.Event.Status) "approved"}}
<span style="font-size:12px; font-weight:700; padding:4px 12px; border-radius:20px; background:rgba(74,222,128,0.12); color:var(--green);">✓ Approved</span>
{{else if eq (print $d.Event.Status) "review"}}
<span style="font-size:12px; font-weight:700; padding:4px 12px; border-radius:20px; background:rgba(251,191,36,0.1); color:#fbbf24;">Under review</span>
{{else if eq (print $d.Event.Status) "rejected"}}
<span style="font-size:12px; font-weight:700; padding:4px 12px; border-radius:20px; background:rgba(239,68,68,0.12); color:var(--red);">Rejected</span>
{{else}}
<span style="font-size:12px; font-weight:700; padding:4px 12px; border-radius:20px; background:var(--bg3); color:var(--text3);">Draft</span>
{{end}}
{{if eq (print $d.Event.Status) "draft"}}
<button onclick="document.getElementById('edit-modal').style.display='flex'" class="btn btn-outline btn-sm">Edit</button>
<form method="post" action="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{$d.Event.ID}}/submit" style="display:inline;">
<button class="btn btn-primary btn-sm" onclick="return confirm('Submit this event for review?')">Submit for review</button>
</form>
<form method="post" action="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{$d.Event.ID}}/delete" style="display:inline;">
<button class="btn btn-sm" style="color:var(--red); background:transparent; border:1px solid var(--red);" onclick="return confirm('Delete this event?')">Delete</button>
</form>
{{else if eq (print $d.Event.Status) "review"}}
<button onclick="document.getElementById('edit-modal').style.display='flex'" class="btn btn-outline btn-sm">Edit</button>
{{end}}
{{if and (or (eq (print $d.MyRole) "admin") (eq (print $d.MyRole) "finance")) (eq (print $d.Event.Status) "review")}}
<button onclick="document.getElementById('review-modal').style.display='flex'" class="btn btn-primary btn-sm">Review</button>
{{end}}
</div>
{{end}}
</div>
{{if $isNew}}
<!-- New event form -->
<div class="card animate-on-scroll" style="max-width:640px;">
<form method="post" action="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/new">
<div style="margin-bottom:16px;">
<label class="form-label">Event name</label>
<input class="form-input" type="text" name="name" placeholder="Annual conference" required autofocus>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px;">
<div>
<label class="form-label">Start date</label>
<input class="form-input" type="date" name="date_start" required>
</div>
<div>
<label class="form-label">End date</label>
<input class="form-input" type="date" name="date_end" required>
</div>
</div>
<div style="margin-bottom:16px;">
<label class="form-label">Description</label>
<textarea class="form-input" name="description" rows="3" placeholder="What is this event about?"></textarea>
</div>
<div style="margin-bottom:16px;">
<label class="form-label">Goals</label>
<textarea class="form-input" name="goals" rows="2" placeholder="What should we achieve?"></textarea>
</div>
{{if $d.Teams}}
<div style="margin-bottom:20px;">
<label class="form-label">Teams</label>
<div style="display:flex; flex-wrap:wrap; gap:8px;">
{{range $d.Teams}}
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; padding:6px 10px; border-radius:var(--radius-sm); border:1px solid var(--border); font-size:13px;">
<input type="checkbox" name="team_ids" value="{{.ID}}"> {{.Name}}
</label>
{{end}}
</div>
</div>
{{end}}
<div style="display:flex; gap:10px;">
<button type="submit" class="btn btn-primary">Create event</button>
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events" class="btn btn-outline">Cancel</a>
</div>
</form>
</div>
{{else}}
<!-- Event detail -->
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:16px; margin-bottom:24px;">
<div class="card value-card animate-on-scroll">
<h2>Planned income</h2>
<div class="value" style="color:var(--green);">{{cents $d.TotalIncome}}</div>
</div>
<div class="card value-card animate-on-scroll">
<h2>Planned expense</h2>
<div class="value" style="color:var(--red);">{{cents $d.TotalExpense}}</div>
</div>
<div class="card value-card animate-on-scroll">
<h2>Net</h2>
<div class="value" style="color:var(--accent2);">{{cents (sub $d.TotalIncome $d.TotalExpense)}}</div>
</div>
</div>
<div style="display:grid; grid-template-columns:2fr 1fr; gap:20px;">
<div>
<!-- Description card -->
<div class="card animate-on-scroll" style="margin-bottom:20px;">
<h2 style="margin-bottom:12px;">Details</h2>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; font-size:13px; margin-bottom:14px;">
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">PERIOD</div>
<div>{{dateShort $d.Event.DateStart}} — {{dateShort $d.Event.DateEnd}}</div>
</div>
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">TEAMS</div>
<div>{{if $d.EventTeams}}{{range $d.EventTeams}}<span style="display:inline-block; padding:1px 6px; border-radius:4px; background:var(--bg3); margin-right:3px; font-size:11px;">{{.Name}}</span>{{end}}{{else}}—{{end}}</div>
</div>
</div>
{{if $d.Event.Description}}
<div style="margin-bottom:10px;">
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:6px;">DESCRIPTION</div>
<p style="font-size:13px; color:var(--text2); line-height:1.6;">{{$d.Event.Description}}</p>
</div>
{{end}}
{{if $d.Event.Goals}}
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:6px;">GOALS</div>
<p style="font-size:13px; color:var(--text2); line-height:1.6;">{{$d.Event.Goals}}</p>
</div>
{{end}}
</div>
<!-- Budget lines -->
<div class="card animate-on-scroll" style="margin-bottom:20px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<h2>Budget lines</h2>
{{if ne (print $d.Event.Status) "approved"}}
<button onclick="document.getElementById('budget-modal').style.display='flex'" class="btn btn-outline btn-sm">+ Add line</button>
{{end}}
</div>
{{if $d.BudgetLines}}
<table>
<thead>
<tr>
<th>Category</th>
<th>Description</th>
<th>Type</th>
<th style="text-align:right;">Amount</th>
{{if ne (print $d.Event.Status) "approved"}}<th></th>{{end}}
</tr>
</thead>
<tbody>
{{range $d.BudgetLines}}
<tr>
<td style="font-size:13px; font-weight:600;">{{.Category}}</td>
<td style="font-size:12px; color:var(--text2);">{{.Description}}</td>
<td>
{{if eq (print .Type) "income"}}
<span style="font-size:11px; font-weight:700; color:var(--green);">income</span>
{{else}}
<span style="font-size:11px; font-weight:700; color:var(--red);">expense</span>
{{end}}
</td>
<td style="text-align:right; font-size:13px;">{{cents .PlannedCents}}</td>
{{if ne (print $d.Event.Status) "approved"}}
<td style="text-align:right;">
<form method="post" action="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{$d.Event.ID}}/budget/{{.ID}}/delete" style="display:inline;">
<button class="btn btn-sm" style="color:var(--red); background:transparent; border:none; padding:2px 6px;" onclick="return confirm('Remove this budget line?')">×</button>
</form>
</td>
{{end}}
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p style="color:var(--text3); font-size:13px;">No budget lines yet. Add income and expense lines to plan this event's finances.</p>
{{end}}
</div>
<!-- Comments -->
<div class="card animate-on-scroll">
<h2 style="margin-bottom:16px;">Comments</h2>
{{if $d.Comments}}
<div style="display:flex; flex-direction:column; gap:12px; margin-bottom:16px;">
{{range $d.Comments}}
<div style="padding:12px 14px; border-radius:var(--radius-sm); background:var(--bg3); border-left:3px solid
{{if eq (print .Kind) "feedback"}}var(--accent2){{else}}var(--accent){{end}};">
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
<span style="font-size:12px; font-weight:600;">{{.UserEmail}}</span>
<span style="font-size:11px; color:var(--text3);">{{dateShort .CreatedAt}} · {{.Kind}}</span>
</div>
<p style="font-size:13px; color:var(--text2); line-height:1.5; margin:0;">{{.Body}}</p>
</div>
{{end}}
</div>
{{end}}
{{if and (eq (print $d.FiscalYear.Status) "closed") (ne (print $d.MyRole) "viewer")}}
<form method="post" action="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{$d.Event.ID}}/feedback">
<div style="margin-bottom:10px;">
<label class="form-label">Post-mortem feedback</label>
<textarea class="form-input" name="comment" rows="3" placeholder="What went well? What could improve?" required></textarea>
</div>
<button type="submit" class="btn btn-outline btn-sm">Add feedback</button>
</form>
{{end}}
</div>
</div>
<!-- Sidebar -->
<div>
<div class="card animate-on-scroll" style="margin-bottom:16px;">
<h2 style="margin-bottom:12px; font-size:14px;">Quick actions</h2>
<div style="display:flex; flex-direction:column; gap:8px;">
<a href="/orgs/{{$d.Org.Slug}}/requests/new?event_id={{$d.Event.ID}}" class="btn btn-outline btn-sm" style="text-align:center;">
+ New request
</a>
</div>
</div>
</div>
</div>
<!-- Edit modal -->
<div id="edit-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:500; align-items:center; justify-content:center; padding:16px;">
<div class="card" style="width:100%; max-width:520px; max-height:80vh; overflow-y:auto;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2>Edit event</h2>
<button onclick="document.getElementById('edit-modal').style.display='none'" style="background:none; border:none; color:var(--text3); font-size:20px; cursor:pointer;">×</button>
</div>
<form method="post" action="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{$d.Event.ID}}/edit">
<div style="margin-bottom:14px;">
<label class="form-label">Name</label>
<input class="form-input" type="text" name="name" value="{{$d.Event.Name}}" required>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:14px;">
<div>
<label class="form-label">Start date</label>
<input class="form-input" type="date" name="date_start" value="{{$d.Event.DateStart | dateInput}}" required>
</div>
<div>
<label class="form-label">End date</label>
<input class="form-input" type="date" name="date_end" value="{{$d.Event.DateEnd | dateInput}}" required>
</div>
</div>
<div style="margin-bottom:14px;">
<label class="form-label">Description</label>
<textarea class="form-input" name="description" rows="3">{{$d.Event.Description}}</textarea>
</div>
<div style="margin-bottom:14px;">
<label class="form-label">Goals</label>
<textarea class="form-input" name="goals" rows="2">{{$d.Event.Goals}}</textarea>
</div>
{{if $d.Teams}}
<div style="margin-bottom:20px;">
<label class="form-label">Teams</label>
<div style="display:flex; flex-wrap:wrap; gap:8px;">
{{range $d.Teams}}
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; padding:6px 10px; border-radius:var(--radius-sm); border:1px solid var(--border); font-size:13px;">
<input type="checkbox" name="team_ids" value="{{.ID}}"
{{range $d.EventTeams}}{{if eq .ID $.ID}}checked{{end}}{{end}}> {{.Name}}
</label>
{{end}}
</div>
</div>
{{end}}
<button type="submit" class="btn btn-primary" style="width:100%;">Save changes</button>
</form>
</div>
</div>
<!-- Add budget line modal -->
<div id="budget-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:500; align-items:center; justify-content:center; padding:16px;">
<div class="card" style="width:100%; max-width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2>Add budget line</h2>
<button onclick="document.getElementById('budget-modal').style.display='none'" style="background:none; border:none; color:var(--text3); font-size:20px; cursor:pointer;">×</button>
</div>
<form method="post" action="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{$d.Event.ID}}/budget">
<div style="margin-bottom:14px;">
<label class="form-label">Type</label>
<select class="form-input" name="type">
<option value="expense">Expense</option>
<option value="income">Income</option>
</select>
</div>
<div style="margin-bottom:14px;">
<label class="form-label">Category</label>
<input class="form-input" type="text" name="category" placeholder="Catering, Transport, Tickets…" required>
</div>
<div style="margin-bottom:14px;">
<label class="form-label">Planned amount (€)</label>
<input class="form-input" type="text" name="amount" placeholder="500.00" required>
</div>
<div style="margin-bottom:20px;">
<label class="form-label">Description</label>
<input class="form-input" type="text" name="description" placeholder="Optional details">
</div>
<button type="submit" class="btn btn-primary" style="width:100%;">Add line</button>
</form>
</div>
</div>
<!-- Admin review modal -->
{{if and (or (eq (print $d.MyRole) "admin") (eq (print $d.MyRole) "finance")) (eq (print $d.Event.Status) "review")}}
<div id="review-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:500; align-items:center; justify-content:center; padding:16px;">
<div class="card" style="width:100%; max-width:440px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2>Review event</h2>
<button onclick="document.getElementById('review-modal').style.display='none'" style="background:none; border:none; color:var(--text3); font-size:20px; cursor:pointer;">×</button>
</div>
<form method="post" action="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{$d.Event.ID}}/review">
<div style="margin-bottom:16px;">
<label class="form-label">Comment (optional)</label>
<textarea class="form-input" name="comment" rows="3" placeholder="Leave feedback or request changes…"></textarea>
</div>
<div style="display:flex; gap:8px;">
<button type="submit" name="action" value="approve" class="btn btn-primary" style="flex:1;" onclick="return confirm('Approve this event?')">Approve</button>
<button type="submit" name="action" value="comment" class="btn btn-outline" style="flex:1;">Comment only</button>
<button type="submit" name="action" value="reject" class="btn btn-sm" style="flex:1; color:var(--red); background:transparent; border:1px solid var(--red);" onclick="return confirm('Reject this event?')">Reject</button>
</div>
</form>
</div>
</div>
{{end}}
{{end}}{{end}}

View File

@ -0,0 +1,85 @@
{{define "content"}}
{{$d := .}}
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div>
<div style="font-size:12px; color:var(--text3); margin-bottom:4px;">
<a href="/orgs" style="color:var(--text3); text-decoration:none;">Orgs</a> /
<a href="/orgs/{{$d.Org.Slug}}" style="color:var(--text3); text-decoration:none;">{{$d.Org.Name}}</a> /
{{$d.FiscalYear.Label}} — Events
</div>
<h1>Events</h1>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/analysis" class="btn btn-outline btn-sm">Analysis</a>
{{if ne (print $d.FiscalYear.Status) "closed"}}
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/new" class="btn btn-primary btn-sm">+ New event</a>
{{end}}
</div>
</div>
<!-- Year status banner -->
<div style="display:flex; gap:8px; align-items:center; margin-bottom:20px;">
<span style="font-size:12px; color:var(--text3);">Fiscal year:</span>
<span style="font-weight:600;">{{$d.FiscalYear.Label}}</span>
<span style="font-size:11px; font-weight:700; padding:2px 8px; border-radius:20px;
{{if eq (print $d.FiscalYear.Status) "active"}}background:rgba(74,222,128,0.12); color:var(--green);
{{else if eq (print $d.FiscalYear.Status) "closed"}}background:var(--bg3); color:var(--text3);
{{else}}background:rgba(251,191,36,0.1); color:#fbbf24;{{end}}">
{{$d.FiscalYear.Status}}
</span>
<span style="font-size:12px; color:var(--text3);">{{dateShort $d.FiscalYear.StartDate}} — {{dateShort $d.FiscalYear.EndDate}}</span>
</div>
{{if $d.Events}}
<div class="card animate-on-scroll" style="padding:0; overflow:hidden;">
<table>
<thead>
<tr>
<th>Event</th>
<th>Teams</th>
<th>Dates</th>
<th style="text-align:right;">Planned income</th>
<th style="text-align:right;">Planned expense</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{{range $d.Events}}
<tr>
<td style="font-weight:600;">{{.Event.Name}}</td>
<td style="font-size:12px; color:var(--text3);">
{{if .Teams}}{{range .Teams}}<span style="display:inline-block; padding:1px 6px; border-radius:4px; background:var(--bg3); margin-right:3px; font-size:11px;">{{.Name}}</span>{{end}}{{else}}—{{end}}
</td>
<td style="font-size:12px; color:var(--text2);">{{dateShort .Event.DateStart}} — {{dateShort .Event.DateEnd}}</td>
<td style="text-align:right; color:var(--green); font-size:13px;">{{cents .TotalIncome}}</td>
<td style="text-align:right; color:var(--red); font-size:13px;">{{cents .TotalExpense}}</td>
<td>
{{if eq (print .Event.Status) "approved"}}
<span style="font-size:11px; font-weight:700; padding:2px 8px; border-radius:20px; background:rgba(74,222,128,0.12); color:var(--green);">approved</span>
{{else if eq (print .Event.Status) "review"}}
<span style="font-size:11px; font-weight:700; padding:2px 8px; border-radius:20px; background:rgba(251,191,36,0.1); color:#fbbf24;">review</span>
{{else if eq (print .Event.Status) "rejected"}}
<span style="font-size:11px; font-weight:700; padding:2px 8px; border-radius:20px; background:rgba(239,68,68,0.12); color:var(--red);">rejected</span>
{{else}}
<span style="font-size:11px; font-weight:700; padding:2px 8px; border-radius:20px; background:var(--bg3); color:var(--text3);">draft</span>
{{end}}
</td>
<td style="text-align:right;">
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/{{.Event.ID}}" class="btn btn-outline btn-sm">View</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="card empty-state animate-on-scroll">
<div style="font-size:36px; margin-bottom:12px;">🗓️</div>
<h3>No events yet</h3>
<p style="margin-bottom:16px;">Plan events with budgets for teams to execute this fiscal year.</p>
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events/new" class="btn btn-primary">Create first event</a>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,89 @@
{{define "content"}}
{{$d := .}}
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div>
<div style="font-size:12px; color:var(--text3); margin-bottom:4px;">
<a href="/orgs" style="color:var(--text3); text-decoration:none;">Orgs</a> /
<a href="/orgs/{{$d.Org.Slug}}" style="color:var(--text3); text-decoration:none;">{{$d.Org.Name}}</a> / Ledger
</div>
<h1>Ledger</h1>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a href="/orgs/{{$d.Org.Slug}}/ledger/import" class="btn btn-outline btn-sm">Import bank CSV</a>
</div>
</div>
<!-- Year selector -->
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:20px;">
{{range $d.FiscalYears}}
<a href="?year_id={{.ID}}" style="padding:5px 12px; border-radius:20px; text-decoration:none; font-size:12px; font-weight:600;
{{if and $d.FiscalYear (eq $d.FiscalYear.ID .ID)}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">
{{.Label}}
</a>
{{end}}
</div>
<!-- Summary cards -->
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin-bottom:24px;">
<div class="card value-card animate-on-scroll">
<h2>Income</h2>
<div class="value" style="color:var(--green);">{{cents $d.TotalIncome}}</div>
</div>
<div class="card value-card animate-on-scroll">
<h2>Expenses</h2>
<div class="value" style="color:var(--red);">{{cents $d.TotalExpense}}</div>
</div>
<div class="card value-card animate-on-scroll">
<h2>Net</h2>
<div class="value" style="color:var(--accent2);">{{cents (sub $d.TotalIncome $d.TotalExpense)}}</div>
</div>
</div>
{{if $d.Entries}}
<div class="card animate-on-scroll" style="padding:0; overflow:hidden;">
<table>
<thead>
<tr>
<th>Date</th>
<th>Description</th>
<th>Event</th>
<th>Team</th>
<th style="text-align:right;">Amount</th>
<th>Bank ref</th>
<th>Reconciled</th>
</tr>
</thead>
<tbody>
{{range $d.Entries}}
<tr>
<td style="font-size:12px; color:var(--text3); white-space:nowrap;">{{dateShort .Date}}</td>
<td style="font-size:13px; max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{.Description}}</td>
<td style="font-size:12px; color:var(--text3);">
{{if .EventID}}{{with index $d.Events .EventID}}{{.Name}}{{end}}{{else}}—{{end}}
</td>
<td style="font-size:12px; color:var(--text3);">
{{if .TeamID}}{{with index $d.Teams .TeamID}}{{.Name}}{{end}}{{else}}—{{end}}
</td>
<td style="text-align:right; font-size:13px; font-weight:600;
{{if ge .AmountCents 0}}color:var(--green);{{else}}color:var(--red);{{end}}">
{{cents .AmountCents}}
</td>
<td style="font-size:11px; color:var(--text3); font-family:monospace;">{{if .BankRef}}{{.BankRef}}{{else}}—{{end}}</td>
<td style="text-align:center;">
{{if .Reconciled}}<span style="color:var(--green);"></span>{{else}}<span style="color:var(--text3);"></span>{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="card empty-state animate-on-scroll">
<div style="font-size:36px; margin-bottom:12px;">📒</div>
<h3>No ledger entries</h3>
<p style="margin-bottom:16px;">Entries are created automatically when requests are approved, or via bank CSV import.</p>
<a href="/orgs/{{$d.Org.Slug}}/ledger/import" class="btn btn-primary">Import bank CSV</a>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,167 @@
{{define "content"}}
{{$d := .}}
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div>
<div style="font-size:12px; color:var(--text3); margin-bottom:4px;">
<a href="/orgs" style="color:var(--text3); text-decoration:none;">Orgs</a> /
<a href="/orgs/{{$d.Org.Slug}}" style="color:var(--text3); text-decoration:none;">{{$d.Org.Name}}</a> /
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/events" style="color:var(--text3); text-decoration:none;">{{$d.FiscalYear.Label}}</a> / Report
</div>
<h1>{{$d.FiscalYear.Label}} Year-End Report</h1>
</div>
<div style="display:flex; gap:8px;">
<a href="/orgs/{{$d.Org.Slug}}/years/{{$d.FiscalYear.ID}}/analysis" class="btn btn-outline btn-sm">Analysis</a>
</div>
</div>
<!-- Year selector -->
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:20px;">
{{range $d.FiscalYears}}
<a href="/orgs/{{$d.Org.Slug}}/years/{{.ID}}/report" style="padding:5px 12px; border-radius:20px; text-decoration:none; font-size:12px; font-weight:600;
{{if eq $d.FiscalYear.ID .ID}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">
{{.Label}}
</a>
{{end}}
</div>
<!-- Executive summary -->
<div class="card animate-on-scroll" style="margin-bottom:28px;">
<h2 style="margin-bottom:16px;">Executive Summary</h2>
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:16px;">
<div>
<div style="font-size:11px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:6px;">PLANNED INCOME</div>
<div style="font-size:20px; font-weight:700; color:var(--green);">{{cents $d.TotalPlannedIncome}}</div>
</div>
<div>
<div style="font-size:11px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:6px;">ACTUAL INCOME</div>
<div style="font-size:20px; font-weight:700; color:{{varColor $d.TotalPlannedIncome $d.TotalActualIncome}};">{{cents $d.TotalActualIncome}}</div>
</div>
<div>
<div style="font-size:11px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:6px;">PLANNED EXPENSE</div>
<div style="font-size:20px; font-weight:700; color:var(--red);">{{cents $d.TotalPlannedExpense}}</div>
</div>
<div>
<div style="font-size:11px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:6px;">ACTUAL EXPENSE</div>
<div style="font-size:20px; font-weight:700; color:{{varColor $d.TotalPlannedExpense $d.TotalActualExpense}};">{{cents $d.TotalActualExpense}}</div>
</div>
</div>
<div style="margin-top:16px; padding-top:16px; border-top:1px solid var(--border); display:flex; gap:32px;">
<div>
<span style="font-size:12px; color:var(--text3);">Planned net: </span>
<span style="font-weight:600;">{{cents (sub $d.TotalPlannedIncome $d.TotalPlannedExpense)}}</span>
</div>
<div>
<span style="font-size:12px; color:var(--text3);">Actual net: </span>
<span style="font-weight:600; color:{{varColor (sub $d.TotalPlannedIncome $d.TotalPlannedExpense) (sub $d.TotalActualIncome $d.TotalActualExpense)}};">
{{cents (sub $d.TotalActualIncome $d.TotalActualExpense)}}
</span>
</div>
<div>
<span style="font-size:12px; color:var(--text3);">Events: </span>
<span style="font-weight:600;">{{len $d.EventReports}}</span>
</div>
<div>
<span style="font-size:12px; color:var(--text3);">Period: </span>
<span style="font-weight:600;">{{dateShort $d.FiscalYear.StartDate}} — {{dateShort $d.FiscalYear.EndDate}}</span>
</div>
</div>
</div>
<!-- Event reports -->
{{range $d.EventReports}}
{{$ev := .Event}}
<div class="card animate-on-scroll" style="margin-bottom:20px;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; flex-wrap:wrap; gap:12px; margin-bottom:16px;">
<div>
<h2 style="margin-bottom:4px;">{{$ev.Name}}</h2>
<div style="font-size:12px; color:var(--text3);">{{dateShort $ev.DateStart}} — {{dateShort $ev.DateEnd}}</div>
{{if .Teams}}
<div style="margin-top:6px;">
{{range .Teams}}<span style="display:inline-block; padding:1px 6px; border-radius:4px; background:var(--bg3); margin-right:3px; font-size:11px; color:var(--text2);">{{.Name}}</span>{{end}}
</div>
{{end}}
</div>
<div style="display:grid; grid-template-columns:repeat(4,auto); gap:16px; text-align:right;">
<div>
<div style="font-size:10px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:3px;">PLAN INC</div>
<div style="font-size:14px; font-weight:600; color:var(--green);">{{cents .PlannedIncome}}</div>
</div>
<div>
<div style="font-size:10px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:3px;">ACT INC</div>
<div style="font-size:14px; font-weight:600; color:{{varColor .PlannedIncome .ActualIncome}};">{{cents .ActualIncome}}</div>
</div>
<div>
<div style="font-size:10px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:3px;">PLAN EXP</div>
<div style="font-size:14px; font-weight:600; color:var(--red);">{{cents .PlannedExpense}}</div>
</div>
<div>
<div style="font-size:10px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:3px;">ACT EXP</div>
<div style="font-size:14px; font-weight:600; color:{{varColor .PlannedExpense .ActualExpense}};">{{cents .ActualExpense}}</div>
</div>
</div>
</div>
{{if $ev.Description}}
<p style="font-size:13px; color:var(--text2); line-height:1.6; margin-bottom:14px;">{{$ev.Description}}</p>
{{end}}
{{if .BudgetLines}}
<details style="margin-bottom:14px;">
<summary style="font-size:12px; font-weight:700; color:var(--text3); cursor:pointer; letter-spacing:.04em;">BUDGET LINES ({{len .BudgetLines}})</summary>
<table style="margin-top:10px;">
<thead>
<tr>
<th>Category</th>
<th>Description</th>
<th>Type</th>
<th style="text-align:right;">Planned</th>
</tr>
</thead>
<tbody>
{{range .BudgetLines}}
<tr>
<td style="font-size:13px; font-weight:600;">{{.Category}}</td>
<td style="font-size:12px; color:var(--text3);">{{.Description}}</td>
<td style="font-size:11px; font-weight:700; {{if eq (print .Type) "income"}}color:var(--green);{{else}}color:var(--red);{{end}}">{{.Type}}</td>
<td style="text-align:right; font-size:13px;">{{cents .PlannedCents}}</td>
</tr>
{{end}}
</tbody>
</table>
</details>
{{end}}
{{if .Comments}}
<div>
<div style="font-size:11px; font-weight:700; letter-spacing:.06em; color:var(--text3); margin-bottom:10px;">TEAM FEEDBACK</div>
<div style="display:flex; flex-direction:column; gap:10px;">
{{range .Comments}}
<div style="padding:10px 14px; border-radius:var(--radius-sm); background:var(--bg3); border-left:3px solid var(--accent2);">
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
<span style="font-size:12px; font-weight:600;">{{.UserEmail}}</span>
<span style="font-size:11px; color:var(--text3);">{{dateShort .CreatedAt}}</span>
</div>
<p style="font-size:13px; color:var(--text2); line-height:1.5; margin:0;">{{.Body}}</p>
</div>
{{end}}
</div>
</div>
{{else if eq (print $d.FiscalYear.Status) "closed"}}
<p style="font-size:12px; color:var(--text3); font-style:italic;">No team feedback submitted for this event.</p>
{{end}}
</div>
{{else}}
<div class="card empty-state animate-on-scroll">
<div style="font-size:36px; margin-bottom:12px;">📊</div>
<h3>No events in this fiscal year</h3>
<p>Events and their outcomes will appear here once created.</p>
</div>
{{end}}
{{if eq (print $d.FiscalYear.Status) "active"}}
<div style="margin-top:24px; padding:16px 20px; border-radius:var(--radius); background:rgba(251,191,36,0.08); border:1px solid rgba(251,191,36,0.3);">
<p style="font-size:13px; color:#fbbf24; margin:0;">⚠ This fiscal year is still active. The report will be final once the year is closed. Team feedback can be added after closing.</p>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,311 @@
{{define "content"}}
{{$d := .}}
{{$req := $d.Request}}
{{$isNew := eq $req.ID ""}}
{{$status := $req.CurrentStatus}}
{{$isManager := or (eq (print $d.MyRole) "admin") (eq (print $d.MyRole) "finance")}}
{{$isOwner := eq $req.SubmittedBy $d.UserID}}
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div>
<div style="font-size:12px; color:var(--text3); margin-bottom:4px;">
<a href="/orgs" style="color:var(--text3); text-decoration:none;">Orgs</a> /
<a href="/orgs/{{$d.Org.Slug}}" style="color:var(--text3); text-decoration:none;">{{$d.Org.Name}}</a> /
<a href="/orgs/{{$d.Org.Slug}}/requests" style="color:var(--text3); text-decoration:none;">Requests</a> /
{{if $isNew}}New{{else}}{{$req.Type}}{{end}}
</div>
<h1>{{if $isNew}}New Request{{else}}{{$req.Type}} Request{{end}}</h1>
</div>
{{if not $isNew}}
<span style="font-size:12px; font-weight:700; padding:4px 14px; border-radius:20px; background:{{statusColor (print $status)}};">{{$status}}</span>
{{end}}
</div>
{{if $isNew}}
<!-- ── New request form ──────────────────────────────────────────────────── -->
<div class="card animate-on-scroll" style="max-width:600px;">
<form method="post" action="/orgs/{{$d.Org.Slug}}/requests/new" id="req-form">
<div style="margin-bottom:16px;">
<label class="form-label">Request type</label>
<select class="form-input" name="type" id="req-type" onchange="showFields(this.value)" required>
<option value="">— choose —</option>
<option value="reimbursement">Reimbursement — you already paid, want money back</option>
<option value="purchase_order">Purchase Order — request to buy something</option>
<option value="cash_advance">Cash Advance — get cash upfront to spend</option>
<option value="income">Income — record an expected or received payment</option>
<option value="budget_transfer">Budget Transfer — move budget between lines</option>
</select>
</div>
<div style="margin-bottom:16px;">
<label class="form-label">Description</label>
<textarea class="form-input" name="description" rows="2" placeholder="Brief description of this request" required></textarea>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px;">
<div>
<label class="form-label">Amount (€)</label>
<input class="form-input" type="text" name="amount" placeholder="0.00" required>
</div>
<div id="field-due-date" style="display:none;">
<label class="form-label">Due date</label>
<input class="form-input" type="date" name="due_date">
</div>
</div>
<div id="field-vendor" style="display:none; margin-bottom:16px;">
<label class="form-label">Vendor / supplier</label>
<input class="form-input" type="text" name="vendor" placeholder="Company or person name">
</div>
<div id="field-payer" style="display:none; margin-bottom:16px;">
<label class="form-label">Payer name</label>
<input class="form-input" type="text" name="payer_name" placeholder="Who is expected to pay">
</div>
<div id="field-payment-method" style="display:none; margin-bottom:16px;">
<label class="form-label">Payment method</label>
<select class="form-input" name="payment_method">
<option value="bank_transfer">Bank transfer</option>
<option value="cash">Cash</option>
<option value="card">Card</option>
<option value="mbway">MBWay</option>
<option value="other">Other</option>
</select>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:20px;">
<div>
<label class="form-label">Event (optional)</label>
<select class="form-input" name="event_id">
<option value="">— none —</option>
{{if $d.FiscalYear}}{{end}}
</select>
</div>
<div>
<label class="form-label">Team (optional)</label>
<select class="form-input" name="team_id">
<option value="">— none —</option>
</select>
</div>
</div>
<div style="display:flex; gap:10px;">
<button type="submit" class="btn btn-primary">Create request</button>
<a href="/orgs/{{$d.Org.Slug}}/requests" class="btn btn-outline">Cancel</a>
</div>
</form>
</div>
<script type="module">
function showFields(type) {
const show = (id, val) => document.getElementById(id).style.display = val ? 'block' : 'none';
show('field-vendor', ['reimbursement','purchase_order'].includes(type));
show('field-payer', type === 'income');
show('field-payment-method', ['reimbursement','cash_advance'].includes(type));
show('field-due-date', ['purchase_order','income'].includes(type));
}
window.showFields = showFields;
</script>
{{else}}
<!-- ── Request detail ────────────────────────────────────────────────────── -->
<div style="display:grid; grid-template-columns:2fr 1fr; gap:20px;">
<div>
<!-- Details card -->
<div class="card animate-on-scroll" style="margin-bottom:20px;">
<h2 style="margin-bottom:16px;">Details</h2>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; font-size:13px;">
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">AMOUNT</div>
<div style="font-size:20px; font-weight:700; color:var(--accent2);">{{cents $req.AmountCents}}</div>
</div>
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">SUBMITTED BY</div>
<div>{{$req.SubmitterEmail}}</div>
</div>
{{if $req.Vendor}}
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">VENDOR</div>
<div>{{$req.Vendor}}</div>
</div>
{{end}}
{{if $req.PaymentMethod}}
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">PAYMENT METHOD</div>
<div>{{$req.PaymentMethod}}</div>
</div>
{{end}}
{{if $d.Event}}
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">EVENT</div>
<div><a href="/orgs/{{$d.Org.Slug}}/years/{{$req.FiscalYearID}}/events/{{$req.EventID}}" style="color:var(--accent2); text-decoration:none;">{{$d.Event.Name}}</a></div>
</div>
{{end}}
{{if $d.Team}}
<div>
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">TEAM</div>
<div>{{$d.Team.Name}}</div>
</div>
{{end}}
<div style="grid-column:1/-1;">
<div style="color:var(--text3); font-size:11px; font-weight:700; letter-spacing:.06em; margin-bottom:4px;">DESCRIPTION</div>
<div style="color:var(--text2); line-height:1.6;">{{$req.Description}}</div>
</div>
</div>
</div>
<!-- PO Delivery -->
{{if and (eq (print $req.Type) "purchase_order") (eq (print $status) "ordered")}}
<div class="card animate-on-scroll" style="margin-bottom:20px; border-color:#fbbf24;">
<h2 style="margin-bottom:14px;">Record Delivery</h2>
<form method="post" action="/orgs/{{$d.Org.Slug}}/requests/{{$req.ID}}/delivery">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px;">
<div>
<label class="form-label">Actual amount (€)</label>
<input class="form-input" type="text" name="actual_amount" placeholder="{{cents $req.AmountCents}}" required>
</div>
<div>
<label class="form-label">Actual vendor</label>
<input class="form-input" type="text" name="actual_vendor" value="{{$req.Vendor}}">
</div>
</div>
<div style="margin-bottom:14px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-size:13px;">
<input type="checkbox" name="store_changed" value="true"> Store/vendor changed from original
</label>
</div>
<div style="margin-bottom:16px;" id="change-note-wrap" style="display:none;">
<label class="form-label">Change note</label>
<input class="form-input" type="text" name="change_note" placeholder="What changed?">
</div>
<button type="submit" class="btn btn-primary">Confirm delivery</button>
</form>
</div>
{{end}}
{{if $req.Delivery}}
<div class="card animate-on-scroll" style="margin-bottom:20px;">
<h2 style="margin-bottom:12px;">Delivery record</h2>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; font-size:13px;">
<div><span style="color:var(--text3);">Actual amount:</span> {{cents $req.Delivery.ActualAmountCents}}</div>
<div><span style="color:var(--text3);">Vendor:</span> {{$req.Delivery.ActualVendor}}</div>
<div><span style="color:var(--text3);">Delivered:</span> {{dateShort $req.Delivery.DeliveredAt}}</div>
{{if $req.Delivery.StoreChanged}}<div style="grid-column:1/-1;"><span style="color:#fbbf24; font-size:11px; font-weight:700;">⚠ Store changed</span> — {{$req.Delivery.ChangeNote}}</div>{{end}}
</div>
</div>
{{end}}
<!-- Cash advance settlement -->
{{if and (eq (print $req.Type) "cash_advance") (or (eq (print $status) "disbursed") (eq (print $status) "settlement_due") (eq (print $status) "partial_settlement"))}}
<div class="card animate-on-scroll" style="margin-bottom:20px; border-color:#fbbf24;">
<h2 style="margin-bottom:14px;">Submit Settlement</h2>
<form method="post" action="/orgs/{{$d.Org.Slug}}/requests/{{$req.ID}}/settle">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px;">
<div>
<label class="form-label">Amount spent (€)</label>
<input class="form-input" type="text" name="amount_spent" required>
</div>
<div>
<label class="form-label">Amount returned (€)</label>
<input class="form-input" type="text" name="amount_returned" placeholder="0.00">
</div>
</div>
<button type="submit" class="btn btn-primary">Submit settlement</button>
</form>
</div>
{{end}}
{{if $req.Settlement}}
<div class="card animate-on-scroll" style="margin-bottom:20px;">
<h2 style="margin-bottom:12px;">Settlement record</h2>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; font-size:13px;">
<div><span style="color:var(--text3);">Spent:</span> {{cents $req.Settlement.AmountSpentCents}}</div>
<div><span style="color:var(--text3);">Returned:</span> {{cents $req.Settlement.AmountReturnedCents}}</div>
<div><span style="color:var(--text3);">Settled at:</span> {{dateShort $req.Settlement.SettledAt}}</div>
</div>
</div>
{{end}}
<!-- Attachments -->
<div class="card animate-on-scroll" style="margin-bottom:20px;">
<h2 style="margin-bottom:12px;">Attachments</h2>
{{if $d.Attachments}}
<div style="display:flex; flex-direction:column; gap:8px; margin-bottom:12px;">
{{range $d.Attachments}}
<div style="display:flex; justify-content:space-between; align-items:center; padding:8px 12px; background:var(--bg3); border-radius:var(--radius-sm);">
<span style="font-size:13px;">{{.Filename}}</span>
<span style="font-size:11px; color:var(--text3);">{{.MimeType}}</span>
</div>
{{end}}
</div>
{{else}}
<p style="font-size:13px; color:var(--text3); margin-bottom:12px;">No attachments yet.</p>
{{end}}
</div>
<!-- Status log -->
<div class="card animate-on-scroll">
<h2 style="margin-bottom:14px;">Status history</h2>
<div style="display:flex; flex-direction:column; gap:10px;">
{{range $req.StatusLog}}
<div style="display:flex; gap:12px; align-items:flex-start;">
<div style="width:8px; height:8px; border-radius:50%; background:var(--accent); margin-top:5px; flex-shrink:0;"></div>
<div style="flex:1;">
<div style="display:flex; gap:8px; align-items:center; margin-bottom:2px;">
<span style="font-size:12px; font-weight:700; padding:1px 7px; border-radius:20px; background:{{statusColor (print .Status)}};">{{.Status}}</span>
<span style="font-size:11px; color:var(--text3);">{{dateShort .ChangedAt}} · {{.ChangedBy}}</span>
</div>
{{if .Comment}}<p style="font-size:13px; color:var(--text2); margin:4px 0 0; line-height:1.5;">{{.Comment}}</p>{{end}}
</div>
</div>
{{end}}
</div>
</div>
</div>
<!-- Sidebar actions -->
<div>
<div class="card animate-on-scroll">
<h2 style="margin-bottom:14px; font-size:14px;">Actions</h2>
<form method="post" action="/orgs/{{$d.Org.Slug}}/requests/{{$req.ID}}/action" style="display:flex; flex-direction:column; gap:8px;">
<div>
<label class="form-label" style="font-size:11px;">Comment</label>
<textarea class="form-input" name="comment" rows="2" placeholder="Optional note…" style="font-size:12px;"></textarea>
</div>
{{if eq (print $status) "draft"}}
{{if $isOwner}}<button name="action" value="submit" class="btn btn-primary" style="width:100%;">Submit for review</button>{{end}}
<button name="action" value="cancel" class="btn btn-outline" style="width:100%; color:var(--red);" onclick="return confirm('Cancel this request?')">Cancel</button>
{{end}}
{{if eq (print $status) "submitted"}}
{{if $isManager}}<button name="action" value="review" class="btn btn-primary" style="width:100%;">Start review</button>{{end}}
<button name="action" value="cancel" class="btn btn-outline" style="width:100%; color:var(--red);" onclick="return confirm('Cancel?')">Cancel</button>
{{end}}
{{if eq (print $status) "info_requested"}}
{{if $isOwner}}<button name="action" value="submit" class="btn btn-primary" style="width:100%;">Resubmit</button>{{end}}
{{end}}
{{if eq (print $status) "under_review"}}
{{if $isManager}}
<button name="action" value="approve" class="btn btn-primary" style="width:100%;" onclick="return confirm('Approve?')">Approve</button>
<button name="action" value="request_info" class="btn btn-outline" style="width:100%;">Request info</button>
<button name="action" value="reject" class="btn btn-sm" style="width:100%; color:var(--red); background:transparent; border:1px solid var(--red);" onclick="return confirm('Reject?')">Reject</button>
{{end}}
{{end}}
{{if eq (print $status) "approved"}}
{{if $isManager}}
{{if eq (print $req.Type) "reimbursement"}}<button name="action" value="mark_paid" class="btn btn-primary" style="width:100%;">Mark as paid</button>{{end}}
{{if eq (print $req.Type) "purchase_order"}}<button name="action" value="mark_ordered" class="btn btn-primary" style="width:100%;">Mark as ordered</button>{{end}}
{{if eq (print $req.Type) "cash_advance"}}<button name="action" value="disburse" class="btn btn-primary" style="width:100%;">Disburse cash</button>{{end}}
{{if eq (print $req.Type) "income"}}<button name="action" value="mark_pending_payment" class="btn btn-primary" style="width:100%;">Mark pending payment</button>{{end}}
{{if eq (print $req.Type) "budget_transfer"}}<button name="action" value="done" class="btn btn-primary" style="width:100%;">Mark done</button>{{end}}
{{end}}
{{end}}
{{if eq (print $status) "disbursed"}}
{{if $isManager}}<button name="action" value="settlement_due" class="btn btn-outline" style="width:100%;">Request settlement</button>{{end}}
{{end}}
{{if eq (print $status) "pending_payment"}}
{{if $isManager}}<button name="action" value="mark_received" class="btn btn-primary" style="width:100%;">Mark as received</button>{{end}}
{{end}}
{{if or (eq (print $status) "paid") (eq (print $status) "delivered") (eq (print $status) "received") (eq (print $status) "settled")}}
{{if $isManager}}<button name="action" value="reconcile" class="btn btn-outline" style="width:100%;">Mark reconciled</button>{{end}}
{{end}}
</form>
</div>
</div>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,75 @@
{{define "content"}}
{{$d := .}}
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div>
<div style="font-size:12px; color:var(--text3); margin-bottom:4px;">
<a href="/orgs" style="color:var(--text3); text-decoration:none;">Orgs</a> /
<a href="/orgs/{{$d.Org.Slug}}" style="color:var(--text3); text-decoration:none;">{{$d.Org.Name}}</a> / Requests
</div>
<h1>Transaction Requests</h1>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a href="/orgs/{{$d.Org.Slug}}/ledger" class="btn btn-outline btn-sm">Ledger</a>
<a href="/orgs/{{$d.Org.Slug}}/requests/new" class="btn btn-primary btn-sm">+ New request</a>
</div>
</div>
<!-- Status filter tabs -->
{{$cur := $d.StatusFilter}}
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:20px; font-size:12px; font-weight:600;">
<a href="?"style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur ""}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">All</a>
<a href="?status=draft" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "draft"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">draft</a>
<a href="?status=submitted" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "submitted"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">submitted</a>
<a href="?status=under_review" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "under_review"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">under_review</a>
<a href="?status=info_requested" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "info_requested"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">info_requested</a>
<a href="?status=approved" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "approved"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">approved</a>
<a href="?status=rejected" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "rejected"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">rejected</a>
<a href="?status=paid" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "paid"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">paid</a>
<a href="?status=ordered" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "ordered"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">ordered</a>
<a href="?status=settled" style="padding:5px 12px; border-radius:20px; text-decoration:none; {{if eq $cur "settled"}}background:var(--accent);color:#000;{{else}}background:var(--bg3);color:var(--text3);{{end}}">settled</a>
</div>
{{if $d.Requests}}
<div class="card animate-on-scroll" style="padding:0; overflow:hidden;">
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
<th>By</th>
<th style="text-align:right;">Amount</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{{range $d.Requests}}
{{$status := .CurrentStatus}}
<tr>
<td>
<span style="font-size:11px; font-weight:700; padding:2px 7px; border-radius:4px; background:var(--bg3); color:var(--text2);">{{.Type}}</span>
</td>
<td style="max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13px;">{{.Description}}</td>
<td style="font-size:12px; color:var(--text3);">{{.SubmitterEmail}}</td>
<td style="text-align:right; font-size:13px; font-weight:600;">{{cents .AmountCents}}</td>
<td>
<span style="font-size:11px; font-weight:700; padding:2px 8px; border-radius:20px; background:{{statusColor (print $status)}};">{{$status}}</span>
</td>
<td style="text-align:right;">
<a href="/orgs/{{$d.Org.Slug}}/requests/{{.ID}}" class="btn btn-outline btn-sm">View</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="card empty-state animate-on-scroll">
<div style="font-size:36px; margin-bottom:12px;">📋</div>
<h3>No requests{{if $d.StatusFilter}} with status "{{$d.StatusFilter}}"{{end}}</h3>
<p style="margin-bottom:16px;">Submit transaction requests to track expenses, income, and transfers.</p>
<a href="/orgs/{{$d.Org.Slug}}/requests/new" class="btn btn-primary">New request</a>
</div>
{{end}}
{{end}}