|
|
|
|
@ -1,5 +1,6 @@
|
|
|
|
|
import type { Database } from 'bun:sqlite';
|
|
|
|
|
import { db, ensureUserExists } from '../db';
|
|
|
|
|
import { getDb as getGlobalDb } from '../db/locator';
|
|
|
|
|
import { AllowedGroups } from '../services/allowed-groups';
|
|
|
|
|
import { isGroupId } from '../utils/whatsapp';
|
|
|
|
|
import { pickNextDisplayCode } from './display-code';
|
|
|
|
|
@ -21,15 +22,19 @@ type CreateAssignmentInput = {
|
|
|
|
|
export class TaskService {
|
|
|
|
|
static dbInstance: Database = db;
|
|
|
|
|
|
|
|
|
|
private static getDb(): Database {
|
|
|
|
|
return ((this as any).dbInstance as Database) ?? getGlobalDb();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static createTask(task: CreateTaskInput, assignments: CreateAssignmentInput[] = []): number {
|
|
|
|
|
const runTx = this.dbInstance.transaction(() => {
|
|
|
|
|
const runTx = this.getDb().transaction(() => {
|
|
|
|
|
|
|
|
|
|
const insertTask = this.dbInstance.prepare(`
|
|
|
|
|
const insertTask = this.getDb().prepare(`
|
|
|
|
|
INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?)
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
const ensuredCreator = ensureUserExists(task.created_by, this.dbInstance);
|
|
|
|
|
const ensuredCreator = ensureUserExists(task.created_by, this.getDb());
|
|
|
|
|
if (!ensuredCreator) {
|
|
|
|
|
throw new Error('No se pudo asegurar created_by');
|
|
|
|
|
}
|
|
|
|
|
@ -49,14 +54,14 @@ export class TaskService {
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
if (groupIdToInsert) {
|
|
|
|
|
const exists = this.dbInstance.prepare(`SELECT 1 FROM groups WHERE id = ? AND COALESCE(is_community,0) = 0`).get(groupIdToInsert);
|
|
|
|
|
const exists = this.getDb().prepare(`SELECT 1 FROM groups WHERE id = ? AND COALESCE(is_community,0) = 0`).get(groupIdToInsert);
|
|
|
|
|
if (!exists) {
|
|
|
|
|
groupIdToInsert = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Elegir display_code global reutilizable entre tareas activas
|
|
|
|
|
const displayCode = pickNextDisplayCode(this.dbInstance);
|
|
|
|
|
const displayCode = pickNextDisplayCode(this.getDb());
|
|
|
|
|
|
|
|
|
|
const runResult = insertTask.run(
|
|
|
|
|
task.description,
|
|
|
|
|
@ -68,7 +73,7 @@ export class TaskService {
|
|
|
|
|
const taskId = Number((runResult as { lastInsertRowid?: number | bigint }).lastInsertRowid);
|
|
|
|
|
|
|
|
|
|
if (assignments.length > 0) {
|
|
|
|
|
const insertAssignment = this.dbInstance.prepare(`
|
|
|
|
|
const insertAssignment = this.getDb().prepare(`
|
|
|
|
|
INSERT INTO task_assignments (task_id, user_id, assigned_by)
|
|
|
|
|
VALUES (?, ?, ?)
|
|
|
|
|
`);
|
|
|
|
|
@ -76,13 +81,13 @@ export class TaskService {
|
|
|
|
|
// Evitar duplicados por (task_id, user_id) tras asegurar usuarios
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
for (const a of assignments) {
|
|
|
|
|
const ensuredUser = ensureUserExists(a.user_id, this.dbInstance);
|
|
|
|
|
const ensuredUser = ensureUserExists(a.user_id, this.getDb());
|
|
|
|
|
if (!ensuredUser) continue;
|
|
|
|
|
if (seen.has(ensuredUser)) continue;
|
|
|
|
|
seen.add(ensuredUser);
|
|
|
|
|
|
|
|
|
|
const ensuredAssigner =
|
|
|
|
|
ensureUserExists(a.assigned_by || ensuredCreator, this.dbInstance) || ensuredCreator;
|
|
|
|
|
ensureUserExists(a.assigned_by || ensuredCreator, this.getDb()) || ensuredCreator;
|
|
|
|
|
|
|
|
|
|
insertAssignment.run(taskId, ensuredUser, ensuredAssigner);
|
|
|
|
|
}
|
|
|
|
|
@ -103,7 +108,7 @@ export class TaskService {
|
|
|
|
|
display_code: number | null;
|
|
|
|
|
assignees: string[];
|
|
|
|
|
}> {
|
|
|
|
|
const rows = this.dbInstance
|
|
|
|
|
const rows = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT id, description, due_date, group_id, display_code
|
|
|
|
|
FROM tasks
|
|
|
|
|
@ -117,7 +122,7 @@ export class TaskService {
|
|
|
|
|
`)
|
|
|
|
|
.all(groupId, limit) as Array<{ id: number; description: string; due_date: string | null; group_id: string | null; display_code: number | null }>;
|
|
|
|
|
|
|
|
|
|
const getAssignees = this.dbInstance.prepare(`
|
|
|
|
|
const getAssignees = this.getDb().prepare(`
|
|
|
|
|
SELECT user_id FROM task_assignments
|
|
|
|
|
WHERE task_id = ?
|
|
|
|
|
ORDER BY assigned_at ASC
|
|
|
|
|
@ -139,7 +144,7 @@ export class TaskService {
|
|
|
|
|
display_code: number | null;
|
|
|
|
|
assignees: string[];
|
|
|
|
|
}> {
|
|
|
|
|
const rows = this.dbInstance
|
|
|
|
|
const rows = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT t.id, t.description, t.due_date, t.group_id, t.display_code
|
|
|
|
|
FROM tasks t
|
|
|
|
|
@ -154,7 +159,7 @@ export class TaskService {
|
|
|
|
|
`)
|
|
|
|
|
.all(userId, limit) as Array<{ id: number; description: string; due_date: string | null; group_id: string | null; display_code: number | null }>;
|
|
|
|
|
|
|
|
|
|
const getAssignees = this.dbInstance.prepare(`
|
|
|
|
|
const getAssignees = this.getDb().prepare(`
|
|
|
|
|
SELECT user_id FROM task_assignments
|
|
|
|
|
WHERE task_id = ?
|
|
|
|
|
ORDER BY assigned_at ASC
|
|
|
|
|
@ -169,7 +174,7 @@ export class TaskService {
|
|
|
|
|
|
|
|
|
|
// Contar pendientes del grupo (sin límite)
|
|
|
|
|
static countGroupPending(groupId: string): number {
|
|
|
|
|
const row = this.dbInstance
|
|
|
|
|
const row = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT COUNT(*) as cnt
|
|
|
|
|
FROM tasks
|
|
|
|
|
@ -182,7 +187,7 @@ export class TaskService {
|
|
|
|
|
|
|
|
|
|
// Contar pendientes asignadas al usuario (sin límite)
|
|
|
|
|
static countUserPending(userId: string): number {
|
|
|
|
|
const row = this.dbInstance
|
|
|
|
|
const row = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT COUNT(*) as cnt
|
|
|
|
|
FROM tasks t
|
|
|
|
|
@ -199,9 +204,9 @@ export class TaskService {
|
|
|
|
|
status: 'updated' | 'already' | 'not_found';
|
|
|
|
|
task?: { id: number; description: string; due_date: string | null; display_code: number | null };
|
|
|
|
|
} {
|
|
|
|
|
const ensured = ensureUserExists(completedBy, this.dbInstance);
|
|
|
|
|
const ensured = ensureUserExists(completedBy, this.getDb());
|
|
|
|
|
|
|
|
|
|
const existing = this.dbInstance
|
|
|
|
|
const existing = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT id, description, due_date, completed, completed_at, display_code, group_id
|
|
|
|
|
FROM tasks
|
|
|
|
|
@ -225,7 +230,7 @@ export class TaskService {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.dbInstance
|
|
|
|
|
this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
UPDATE tasks
|
|
|
|
|
SET completed = 1,
|
|
|
|
|
@ -237,7 +242,7 @@ export class TaskService {
|
|
|
|
|
|
|
|
|
|
// Fase 2: reacción ✅ al completar dentro del TTL y con gating
|
|
|
|
|
try {
|
|
|
|
|
enqueueCompletionReactionIfEligible(this.dbInstance, taskId);
|
|
|
|
|
enqueueCompletionReactionIfEligible(this.getDb(), taskId);
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
@ -260,7 +265,7 @@ export class TaskService {
|
|
|
|
|
display_code: number | null;
|
|
|
|
|
assignees: string[];
|
|
|
|
|
}> {
|
|
|
|
|
const rows = this.dbInstance
|
|
|
|
|
const rows = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT id, description, due_date, group_id, display_code
|
|
|
|
|
FROM tasks
|
|
|
|
|
@ -282,7 +287,7 @@ export class TaskService {
|
|
|
|
|
|
|
|
|
|
// Contar pendientes sin dueño del grupo (sin límite)
|
|
|
|
|
static countGroupUnassigned(groupId: string): number {
|
|
|
|
|
const row = this.dbInstance
|
|
|
|
|
const row = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT COUNT(*) as cnt
|
|
|
|
|
FROM tasks t
|
|
|
|
|
@ -301,12 +306,12 @@ export class TaskService {
|
|
|
|
|
status: 'claimed' | 'already' | 'not_found' | 'completed';
|
|
|
|
|
task?: { id: number; description: string; due_date: string | null; display_code: number | null };
|
|
|
|
|
} {
|
|
|
|
|
const ensuredUser = ensureUserExists(userId, this.dbInstance);
|
|
|
|
|
const ensuredUser = ensureUserExists(userId, this.getDb());
|
|
|
|
|
if (!ensuredUser) {
|
|
|
|
|
throw new Error('No se pudo asegurar el usuario');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existing = this.dbInstance
|
|
|
|
|
const existing = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT id, description, due_date, group_id, completed, completed_at, display_code
|
|
|
|
|
FROM tasks
|
|
|
|
|
@ -330,7 +335,7 @@ export class TaskService {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const already = this.dbInstance
|
|
|
|
|
const already = this.getDb()
|
|
|
|
|
.prepare(`SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?`)
|
|
|
|
|
.get(taskId, ensuredUser);
|
|
|
|
|
|
|
|
|
|
@ -346,12 +351,12 @@ export class TaskService {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const insertAssignment = this.dbInstance.prepare(`
|
|
|
|
|
const insertAssignment = this.getDb().prepare(`
|
|
|
|
|
INSERT OR IGNORE INTO task_assignments (task_id, user_id, assigned_by)
|
|
|
|
|
VALUES (?, ?, ?)
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
this.dbInstance.transaction(() => {
|
|
|
|
|
this.getDb().transaction(() => {
|
|
|
|
|
insertAssignment.run(taskId, ensuredUser, ensuredUser);
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
@ -372,12 +377,12 @@ export class TaskService {
|
|
|
|
|
task?: { id: number; description: string; due_date: string | null; display_code: number | null };
|
|
|
|
|
now_unassigned?: boolean; // true si tras soltar no quedan asignados
|
|
|
|
|
} {
|
|
|
|
|
const ensuredUser = ensureUserExists(userId, this.dbInstance);
|
|
|
|
|
const ensuredUser = ensureUserExists(userId, this.getDb());
|
|
|
|
|
if (!ensuredUser) {
|
|
|
|
|
throw new Error('No se pudo asegurar el usuario');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existing = this.dbInstance
|
|
|
|
|
const existing = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT id, description, due_date, group_id, completed, completed_at, display_code
|
|
|
|
|
FROM tasks
|
|
|
|
|
@ -403,7 +408,7 @@ export class TaskService {
|
|
|
|
|
|
|
|
|
|
// Regla: no permitir soltar si es tarea personal y el usuario es el único asignatario
|
|
|
|
|
try {
|
|
|
|
|
const stats = this.dbInstance.prepare(`
|
|
|
|
|
const stats = this.getDb().prepare(`
|
|
|
|
|
SELECT COUNT(*) AS cnt,
|
|
|
|
|
SUM(CASE WHEN user_id = ? THEN 1 ELSE 0 END) AS mine
|
|
|
|
|
FROM task_assignments
|
|
|
|
|
@ -425,14 +430,14 @@ export class TaskService {
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
const deleteStmt = this.dbInstance.prepare(`
|
|
|
|
|
const deleteStmt = this.getDb().prepare(`
|
|
|
|
|
DELETE FROM task_assignments
|
|
|
|
|
WHERE task_id = ? AND user_id = ?
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
const result = deleteStmt.run(taskId, ensuredUser) as { changes?: number };
|
|
|
|
|
|
|
|
|
|
const cntRow = this.dbInstance
|
|
|
|
|
const cntRow = this.getDb()
|
|
|
|
|
.prepare(`SELECT COUNT(*) as cnt FROM task_assignments WHERE task_id = ?`)
|
|
|
|
|
.get(taskId) as { cnt?: number } | undefined;
|
|
|
|
|
const remaining = Number(cntRow?.cnt || 0);
|
|
|
|
|
@ -474,7 +479,7 @@ export class TaskService {
|
|
|
|
|
completed: number;
|
|
|
|
|
completed_at: string | null;
|
|
|
|
|
} | null {
|
|
|
|
|
const row = this.dbInstance.prepare(`
|
|
|
|
|
const row = this.getDb().prepare(`
|
|
|
|
|
SELECT
|
|
|
|
|
id,
|
|
|
|
|
description,
|
|
|
|
|
@ -492,7 +497,7 @@ export class TaskService {
|
|
|
|
|
|
|
|
|
|
// Buscar tarea activa por display_code global
|
|
|
|
|
static getActiveTaskByDisplayCode(displayCode: number): { id: number; description: string; due_date: string | null; display_code: number | null } | null {
|
|
|
|
|
const row = this.dbInstance.prepare(`
|
|
|
|
|
const row = this.getDb().prepare(`
|
|
|
|
|
SELECT id, description, due_date, display_code
|
|
|
|
|
FROM tasks
|
|
|
|
|
WHERE display_code = ? AND COALESCE(completed, 0) = 0 AND completed_at IS NULL
|
|
|
|
|
@ -542,7 +547,7 @@ export class TaskService {
|
|
|
|
|
group_name: string | null;
|
|
|
|
|
display_code: number | null;
|
|
|
|
|
}> {
|
|
|
|
|
const rows = this.dbInstance
|
|
|
|
|
const rows = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT t.id, t.description, t.due_date, t.group_id, t.display_code, g.name AS group_name
|
|
|
|
|
FROM tasks t
|
|
|
|
|
@ -567,7 +572,7 @@ export class TaskService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static countAllActive(): number {
|
|
|
|
|
const row = this.dbInstance
|
|
|
|
|
const row = this.getDb()
|
|
|
|
|
.prepare(`
|
|
|
|
|
SELECT COUNT(*) AS cnt
|
|
|
|
|
FROM tasks t
|
|
|
|
|
|