From 07e3525dae4f4d5085b993f465c434dbe2a7f2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rodrigues?= <95761178+GoncaloRodri@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:00:23 +0100 Subject: [PATCH] =?UTF-8?q?fix(finance):=20org=20polish=20=E2=80=94=20brok?= =?UTF-8?q?en=20links,=20upload=20handler,=20populated=20selects=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - org_home.html: fix Requests/Ledger links (were pointing to non-existent /years/{id}/requests routes; corrected to /orgs/{slug}/requests and /orgs/{slug}/ledger). Add Analysis link. Add Close year button for admins. - OrgRequestNew: pass events + teams to GET template so dropdowns are populated (were silently discarded with _ = events). - OrgRequestDetailData: add NewEvents/NewTeams fields for new-request form. - OrgRequestUpload: implement file upload handler — saves to /data/org-files/{org_id}/{req_id}/{id} and records metadata in MongoDB. Register POST /orgs/{slug}/requests/{req_id}/upload route. - org_request_detail.html: show upload form in attachments section; populate event/team selects on new request form. Co-authored-by: Gonçalo Rodrigues Co-authored-by: Claude Sonnet 4.6 --- apps/finance/services/api/main/handler_org.go | 74 ++++++++++++++++++- apps/finance/services/api/main/models_org.go | 3 + .../services/api/main/templates/org_home.html | 15 +++- .../main/templates/org_request_detail.html | 20 ++++- 4 files changed, 101 insertions(+), 11 deletions(-) diff --git a/apps/finance/services/api/main/handler_org.go b/apps/finance/services/api/main/handler_org.go index 3a29ab3..4a138d7 100644 --- a/apps/finance/services/api/main/handler_org.go +++ b/apps/finance/services/api/main/handler_org.go @@ -8,6 +8,7 @@ import ( "io" "log/slog" "net/http" + "os" "regexp" "strings" "time" @@ -1027,10 +1028,9 @@ func (h *Handler) OrgRequestNew(w http.ResponseWriter, r *http.Request) { Org: *org, MyRole: me.Role, FiscalYear: activeYear, - // Use Request.ID="" to signal "new form" in template + NewEvents: events, + NewTeams: teams, }) - _ = events - _ = teams return } @@ -1387,6 +1387,73 @@ func (h *Handler) OrgRequestSettle(w http.ResponseWriter, r *http.Request) { })(w, r) } +func (h *Handler) OrgRequestUpload(w http.ResponseWriter, r *http.Request) { + h.requireOrgMember(func(w http.ResponseWriter, r *http.Request, org *Org, me *OrgMember) { + ctx := r.Context() + reqID := r.PathValue("req_id") + + if _, err := h.store.getTxRequest(ctx, reqID, org.ID); err != nil { + http.Error(w, "request not found", http.StatusNotFound) + return + } + + if err := r.ParseMultipartForm(20 << 20); err != nil { + http.Error(w, "file too large (max 20 MB)", http.StatusBadRequest) + return + } + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, "file required", http.StatusBadRequest) + return + } + defer file.Close() + + attachID := bson.NewObjectID().Hex() + dir := fmt.Sprintf("/data/org-files/%s/%s", org.ID, reqID) + if err := os.MkdirAll(dir, 0o750); err != nil { + slog.Error("mkdir attachment dir", "err", err) + http.Error(w, "storage error", http.StatusInternalServerError) + return + } + storagePath := fmt.Sprintf("%s/%s", dir, attachID) + dst, err := os.Create(storagePath) + if err != nil { + slog.Error("create attachment file", "err", err) + http.Error(w, "storage error", http.StatusInternalServerError) + return + } + defer dst.Close() + size, err := io.Copy(dst, file) + if err != nil { + slog.Error("write attachment file", "err", err) + http.Error(w, "storage error", http.StatusInternalServerError) + return + } + + mime := header.Header.Get("Content-Type") + if mime == "" { + mime = "application/octet-stream" + } + attach := &OrgAttachment{ + ID: attachID, + OrgID: org.ID, + RequestID: reqID, + UploadedBy: me.ID, + UploadedAt: time.Now(), + Filename: header.Filename, + MimeType: mime, + SizeBytes: size, + StoragePath: storagePath, + } + if err := h.store.createAttachment(ctx, attach); err != nil { + slog.Error("save attachment metadata", "err", err) + http.Error(w, "could not save attachment", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/orgs/"+org.Slug+"/requests/"+reqID, http.StatusSeeOther) + })(w, r) +} + // ── Ledger ──────────────────────────────────────────────────────────────────── func (h *Handler) OrgLedger(w http.ResponseWriter, r *http.Request) { @@ -1807,6 +1874,7 @@ func (h *Handler) RegisterOrgRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /orgs/{slug}/requests/{req_id}/action", h.OrgRequestAction) mux.HandleFunc("POST /orgs/{slug}/requests/{req_id}/delivery", h.OrgRequestDelivery) mux.HandleFunc("POST /orgs/{slug}/requests/{req_id}/settle", h.OrgRequestSettle) + mux.HandleFunc("POST /orgs/{slug}/requests/{req_id}/upload", h.OrgRequestUpload) // Ledger (literal "import" before potential wildcards) mux.HandleFunc("GET /orgs/{slug}/ledger", h.OrgLedger) diff --git a/apps/finance/services/api/main/models_org.go b/apps/finance/services/api/main/models_org.go index 337bbd2..21daf46 100644 --- a/apps/finance/services/api/main/models_org.go +++ b/apps/finance/services/api/main/models_org.go @@ -421,6 +421,9 @@ type OrgRequestDetailData struct { FiscalYear *FiscalYear Attachments []OrgAttachment Error string + // populated on new-request GET + NewEvents []OrgEvent + NewTeams []OrgTeam } type OrgLedgerData struct { diff --git a/apps/finance/services/api/main/templates/org_home.html b/apps/finance/services/api/main/templates/org_home.html index fe699f3..9077239 100644 --- a/apps/finance/services/api/main/templates/org_home.html +++ b/apps/finance/services/api/main/templates/org_home.html @@ -43,10 +43,17 @@ {{dateShort $d.ActiveYear.StartDate}} — {{dateShort $d.ActiveYear.EndDate}} -
- Events - Requests - Ledger +
+ Events + Analysis + Requests + Ledger + {{if eq $d.MyRole "admin"}} +
+ +
+ {{end}}
diff --git a/apps/finance/services/api/main/templates/org_request_detail.html b/apps/finance/services/api/main/templates/org_request_detail.html index e982374..6adbd63 100644 --- a/apps/finance/services/api/main/templates/org_request_detail.html +++ b/apps/finance/services/api/main/templates/org_request_detail.html @@ -73,13 +73,18 @@
@@ -224,17 +229,24 @@ window.showFields = showFields;

Attachments

{{if $d.Attachments}} -
+
{{range $d.Attachments}}
- {{.Filename}} + 📎 {{.Filename}} {{.MimeType}}
{{end}}
{{else}} -

No attachments yet.

+

No attachments yet.

{{end}} + {{if ne (print $status) "reconciled"}}{{if ne (print $status) "cancelled"}}{{if ne (print $status) "rejected"}} +
+ + +
+ {{end}}{{end}}{{end}}