using Invercargill; using Invercargill.DataStructures; using Invercargill.Expressions; using InvercargillSql.Orm; using Inversion; using ClankerSaddle.Data; namespace ClankerSaddle.Services { /** * The dashboard scratchpad (goal 8 task 3): personal notes/todo * rows for jotting down batches of changes BEFORE they are ready * to be full-on goals or tasks. `notes` carries no project (or * any other) semantics — it is the human's own list — and a note * promotes into a DRAFT goal with the body as the brief, after * which it is frozen and linked to that goal. */ public class NoteService : Object { private OrmSession db = inject(); private GoalService goals = inject(); private StatePulse pulse = inject(); /** Loads the note identified by `id`, or null. */ public async Note? get_note(int64 id) throws Error { return yield db.query() .where(expr("id == $0", elem(id))) .first_async(); } /** * Inserts a note. The body is trimmed and must be non-empty; * a note starts undone and unpromoted. */ public async Note add_note(string body) throws Error { string trimmed = body.strip(); if (trimmed.length == 0) { throw new Error(Quark.from_string("clanker-saddle"), 95, "A note body is required"); } var note = new Note() { body = trimmed, done = false, created_at = new DateTime.now_local() }; yield db.insert_async(note); yield pulse.bump(0, "note added"); return note; } /** * Every note, undone first, then newest first (the sort runs * in memory — the ORM's order_by carries a single key). * Promotion freezes a note but does not finish it, so a * promoted note keeps its place by its own done flag. */ public async Enumerable list_notes() throws Error { var rows = yield db.query() .order_by_desc(expr("id")) .materialise_async(); return rows.sort((a, b) => { if (a.done != b.done) { return a.done ? 1 : -1; } return a.id > b.id ? -1 : 1; }); } /** * Sets (or clears) a note's done flag, stamping done_at on * the transition. Returns the saved note, or null when no * such note exists. A promoted note stays toggleable — done * still means "I dealt with this", promotion only locks the * create-goal path. */ public async Note? set_done(int64 note_id, bool done) throws Error { var note = yield get_note(note_id); if (note == null) { return null; } if (((!)note).done != done) { ((!)note).done = done; ((!)note).done_at = done ? new DateTime.now_local() : null; yield db.update_async((!)note); yield pulse.bump(0, "note toggled"); } return note; } /** Deletes the note (idempotent: an absent note is fine). */ public async void delete_note(int64 note_id) throws Error { var note = yield get_note(note_id); if (note == null) { return; } yield db.delete_async((!)note); yield pulse.bump(0, "note deleted"); } /** * Derives the promote-to-goal title from the note body: the * first non-empty line, clamped to ~80 characters (the clamp * appends an ellipsis when it actually cut anything). */ public static string title_from_body(string body) { string first_line = ""; foreach (string line in body.split("\n")) { if (line.strip().length > 0) { first_line = line.strip(); break; } } if (first_line.char_count() > 80) { first_line = first_line.substring(0, (long) first_line.index_of_nth_char(80)) + "…"; } return first_line.length > 0 ? first_line : "Note goal"; } /** * Promotes an UNDONE, unpromoted note into a DRAFT goal on * `project_id` (title = {@link title_from_body}, brief = the * full note body, branch = `branch_name`), stamps the note's * goal_id and returns the created goal. The human lands on * the goal's page to set links/branch details and press * start. A promoted (or done) note refuses — the row is * frozen once its goal exists. */ public async Goal promote_to_goal(int64 note_id, int64 project_id, string branch_name) throws Error { var note = yield get_note(note_id); if (note == null) { throw new Error(Quark.from_string("clanker-saddle"), 96, @"No note exists with id $(note_id)"); } if (((!)note).goal_id > 0) { throw new Error(Quark.from_string("clanker-saddle"), 97, @"Note #$(note_id) already promoted to goal #$(((!)note).goal_id)"); } if (((!)note).done) { throw new Error(Quark.from_string("clanker-saddle"), 98, @"Note #$(note_id) is done — only open notes promote to goals"); } var goal = yield goals.create_goal(project_id, title_from_body(((!)note).body), ((!)note).body, branch_name, null, 3); ((!)note).goal_id = goal.id; yield db.update_async((!)note); yield pulse.bump(0, "note promoted"); return goal; } } }