using Inversion; using Spry; using Spry.Authentication; using Statum; using ClankerSaddle.Data; using ClankerSaddle.Services; namespace ClankerSaddle { /** Sealed note identity carried by the note row actions. */ public class NoteActionPrivate : Object { public int64 note_id { get; set; } } /** * Common base for actions sealed against a note id: materialises * the typed private blob and exposes the resolved id (falling * back to the hidden `note_id` form field). */ public abstract class SealedNoteAction : TypedStatumAction { protected override Type private_type { get { return typeof(NoteActionPrivate); } } /** The sealed note id, or 0 when neither blob nor form field carried one. */ protected int64 sealed_note_id() { var ctx = request_private; return ctx != null ? ((!)ctx).note_id : Forms.integer(request, "note_id", 0); } } /** * Common base for the note-row actions that answer with the * refreshed dashboard slot (toggle/delete — add and promote carry * no sealed id of their own, and add reuses the helper below): * owns the service set {@link DashboardState.build} needs, so a * mutation answers with refreshed state in ONE call. */ public abstract class DashboardRefreshingNoteAction : SealedNoteAction { protected TaskService task_service = inject(); protected SaddleService saddle_service = inject(); protected TimelineService timeline_service = inject(); protected InvercargillSql.Orm.OrmSession db = Inversion.inject(); protected PushService push_service = inject(); protected NoteService note_service = inject(); protected UserService users = inject(); protected ModelConfigService model_config = inject(); /** * Rebuilds the dashboard slot from current data — the same * build the page and its pulse refresh run. */ protected async Statum.Slot dashboard_slot() throws GLib.Error { var me = SpryAuth.current(request); return yield DashboardState.build(state_service, task_service, saddle_service, timeline_service, db, note_service, action_registry, push_service, me != null ? ((!)me).user_id : 0, model_config); } } /** * Adds a dashboard scratchpad note (the body is the whole note — * no project or assignment semantics). Answers with the * refreshed dashboard slot, like the page's own refresh. A plain * action (no sealed id — the form is not row-scoped). */ public class AddNoteAction : StatumAction { private NoteService notes = inject(); private TaskService task_service = inject(); private SaddleService saddle_service = inject(); private TimelineService timeline_service = inject(); private NoteService note_service = inject(); private InvercargillSql.Orm.OrmSession db = Inversion.inject(); private PushService push_service = inject(); private UserService users = inject(); private ModelConfigService model_config = inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } string body = Forms.field_raw(request, "body"); if (body.strip().length == 0) { return directives().notify("error", "A note body is required"); } try { yield notes.add_note(body); var me = SpryAuth.current(request); var state = yield DashboardState.build(state_service, task_service, saddle_service, timeline_service, db, note_service, action_registry, push_service, me != null ? ((!)me).user_id : 0, model_config); return directives().set(state); } catch (Error e) { return directives().notify("error", @"Could not add the note: $(e.message)"); } } } /** * Toggles a note's done flag (the checkbox on the row). The id * comes from the sealed {@link NoteActionPrivate} blob; answers * with the refreshed dashboard slot. */ public class ToggleNoteAction : DashboardRefreshingNoteAction { private NoteService notes = inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } int64 note_id = sealed_note_id(); if (note_id <= 0) { return directives().navigate("/"); } try { // Flip (the row button carries no direction field): an // explicit "0"/"1" form value still forces a direction. var current = yield notes.get_note(note_id); if (current == null) { return directives().notify("error", "No such note").navigate("/"); } string direction = Forms.field(request, "done"); bool target = direction.length > 0 ? direction == "1" : !((!)current).done; yield notes.set_done(note_id, target); return directives().set(yield dashboard_slot()); } catch (Error e) { return directives().notify("error", @"Could not update the note: $(e.message)"); } } } /** * Deletes a note. The id comes from the sealed private blob; * answers with the refreshed dashboard slot. */ public class DeleteNoteAction : DashboardRefreshingNoteAction { private NoteService notes = inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } int64 note_id = sealed_note_id(); if (note_id <= 0) { return directives().navigate("/"); } try { yield notes.delete_note(note_id); return directives().set(yield dashboard_slot()); } catch (Error e) { return directives().notify("error", @"Could not delete the note: $(e.message)"); } } } /** * Promotes a note into a DRAFT goal (goal 8 task 3): title = the * note's first line, brief = the full note body, on the chosen * project with the chosen branch. The note is stamped with the * goal id (frozen — no further promote) and the human lands on * the goal's draft page to finish it (links, base branch, start). */ public class PromoteNoteAction : SealedNoteAction { private NoteService notes = inject(); private UserService users = inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } int64 note_id = sealed_note_id(); if (note_id <= 0) { return directives().navigate("/"); } try { var goal = yield notes.promote_to_goal(note_id, Forms.integer(request, "project_id", 0), Forms.field(request, "branch_name")); return directives() .notify("success", @"Goal '$(goal.title)' created in draft from note #$(note_id) — set it up and press start") .navigate(@"/goals/$(goal.id)"); } catch (Error e) { return directives() .notify("error", @"Could not create the goal from note #$(note_id): $(e.message)") .navigate("/"); } } } }