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 { "isOver": func(spent, budget int64) bool {
return budget > 0 && spent > budget 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 { "jsonVals": func(m map[string]int64) template.JS {
var vals []string var vals []string
for _, v := range m { for _, v := range m {
@ -135,7 +164,15 @@ var (
orgTeamsTmpl = parseTmpl("templates/base.html", "templates/org_teams.html") orgTeamsTmpl = parseTmpl("templates/base.html", "templates/org_teams.html")
orgMembersTmpl = parseTmpl("templates/base.html", "templates/org_members.html") orgMembersTmpl = parseTmpl("templates/base.html", "templates/org_members.html")
orgInviteTmpl = parseTmpl("templates/base.html", "templates/org_invite.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 { type authInfo struct {
@ -244,6 +281,11 @@ type storeIface interface {
createTxRequest(ctx context.Context, r *TxRequest) error createTxRequest(ctx context.Context, r *TxRequest) error
appendStatusLog(ctx context.Context, reqID, orgID string, entry StatusLogEntry) error appendStatusLog(ctx context.Context, reqID, orgID string, entry StatusLogEntry) error
updateTxRequest(ctx context.Context, reqID, orgID string, update bson.M) 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 { 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) createTxRequest(_ context.Context, _ *TxRequest) error { return nil }
func (m *mockStore) appendStatusLog(_ context.Context, _, _ string, _ StatusLogEntry) 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) 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 ─────────────────────────────────────────────────────────────────── // ── helpers ───────────────────────────────────────────────────────────────────

View File

@ -354,3 +354,173 @@ type OrgInviteData struct {
Error string Error string
Link string // generated invite link shown after creation 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 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}}