using Invercargill; using Invercargill.DataStructures; using Spry; using Spry.Authentication; using Statum; using ClankerSaddle.Data; using ClankerSaddle.Services; namespace ClankerSaddle { /** A project row on the projects list, with task counts and a link to its page. */ public class ProjectRowView : Object { public int64 id { get; set; } public string name { get; set; default = ""; } public string git_url { get; set; default = ""; } public string default_branch { get; set; default = ""; } public string summary { get; set; default = ""; } public int64 open_tasks { get; set; } public int64 done_tasks { get; set; } } /** Public data of the `projects` slot. */ public class ProjectsPublic : Object { public Statum.Model.ActionDto create { get; set; } } /** * Entrypoint for /projects: lists every project with open/done task * counts and authors the create-project action (login required). */ public class ProjectsEntrypoint : StatumEntrypoint { private ProjectService project_service = Inversion.inject(); private TaskService task_service = Inversion.inject(); private UserService users = Inversion.inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } var rows = new Vector(); var tasks = yield task_service.list_tasks(null, null); foreach (var project in yield project_service.list_projects()) { var view = new ProjectRowView() { id = project.id, name = project.name, git_url = project.git_url, default_branch = project.default_branch, summary = project.summary ?? "" }; foreach (var task in tasks) { if (task.project_id != project.id) { continue; } if (task.status == STATUS_DONE || task.status == STATUS_FAILED) { view.done_tasks++; } else { view.open_tasks++; } } rows.add(view); } var pub = new ProjectsPublic(); pub.create = action_registry.author(); var props = (PropertyDictionary) GObjectMapping.to_properties(pub); props["projects"] = StateJson.list(rows.to_immutable_buffer()); var slot = state_service.new_slot(Scope.PAGE, new State() { type_name = "projects", public_data = props, private_data = new PropertyDictionary() }); return directives().set(slot); } } /** A linked project on the project detail page, with a remove action. */ public class ProjectLinkView : Object { public int64 link_id { get; set; } public int64 project_id { get; set; } public string name { get; set; default = ""; } public string git_url { get; set; default = ""; } public string relationship { get; set; default = ""; } public bool co_developed { get; set; } public Statum.Model.ActionDto? remove { get; set; } } /** A task row on the project detail page. */ public class ProjectTaskRowView : Object { public int64 id { get; set; } public string title { get; set; default = ""; } public string status { get; set; default = ""; } public string display { get; set; default = ""; } public int64 priority { get; set; } } /** A rule attached to the project, with its detach action. */ public class ProjectRuleRowView : Object { public int64 link_id { get; set; } public int64 rule_id { get; set; } public string name { get; set; default = ""; } public string body { get; set; default = ""; } public Statum.Model.ActionDto? detach { get; set; } } /** A rule option in the attach selects (project and task pages). */ public class RuleOptionView : Object { public int64 id { get; set; } public string name { get; set; default = ""; } } /** * A project note on the project page: LLM-authored via `saddle * note`, with its date, source task link and a delete action (no * manual create — notes are user-curated, not user-written). */ public class ProjectNoteRowView : Object { public int64 id { get; set; } public string project_name { get; set; default = ""; } public int64 source_task_id { get; set; } public string body { get; set; default = ""; } public string created_display { get; set; default = ""; } public Statum.Model.ActionDto? delete { get; set; } } /** Public data of the `project` slot. */ public class ProjectDetailPublic : Object { public bool exists { get; set; } public int64 id { get; set; } public string name { get; set; default = ""; } public string git_url { get; set; default = ""; } public string default_branch { get; set; default = ""; } public string summary { get; set; default = ""; } public string created_display { get; set; default = ""; } public Statum.Model.ActionDto? update { get; set; } public Statum.Model.ActionDto? add_link { get; set; } public Statum.Model.ActionDto? attach_rule { get; set; } } /** * Entrypoint for /projects/{project_id}: the project's editable card, * its co-developed/reference links (with remove actions), its rules * (with detach actions and the attach form), the other projects * available for linking, and its task list. */ public class ProjectDetailEntrypoint : StatumEntrypoint { private ProjectService project_service = Inversion.inject(); private TaskService task_service = Inversion.inject(); private RuleService rule_service = Inversion.inject(); private UserService users = Inversion.inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } int64 project_id = int64.parse(request.route("project_id") ?? "0"); var project = yield project_service.get_project(project_id); if (project == null) { return directives().set_typed("project", Scope.PAGE, new ProjectDetailPublic() { exists = false }); } var sealed = new ProjectActionPrivate() { project_id = project_id }; var pub = new ProjectDetailPublic() { exists = true, id = ((!)project).id, name = ((!)project).name, git_url = ((!)project).git_url, default_branch = ((!)project).default_branch, summary = ((!)project).summary ?? "", created_display = ((!)project).created_at != null ? ((!)((!)project).created_at).format("%Y-%m-%d %H:%M") : "", update = action_registry.author_private(sealed), add_link = action_registry.author_private(sealed), attach_rule = action_registry.author_private(sealed) }; var links = new Vector(); foreach (var link in yield project_service.list_links(project_id)) { var linked = yield project_service.get_project(link.linked_project_id); if (linked == null) { continue; } var private_blob = new ProjectLinkActionPrivate() { project_id = project_id, link_id = link.id }; links.add(new ProjectLinkView() { link_id = link.id, project_id = link.linked_project_id, name = ((!)linked).name, git_url = ((!)linked).git_url, relationship = link.relationship, co_developed = link.relationship == LINK_CO_DEVELOPED, remove = action_registry.author_private(private_blob) }); } var linkable = new Vector(); foreach (var other in yield project_service.list_projects()) { if (other.id != project_id) { linkable.add(new ProjectRowView() { id = other.id, name = other.name, git_url = other.git_url, default_branch = other.default_branch, summary = other.summary ?? "" }); } } var project_tasks = yield task_service.list_tasks(null, project_id); var task_display = yield task_service.display_map_for(project_tasks); var task_rows = new Vector(); foreach (var task in project_tasks) { var display = task_display.get_or_default(task.id.to_string()); task_rows.add(new ProjectTaskRowView() { id = task.id, title = task.title, status = task.status, display = display != null ? ((!)display).status : task.status, priority = task.priority, }); } var rule_rows = new Vector(); foreach (var link in yield rule_service.list_project_attachments(project_id)) { var rule = yield rule_service.get_rule(link.rule_id); if (rule == null) { continue; } var private_blob = new ProjectRuleActionPrivate() { project_id = project_id, link_id = link.id }; rule_rows.add(new ProjectRuleRowView() { link_id = link.id, rule_id = link.rule_id, name = ((!)rule).name, body = ((!)rule).body, detach = action_registry.author_private(private_blob) }); } var rule_options = new Vector(); foreach (var rule in yield rule_service.list_rules()) { rule_options.add(new RuleOptionView() { id = rule.id, name = rule.name }); } var note_rows = new Vector(); foreach (var note in yield project_service.list_notes(project_id)) { var note_private = new ProjectNoteActionPrivate() { project_id = project_id, note_id = note.id }; note_rows.add(new ProjectNoteRowView() { id = note.id, project_name = ((!)project).name, source_task_id = note.source_task_id, body = note.body, created_display = note.created_at != null ? ((!)note.created_at).format("%Y-%m-%d %H:%M") : "", delete = action_registry.author_private(note_private) }); } var props = (PropertyDictionary) GObjectMapping.to_properties(pub); props["links"] = StateJson.list(links.to_immutable_buffer()); props["linkable"] = StateJson.list(linkable.to_immutable_buffer()); props["tasks"] = StateJson.list(task_rows.to_immutable_buffer()); props["rules"] = StateJson.list(rule_rows.to_immutable_buffer()); props["rule_options"] = StateJson.list(rule_options.to_immutable_buffer()); props["notes"] = StateJson.list(note_rows.to_immutable_buffer()); var slot = state_service.new_slot(Scope.PAGE, new State() { type_name = "project", public_data = props, private_data = new PropertyDictionary() }); return directives().set(slot); } } }