package model import "time" // Feedback is a user-submitted message — bug report, feature request, // or general comment — captured by the /feedback page. Surfaced to // admins both via the /stats page and via an email fan-out on insert. // // UserID is a pointer so the model can carry anonymous submissions // (logged-out visitors) and orphaned rows (the submitter has since // deleted their account — the FK is set null). ContactEmail is the // optional reply-to filled in by anonymous submitters. // // AuthorHandle / AuthorName / AuthorEmail are populated by joins in // the repo's lookup helpers (used by the admin email template and the // /stats page). They're nil for anonymous rows. type Feedback struct { ID string `json:"id"` UserID *string `json:"user_id,omitempty"` Category string `json:"category"` // CategoryLabel is the category's current display name (falls back to // the key if the category was ever removed). CategoryLabel string `json:"category_label"` Message string `json:"message"` ContactEmail *string `json:"contact_email,omitempty"` CreatedAt time.Time `json:"created_at"` // AcknowledgedAt is set when an admin presses "Acknowledge" on the // /stats page — the team has seen it and (if it needs work) the devs // have started. NULL until then. One-way like ResolvedAt. AcknowledgedAt *time.Time `json:"acknowledged_at,omitempty"` // ResolvedAt is set the moment an admin presses "Mark resolved" on // the /stats page. NULL while open. The action is one-way — the // repo's MarkResolved guards on `resolved_at IS NULL` so a second // resolve is a no-op. ResolvedAt *time.Time `json:"resolved_at,omitempty"` // ReplyMessage is the admin's written reply to the submitter, sent // (and stored) from the admin panel. NULL until an admin replies. // RepliedAt stamps when it was sent. Surfaced to the submitter on the // "My feedback" page as the team's response. ReplyMessage *string `json:"reply_message,omitempty"` RepliedAt *time.Time `json:"replied_at,omitempty"` // HasContactEmail reports *whether* an anonymous submitter left a reply-to // address, without carrying the address itself. Populated only by // Recent(), whose sole consumer is the PUBLIC /stats page — that page has // to distinguish "anonymous, but we can reply" from "anonymous dead end" // while never handling the email. Admins see the real address in the // admin panel, which is auth-gated. HasContactEmail bool `json:"-"` AuthorHandle *string `json:"author_handle,omitempty"` AuthorName *string `json:"author_name,omitempty"` AuthorEmail *string `json:"-"` // server-side only; never serialised } // FeedbackCategory is one option on the /feedback form, managed from the // admin panel. Key is what feedback.category stores and never changes. type FeedbackCategory struct { Key string `json:"key"` Label string `json:"label"` Description string `json:"description"` SortOrder int `json:"sort_order"` ArchivedAt *time.Time `json:"archived_at,omitempty"` // UsageCount is admin-only: how much feedback is filed under it. UsageCount int `json:"usage_count,omitempty"` }