PhoenixKitProjects.Projects (PhoenixKitProjects v0.1.1)

Copy Markdown View Source

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

error_atom()

@type error_atom() :: :not_found | :template_not_found | :task_not_found

Atom-shaped error returned for not-found / missing-resource cases.

uuid()

@type uuid() :: String.t() | <<_::128>>

UUIDv7 string or raw 16-byte binary (Ecto accepts either).

Functions

add_dependency(assignment_uuid, depends_on_uuid)

@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/2clone_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).

add_task_dependency(task_uuid, depends_on_task_uuid)

@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.

apply_template_dependencies(assignment)

@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.

assignment_status_counts()

@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.

available_dependencies(project_uuid, assignment_uuid)

@spec available_dependencies(uuid(), uuid()) :: [
  PhoenixKitProjects.Schemas.Assignment.t()
]

Assignments in this project that the given assignment does NOT yet depend on.

available_task_dependencies(task_uuid)

@spec available_task_dependencies(uuid()) :: [PhoenixKitProjects.Schemas.Task.t()]

Tasks that the given task does not yet depend on (for the dependency picker).

change_assignment(a, attrs \\ %{})

@spec change_assignment(PhoenixKitProjects.Schemas.Assignment.t(), map()) ::
  Ecto.Changeset.t()

Returns a changeset for the given assignment.

change_project(p, attrs \\ %{})

Returns a changeset for the given project.

change_task(t, attrs \\ %{})

Returns a changeset for the given task.

complete_assignment(a, completed_by_uuid)

@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.

count_projects()

@spec count_projects() :: non_neg_integer()

Total number of projects (including templates).

count_tasks()

@spec count_tasks() :: non_neg_integer()

Total number of tasks in the library.

count_templates()

@spec count_templates() :: non_neg_integer()

Total number of template projects.

create_assignment(attrs)

@spec create_assignment(map()) ::
  {:ok, PhoenixKitProjects.Schemas.Assignment.t()}
  | {:error, Ecto.Changeset.t()}

Inserts an assignment and broadcasts :assignment_created.

create_assignment_from_template(task_uuid, attrs)

@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.

create_project(attrs)

@spec create_project(map()) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, Ecto.Changeset.t()}

Inserts a project and broadcasts :project_created.

create_project_from_template(template_uuid, project_attrs)

@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.

create_task(attrs)

@spec create_task(map()) ::
  {:ok, PhoenixKitProjects.Schemas.Task.t()} | {:error, Ecto.Changeset.t()}

Inserts a task and broadcasts :task_created.

delete_assignment(a)

Deletes an assignment and broadcasts :assignment_deleted.

delete_project(p)

Deletes a project and broadcasts :project_deleted.

delete_task(t)

Deletes a task and broadcasts :task_deleted.

dependencies_met?(assignment_uuid)

@spec dependencies_met?(uuid()) :: boolean()

Check if all dependencies of an assignment are done.

get_assignment(uuid)

@spec get_assignment(uuid()) :: PhoenixKitProjects.Schemas.Assignment.t() | nil

Fetches an assignment by uuid with related records preloaded, or nil if not found.

get_project(uuid)

@spec get_project(uuid()) :: PhoenixKitProjects.Schemas.Project.t() | nil

Fetches a project by uuid, or nil if not found.

get_project!(uuid)

@spec get_project!(uuid()) :: PhoenixKitProjects.Schemas.Project.t()

Fetches a project by uuid. Raises if not found.

get_task(uuid)

@spec get_task(uuid()) :: PhoenixKitProjects.Schemas.Task.t() | nil

Fetches a task by uuid, or nil if not found.

get_task!(uuid)

@spec get_task!(uuid()) :: PhoenixKitProjects.Schemas.Task.t()

Fetches a task by uuid. Raises if not found.

list_active_projects()

@spec list_active_projects() :: [PhoenixKitProjects.Schemas.Project.t()]

Active, in-flight projects (started but not yet completed).

list_all_dependencies(project_uuid)

@spec list_all_dependencies(uuid()) :: [PhoenixKitProjects.Schemas.Dependency.t()]

All dependencies across every assignment in a project (used when cloning templates).

list_assignments(project_uuid)

@spec list_assignments(uuid()) :: [PhoenixKitProjects.Schemas.Assignment.t()]

Lists assignments within a project, ordered by position, with related records preloaded.

list_assignments_for_user(user_uuid)

@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.

list_dependencies(assignment_uuid)

@spec list_dependencies(uuid()) :: [PhoenixKitProjects.Schemas.Dependency.t()]

Dependencies declared on a single assignment.

list_projects(opts \\ [])

@spec list_projects(keyword()) :: [PhoenixKitProjects.Schemas.Project.t()]

Lists projects. Accepts :status filter and :include_templates (default false).

list_recently_completed_projects(limit \\ 5)

@spec list_recently_completed_projects(pos_integer()) :: [
  PhoenixKitProjects.Schemas.Project.t()
]

Completed projects (all tasks done), most recently completed first.

list_setup_projects()

@spec list_setup_projects() :: [PhoenixKitProjects.Schemas.Project.t()]

Projects not yet started, in setup (immediate mode, not scheduled).

list_task_dependencies(task_uuid)

@spec list_task_dependencies(uuid()) :: [
  PhoenixKitProjects.Schemas.TaskDependency.t()
]

Template-level dependencies declared on the given task.

list_tasks()

@spec list_tasks() :: [PhoenixKitProjects.Schemas.Task.t()]

Lists all task-library entries, preloaded with defaults.

list_templates()

@spec list_templates() :: [PhoenixKitProjects.Schemas.Project.t()]

Lists projects that are templates, ordered by name.

list_upcoming_projects()

@spec list_upcoming_projects() :: [PhoenixKitProjects.Schemas.Project.t()]

Scheduled projects waiting to start.

project_summaries(projects)

@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.

project_summary(project)

@spec project_summary(PhoenixKitProjects.Schemas.Project.t()) :: map() | nil

Summary of an active project for the overview dashboard. Returns a map with progress stats.

recompute_project_completion(project_uuid)

@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.

remove_dependency(assignment_uuid, depends_on_uuid)

@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.

remove_task_dependency(task_uuid, depends_on_task_uuid)

@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.

reopen_assignment(a)

Reverts an assignment to todo and clears its completion fields.

start_project(p)

Stamps started_at on the project and broadcasts :project_started.

update_assignment_form(a, attrs)

@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.

update_assignment_status(a, attrs)

@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.

update_project(p, attrs)

Updates a project and broadcasts :project_updated.

update_task(t, attrs)

Updates a task and broadcasts :task_updated.