# NisTask Task management integrated into the NIS per-user SQLite database — supports CRUD, entity linking, 60+ query functions, recurrence, subtasks, and analytics. --- ## Table of Contents 1. [Overview](#overview) 2. [Features](#features) 3. [Time Conventions](#time-conventions) 4. [Usage](#usage) 5. [API Reference](#api-reference) 6. [Query Categories](#query-categories) 7. [Related Documentation](#related-documentation) --- ## Overview `NisTask` is a pure functional module (no GenServer) that stores tasks in the `nis_tasks` table within each user's NIS SQLite database. It replaces the older `TaskManager` by co-locating tasks alongside contacts, companies, and other NIS entities. Tasks support entity linking via `nis_task_links`, FTS5 indexing through `Nis.index_fts/4`, subtask hierarchies via `parent_id`, and recurrence patterns via the `recur` field processed by `TaskRecurrence`. All database access goes through the user's Sqler instance (`Nis.db(username)`), with `Nis.ensure_db/1` called before every operation to guarantee the database is initialized. ## Features | Feature | Description | |---------|-------------| | CRUD | Create, read, update, delete, complete tasks | | Entity linking | Link tasks to contacts, companies, or other NIS entities | | Subtasks | Parent-child task hierarchies via `parent_id` | | Recurrence | Auto-create next instance on completion (`daily`, `weekly`, `hourly`, `every 3 days`, `every 5 minutes`, etc.) | | Pushover notifications | Optional alarm at due time via Alarm → Pushover pipeline; auto-cancels on complete/delete | | Tags | JSON-array tags with `json_each` queries | | Status queries | Filter by todo, in_progress, blocked, completed | | Due date queries | Today, tomorrow, this week, next hour, overdue | | Waiting/expiry | Snooze with `after_at`, auto-expire with `expires_at` | | Priority queries | Sort by priority, top-5, high-priority tagged | | Dashboard | Daily summary, agenda, next-up, stale task detection | | Analytics | Count by status/tag, completion rates, priority distribution | | Data integrity | Detect orphan subtasks, mismatched completion states | | FTS indexing | Full-text search on title, note, waiting_for, tags | | Legacy migration | Import from old TaskManager SQLite databases | ## Time Conventions | Field | Unit | Description | |-------|------|-------------| | `id` | milliseconds | Auto-generated by Sqler (creation timestamp) | | `updated_at` | milliseconds | Auto-managed by Sqler for optimistic locking | | `due_at` | **seconds** | User-facing due date (Unix epoch) | | `after_at` | **seconds** | Task becomes actionable after this time | | `expires_at` | **seconds** | Task expires at this time | | `completed_at` | **seconds** | When the task was completed | | `alarm_id` | integer (nullable) | Alarm ID for Pushover notification at due time | > `id / 1000` converts to Unix seconds — this is used in analytics queries like `avg_completion_time` to compute elapsed time since creation. ## Usage ### Create a Task ```elixir {:ok, id} = NisTask.create("james", %{ title: "Review PR #42", priority: 7, due_at: 1709164800, tags: ["work", "urgent"], status: "todo" }) ``` ### Complete a Task ```elixir {:ok, _} = NisTask.complete("james", id) ``` ### Complete a Recurring Task ```elixir {:ok, %{completed: old_task, next: new_task}} = NisTask.complete_recurring("james", id, "Asia/Ho_Chi_Minh") ``` ### Link to an Entity ```elixir {:ok, :linked} = NisTask.link("james", task_id, "contact", contact_id) tasks = NisTask.tasks_for_entity("james", "contact", contact_id) ``` ### Query Tasks ```elixir overdue = NisTask.overdue("james") due_today = NisTask.due_today("james", "Asia/Ho_Chi_Minh") agenda = NisTask.todays_agenda("james", "Asia/Ho_Chi_Minh") summary = NisTask.daily_summary("james", "Asia/Ho_Chi_Minh") ``` ## API Reference ### `create/2` Create a new task. Auto-indexes in FTS. **Parameters:** - `username` (string) - `params` (map) — optional keys: `:title`, `:priority` (default 0), `:note`, `:after_at`, `:expires_at`, `:waiting_for`, `:status` (default "todo"), `:due_at`, `:tags`, `:parent_id`, `:recur`, `:alarm_id` **Returns:** `{:ok, id}` or `{:error, reason}` ```elixir {:ok, id} = NisTask.create("james", %{title: "Buy groceries", priority: 3, tags: ["home"]}) ``` ### `get/2` Fetch a task by ID, including its entity links. **Parameters:** - `username` (string) - `id` (integer) **Returns:** `{:ok, map}` (with `"links"` key) or `{:error, :not_found}` ```elixir {:ok, task} = NisTask.get("james", id) # task["links"] => [%{"entity_type" => "contact", "entity_id" => 42}] ``` ### `update/3` Update a task with optimistic locking. Re-indexes FTS. **Parameters:** - `username` (string) - `id` (integer) - `params` (map) — any task field except `id` **Returns:** `{:ok, updated_at}` or `{:error, reason}` ```elixir {:ok, _} = NisTask.update("james", id, %{priority: 9, status: "in_progress"}) ``` ### `complete/2` Mark a task as completed with current timestamp. **Returns:** `{:ok, updated_at}` ```elixir {:ok, _} = NisTask.complete("james", id) ``` ### `delete/2` Delete a task and its links and FTS entry. **Returns:** `{:ok, count}` ```elixir {:ok, 1} = NisTask.delete("james", id) ``` ### `link/4` Link a task to an entity. Deduplicates — returns `{:ok, :already_linked}` if the link exists. **Parameters:** - `username` (string) - `task_id` (integer) - `entity_type` (string) — e.g. `"contact"`, `"company"` - `entity_id` (integer) **Returns:** `{:ok, :linked}` or `{:ok, :already_linked}` ```elixir {:ok, :linked} = NisTask.link("james", task_id, "contact", 42) ``` ### `unlink/4` Remove a task-entity link. **Returns:** `:ok` ```elixir :ok = NisTask.unlink("james", task_id, "contact", 42) ``` ### `links_for/2` Get all entity links for a task. **Returns:** list of `%{"entity_type" => type, "entity_id" => id}` ```elixir links = NisTask.links_for("james", task_id) ``` ### `tasks_for_entity/3` Get all tasks linked to an entity, sorted by priority DESC. **Returns:** list of task maps ```elixir tasks = NisTask.tasks_for_entity("james", "contact", 42) ``` ### `complete_recurring/3` Complete a recurring task and create the next instance with recalculated due date. **Parameters:** - `username` (string) - `id` (integer) - `tz` (string, optional) — timezone for next due calculation, defaults to `"Etc/UTC"` **Returns:** `{:ok, %{completed: map, next: map | nil}}` ```elixir {:ok, %{completed: done, next: new_task}} = NisTask.complete_recurring("james", id, "Asia/Ho_Chi_Minh") ``` ### `next_actionable/2` Get the single highest-priority actionable task (not waiting, not expired). **Returns:** `{:ok, map}` or `{:error, :not_found}` ```elixir {:ok, task} = NisTask.next_actionable("james", "Asia/Ho_Chi_Minh") ``` ### `daily_summary/2` Aggregate daily summary with completed today, overdue, due today, due tomorrow, and blocked. **Returns:** map with keys `:completed_today`, `:overdue`, `:due_today`, `:due_tomorrow`, `:blocked` ```elixir summary = NisTask.daily_summary("james", "Asia/Ho_Chi_Minh") ``` ### `migrate_from_legacy/2` Import tasks from an old TaskManager SQLite database. **Parameters:** - `username` (string) - `user_id` (integer) — the legacy user ID (looks for `data/tasks_user_{id}.sqlite`) **Returns:** `{:ok, count}` or `{:error, :legacy_db_not_found}` ```elixir {:ok, 42} = NisTask.migrate_from_legacy("james", 1) ``` ## Query Categories ### Status Queries | Function | Description | |----------|-------------| | `open_tasks/1` | Status in `todo`, `in_progress` | | `blocked_tasks/1` | Status = `blocked` | | `completed_tasks/1` | Status = `completed` | | `in_progress_tasks/1` | Status = `in_progress` | | `todo_without_due/1` | Todo tasks with no due date | ### Due Date Queries | Function | Description | |----------|-------------| | `due_today/2` | Due between start/end of today (tz-aware) | | `due_tomorrow/2` | Due between start of tomorrow and day after | | `due_this_week/2` | Due within next 7 days | | `due_next_hour/1` | Due within 60 minutes | | `due_next_30_minutes/1` | Due within 30 minutes | | `overdue/1` | Due date passed, not completed | | `no_due_date/1` | Open tasks with no due date | ### Waiting / Expiry Queries | Function | Description | |----------|-------------| | `currently_waiting/1` | `after_at` is in the future | | `wait_ended_today/2` | `after_at` passed today | | `expiring_today/2` | Expires before end of today | | `expired_never_completed/1` | Expired and never completed | | `becoming_active_24h/1` | `after_at` within next 24 hours | ### Priority Queries | Function | Description | |----------|-------------| | `open_by_priority/1` | Open tasks sorted by priority DESC | | `due_next_hour_by_priority/1` | Due next hour, sorted by priority | | `highest_priority_open/1` | Single highest-priority open task | | `top_5_priority_this_week/2` | Top 5 by priority due this week | | `overdue_by_priority/1` | Overdue sorted by priority DESC | | `blocked_by_priority/1` | Blocked sorted by priority DESC | ### Completion Queries | Function | Description | |----------|-------------| | `completed_today/2` | Completed today (tz-aware) | | `completed_this_week/1` | Completed in last 7 days | | `completed_last_30_days/1` | Completed in last 30 days | | `completed_late/1` | Completed after due date | | `completed_early/1` | Completed before due date | | `completed_per_day_last_7/1` | Day-by-day completion counts | ### Tag Queries | Function | Description | |----------|-------------| | `open_tagged/2` | Open tasks with a specific tag | | `open_tagged_due_today/3` | Tagged + due today | | `all_tags/1` | All distinct tags | | `tag_distribution/1` | Tag counts for open tasks | | `high_priority_tagged/3` | High-priority tasks with tag | | `blocked_tagged/2` | Blocked tasks with tag | ### Subtask Queries | Function | Description | |----------|-------------| | `top_level_tasks/1` | Tasks with no parent | | `subtasks_of/2` | Children of a parent task | | `parents_with_incomplete_subtasks/1` | Parents with unfinished children | | `parents_all_subtasks_complete/1` | Parents whose children are all done | | `subtask_count_per_parent/1` | Count of subtasks per parent | | `orphan_subtasks/1` | Subtasks whose parent no longer exists | ### Blocking Queries | Function | Description | |----------|-------------| | `blocked_with_reason/1` | All blocked tasks | | `blocked_more_than_7_days/1` | Blocked for over a week | | `waiting_for_not_blocked/1` | Has `waiting_for` but not blocked status | | `blocked_by_duration/1` | Blocked tasks sorted by how long they've been blocked | ### Dashboard Queries | Function | Description | |----------|-------------| | `todays_agenda/2` | Due today, not waiting, not expired, by priority | | `next_up/1` | Top 10 open tasks with no due date, by priority | | `becoming_actionable_today/2` | `after_at` falls within today | | `stale_tasks/1` | In-progress for over 7 days | | `updated_last_hour/1` | Recently touched tasks | ### Analytics Queries | Function | Description | |----------|-------------| | `count_by_status/1` | Open task counts grouped by status | | `count_by_tag/1` | All task counts grouped by tag | | `avg_completion_time/1` | Average seconds from creation to completion | | `count_overdue/1` | Number of overdue tasks | | `count_expiring_48h/1` | Tasks expiring within 48 hours | | `priority_distribution/1` | Open task counts grouped by priority | ### Data Integrity Queries | Function | Description | |----------|-------------| | `completed_at_but_not_completed/1` | Has `completed_at` but status != completed | | `completed_but_no_completed_at/1` | Status = completed but no `completed_at` | | `expires_before_due/1` | Expires before the due date | | `past_waiting_still_blocked/1` | `after_at` passed but still blocked | | `empty_title/1` | Tasks with null or empty title | ### Recurrence Queries | Function | Description | |----------|-------------| | `recurring_tasks/1` | Open tasks with a recurrence pattern | | `recurring_due_today/2` | Recurring tasks due today | ## Related Documentation - [Nis](nis.md) — parent CRM module, database setup, entity CRUD - [NisTaskRest](nis_task_rest.md) — REST API bridge for tasks - [TaskCapture](task_capture.md) — natural language task parser - [TaskRecurrence](task_recurrence.md) — recurrence engine --- *Source: `lib/nis_task.ex` — Last updated: 2026-03-03*