Context for projects, tasks, assignments, and dependencies.
Summary
Types
Atom-shaped error returned for not-found / missing-resource cases.
UUIDv7 string or raw 16-byte binary (Ecto accepts either).
Functions
Adds an assignment-level dependency and broadcasts :dependency_added.
Adds a template-level dependency from one task to another.
When an assignment is created, auto-create assignment-level dependencies from the task template's defaults (linking to sibling assignments already in the same project).
Counts of assignments by status across active non-template projects. Returns a map like %{"todo" => 5, "in_progress" => 2, "done" => 10}.
Assignments in this project that the given assignment does NOT yet depend on.
Tasks that the given task does not yet depend on (for the dependency picker).
Returns a changeset for the given assignment.
Returns a changeset for the given project.
Returns a changeset for the given task.
Marks an assignment done, stamping completed_by_uuid and completed_at.
Total number of projects (including templates).
Total number of tasks in the library.
Total number of template projects.
Inserts an assignment and broadcasts :assignment_created.
Creates an assignment pre-populated from the task template's defaults (description, duration, default assignee). The caller's attrs override any template values.
Inserts a project and broadcasts :project_created.
Creates a new project by cloning a template. Copies all assignments (with their task links, descriptions, durations, assignees, weekends settings) and re-creates dependencies between the cloned assignments.
Inserts a task and broadcasts :task_created.
Deletes an assignment and broadcasts :assignment_deleted.
Deletes a project and broadcasts :project_deleted.
Deletes a task and broadcasts :task_deleted.
Check if all dependencies of an assignment are done.
Fetches an assignment by uuid with related records preloaded, or nil if not found.
Fetches a project by uuid, or nil if not found.
Fetches a project by uuid. Raises if not found.
Fetches a task by uuid, or nil if not found.
Fetches a task by uuid. Raises if not found.
Active, in-flight projects (started but not yet completed).
All dependencies across every assignment in a project (used when cloning templates).
Lists assignments within a project, ordered by position, with related records preloaded.
Assignments currently assigned to the given user's staff record. Returns non-done assignments across all active projects, with project preloaded.
Dependencies declared on a single assignment.
Lists projects. Accepts :status filter and :include_templates (default false).
Completed projects (all tasks done), most recently completed first.
Projects not yet started, in setup (immediate mode, not scheduled).
Template-level dependencies declared on the given task.
Lists all task-library entries, preloaded with defaults.
Lists projects that are templates, ordered by name.
Scheduled projects waiting to start.
Batched summaries for many projects — loads all their assignments in one query, then groups in memory. Preserves the input project order.
Summary of an active project for the overview dashboard. Returns a map with progress stats.
Called after an assignment status change. Checks whether all assignments
in the project are done. If so, sets completed_at. If not (e.g., a task
was reopened), clears it. Returns the (possibly updated) project.
Removes an assignment-level dependency and broadcasts :dependency_removed.
Removes a template-level dependency. Returns {:error, :not_found} if missing.
Reverts an assignment to todo and clears its completion fields.
Stamps started_at on the project and broadcasts :project_started.
Form-safe update for user-submitted attrs. Does NOT apply completion
fields (completed_by_uuid, completed_at) even if they appear in
attrs — they are silently dropped by Assignment.changeset/2.
Server-trusted update that additionally casts completed_by_uuid and
completed_at. Only call from server code (never pass raw form attrs),
since the caller vouches for those fields.
Updates a project and broadcasts :project_updated.
Updates a task and broadcasts :task_updated.
Types
@type error_atom() :: :not_found | :template_not_found | :task_not_found
Atom-shaped error returned for not-found / missing-resource cases.
@type uuid() :: String.t() | <<_::128>>
UUIDv7 string or raw 16-byte binary (Ecto accepts either).
Functions
@spec add_dependency(uuid(), uuid()) :: {:ok, PhoenixKitProjects.Schemas.Dependency.t()} | {:error, Ecto.Changeset.t()}
Adds an assignment-level dependency and broadcasts :dependency_added.
Rejects any edge that would introduce a cycle — i.e., if depends_on_uuid
already (transitively) depends on assignment_uuid, the insert is
refused with a changeset error. The schema-level self-reference check
handles the A == B case; this function handles multi-hop cycles
(A → B, then B → A).
The cycle check + insert run inside a :serializable transaction.
Without this, two concurrent calls — add_dependency(A, B) and
add_dependency(B, A) — could each read an acyclic graph, both
pass the check, both insert, and produce a cycle (the unique pair
index doesn't catch this; it only rejects identical duplicate
edges). At :serializable Postgres aborts the loser with
serialization_failure (SQLSTATE 40001); we catch that and
return a friendly changeset error so the caller can retry.
When called from inside another transaction (e.g. via
create_project_from_template/2 → clone_template/2), the inner
repo().transaction/2 becomes a savepoint and Postgres ignores
the inner isolation: keyword — the protection only holds if the
outer transaction is itself opened at :serializable (which
clone_template/2 does).
@spec add_task_dependency(uuid(), uuid()) :: {:ok, PhoenixKitProjects.Schemas.TaskDependency.t()} | {:error, Ecto.Changeset.t()}
Adds a template-level dependency from one task to another.
@spec apply_template_dependencies(PhoenixKitProjects.Schemas.Assignment.t()) :: :ok | {:ok, term()} | {:error, term()}
When an assignment is created, auto-create assignment-level dependencies from the task template's defaults (linking to sibling assignments already in the same project).
Idempotent: duplicate (assignment_uuid, depends_on_uuid) pairs return
a unique-constraint changeset, which we translate into a no-op (already
exists is the desired end state). All inserts run in a single
transaction so a partial failure rolls the batch back.
@spec assignment_status_counts() :: %{optional(String.t()) => non_neg_integer()}
Counts of assignments by status across active non-template projects. Returns a map like %{"todo" => 5, "in_progress" => 2, "done" => 10}.
Filters on p.status == "active" to match the dashboard's intent —
assignments inside archived projects shouldn't inflate the workload
stats shown alongside list_active_projects/0.
@spec available_dependencies(uuid(), uuid()) :: [ PhoenixKitProjects.Schemas.Assignment.t() ]
Assignments in this project that the given assignment does NOT yet depend on.
@spec available_task_dependencies(uuid()) :: [PhoenixKitProjects.Schemas.Task.t()]
Tasks that the given task does not yet depend on (for the dependency picker).
@spec change_assignment(PhoenixKitProjects.Schemas.Assignment.t(), map()) :: Ecto.Changeset.t()
Returns a changeset for the given assignment.
@spec change_project(PhoenixKitProjects.Schemas.Project.t(), map()) :: Ecto.Changeset.t()
Returns a changeset for the given project.
@spec change_task(PhoenixKitProjects.Schemas.Task.t(), map()) :: Ecto.Changeset.t()
Returns a changeset for the given task.
@spec complete_assignment(PhoenixKitProjects.Schemas.Assignment.t(), uuid() | nil) :: {:ok, PhoenixKitProjects.Schemas.Assignment.t()} | {:error, Ecto.Changeset.t()}
Marks an assignment done, stamping completed_by_uuid and completed_at.
@spec count_projects() :: non_neg_integer()
Total number of projects (including templates).
@spec count_tasks() :: non_neg_integer()
Total number of tasks in the library.
@spec count_templates() :: non_neg_integer()
Total number of template projects.
@spec create_assignment(map()) :: {:ok, PhoenixKitProjects.Schemas.Assignment.t()} | {:error, Ecto.Changeset.t()}
Inserts an assignment and broadcasts :assignment_created.
@spec create_assignment_from_template(uuid(), map()) :: {:ok, PhoenixKitProjects.Schemas.Assignment.t()} | {:error, :task_not_found | Ecto.Changeset.t()}
Creates an assignment pre-populated from the task template's defaults (description, duration, default assignee). The caller's attrs override any template values.
@spec create_project(map()) :: {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, Ecto.Changeset.t()}
Inserts a project and broadcasts :project_created.
@spec create_project_from_template(uuid(), map()) :: {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, :template_not_found | Ecto.Changeset.t() | term()}
Creates a new project by cloning a template. Copies all assignments (with their task links, descriptions, durations, assignees, weekends settings) and re-creates dependencies between the cloned assignments.
@spec create_task(map()) :: {:ok, PhoenixKitProjects.Schemas.Task.t()} | {:error, Ecto.Changeset.t()}
Inserts a task and broadcasts :task_created.
@spec delete_assignment(PhoenixKitProjects.Schemas.Assignment.t()) :: {:ok, PhoenixKitProjects.Schemas.Assignment.t()} | {:error, Ecto.Changeset.t()}
Deletes an assignment and broadcasts :assignment_deleted.
@spec delete_project(PhoenixKitProjects.Schemas.Project.t()) :: {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, Ecto.Changeset.t()}
Deletes a project and broadcasts :project_deleted.
@spec delete_task(PhoenixKitProjects.Schemas.Task.t()) :: {:ok, PhoenixKitProjects.Schemas.Task.t()} | {:error, Ecto.Changeset.t()}
Deletes a task and broadcasts :task_deleted.
Check if all dependencies of an assignment are done.
@spec get_assignment(uuid()) :: PhoenixKitProjects.Schemas.Assignment.t() | nil
Fetches an assignment by uuid with related records preloaded, or nil if not found.
@spec get_project(uuid()) :: PhoenixKitProjects.Schemas.Project.t() | nil
Fetches a project by uuid, or nil if not found.
@spec get_project!(uuid()) :: PhoenixKitProjects.Schemas.Project.t()
Fetches a project by uuid. Raises if not found.
@spec get_task(uuid()) :: PhoenixKitProjects.Schemas.Task.t() | nil
Fetches a task by uuid, or nil if not found.
@spec get_task!(uuid()) :: PhoenixKitProjects.Schemas.Task.t()
Fetches a task by uuid. Raises if not found.
@spec list_active_projects() :: [PhoenixKitProjects.Schemas.Project.t()]
Active, in-flight projects (started but not yet completed).
@spec list_all_dependencies(uuid()) :: [PhoenixKitProjects.Schemas.Dependency.t()]
All dependencies across every assignment in a project (used when cloning templates).
@spec list_assignments(uuid()) :: [PhoenixKitProjects.Schemas.Assignment.t()]
Lists assignments within a project, ordered by position, with related records preloaded.
@spec list_assignments_for_user(uuid()) :: [PhoenixKitProjects.Schemas.Assignment.t()]
Assignments currently assigned to the given user's staff record. Returns non-done assignments across all active projects, with project preloaded.
The rescue [Postgrex.Error, DBConnection.ConnectionError, Ecto.QueryError]
block at the bottom is intentional: a hard dep on
phoenix_kit_staff means a Staff outage (missing tables in early
install, sandbox-shutdown in tests, transient connection drop) would
otherwise take the Projects dashboard down. The rescue degrades
gracefully to "no assignments for this user" — the dashboard keeps
rendering for everyone else's data. Don't "clean it up" by
narrowing or removing.
@spec list_dependencies(uuid()) :: [PhoenixKitProjects.Schemas.Dependency.t()]
Dependencies declared on a single assignment.
@spec list_projects(keyword()) :: [PhoenixKitProjects.Schemas.Project.t()]
Lists projects. Accepts :status filter and :include_templates (default false).
@spec list_recently_completed_projects(pos_integer()) :: [ PhoenixKitProjects.Schemas.Project.t() ]
Completed projects (all tasks done), most recently completed first.
@spec list_setup_projects() :: [PhoenixKitProjects.Schemas.Project.t()]
Projects not yet started, in setup (immediate mode, not scheduled).
@spec list_task_dependencies(uuid()) :: [ PhoenixKitProjects.Schemas.TaskDependency.t() ]
Template-level dependencies declared on the given task.
@spec list_tasks() :: [PhoenixKitProjects.Schemas.Task.t()]
Lists all task-library entries, preloaded with defaults.
@spec list_templates() :: [PhoenixKitProjects.Schemas.Project.t()]
Lists projects that are templates, ordered by name.
@spec list_upcoming_projects() :: [PhoenixKitProjects.Schemas.Project.t()]
Scheduled projects waiting to start.
@spec project_summaries([PhoenixKitProjects.Schemas.Project.t()]) :: [map()]
Batched summaries for many projects — loads all their assignments in one query, then groups in memory. Preserves the input project order.
@spec project_summary(PhoenixKitProjects.Schemas.Project.t()) :: map() | nil
Summary of an active project for the overview dashboard. Returns a map with progress stats.
@spec recompute_project_completion(uuid()) :: :ok | {:completed, PhoenixKitProjects.Schemas.Project.t()} | {:reopened, PhoenixKitProjects.Schemas.Project.t()} | {:unchanged, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}
Called after an assignment status change. Checks whether all assignments
in the project are done. If so, sets completed_at. If not (e.g., a task
was reopened), clears it. Returns the (possibly updated) project.
@spec remove_dependency(uuid(), uuid()) :: {:ok, PhoenixKitProjects.Schemas.Dependency.t()} | {:error, :not_found | Ecto.Changeset.t()}
Removes an assignment-level dependency and broadcasts :dependency_removed.
@spec remove_task_dependency(uuid(), uuid()) :: {:ok, PhoenixKitProjects.Schemas.TaskDependency.t()} | {:error, :not_found | Ecto.Changeset.t()}
Removes a template-level dependency. Returns {:error, :not_found} if missing.
@spec reopen_assignment(PhoenixKitProjects.Schemas.Assignment.t()) :: {:ok, PhoenixKitProjects.Schemas.Assignment.t()} | {:error, Ecto.Changeset.t()}
Reverts an assignment to todo and clears its completion fields.
@spec start_project(PhoenixKitProjects.Schemas.Project.t()) :: {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, Ecto.Changeset.t()}
Stamps started_at on the project and broadcasts :project_started.
@spec update_assignment_form(PhoenixKitProjects.Schemas.Assignment.t(), map()) :: {:ok, PhoenixKitProjects.Schemas.Assignment.t()} | {:error, Ecto.Changeset.t()}
Form-safe update for user-submitted attrs. Does NOT apply completion
fields (completed_by_uuid, completed_at) even if they appear in
attrs — they are silently dropped by Assignment.changeset/2.
Use update_assignment_status/2 instead when updating from server
code that legitimately owns those fields (completion transitions,
progress updates).
The _form suffix is a deliberate smell: if you reach for this
function, double-check whether your caller is really a form handler.
@spec update_assignment_status(PhoenixKitProjects.Schemas.Assignment.t(), map()) :: {:ok, PhoenixKitProjects.Schemas.Assignment.t()} | {:error, Ecto.Changeset.t()}
Server-trusted update that additionally casts completed_by_uuid and
completed_at. Only call from server code (never pass raw form attrs),
since the caller vouches for those fields.
@spec update_project(PhoenixKitProjects.Schemas.Project.t(), map()) :: {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, Ecto.Changeset.t()}
Updates a project and broadcasts :project_updated.
@spec update_task(PhoenixKitProjects.Schemas.Task.t(), map()) :: {:ok, PhoenixKitProjects.Schemas.Task.t()} | {:error, Ecto.Changeset.t()}
Updates a task and broadcasts :task_updated.