import { create } from "zustand"; import { loadDataApi, saveDataApi, getUniqueIdApi, s3UploadFileApi } from "@/request/api"; import { scheduleTodoReminder, cancelTodoReminders, requestNotificationPermission, } from "@/helpers/notification"; import { Todo, TodoFile, TodoLog, TodoLogType, TodoRating, TodoRepeatType, TodoSaveChanges, TodoStatus, TodoFamilyMember, } from "@/types/todo"; const TABLES = { todo: "b_todo", todoLog: "b_todo_log", todoRating: "b_todo_rating", todoFile: "b_todo_file", } as const; interface TodoState { todos: Todo[]; todoLogs: TodoLog[]; todoRatings: TodoRating[]; todoFiles: TodoFile[]; members: TodoFamilyMember[]; clear: () => void; fetchAll: (familyId: number) => Promise; fetchTodos: (familyId: number) => Promise; fetchTodoLogs: (familyId: number, todoId?: number) => Promise; fetchTodoRatings: (familyId: number, todoId?: number) => Promise; fetchTodoFiles: (familyId: number, todoId?: number) => Promise; fetchMembers: (familyId: number) => Promise; saveTodoChanges: (changes: TodoSaveChanges) => Promise; saveTodoLogChanges: (changes: TodoSaveChanges) => Promise; saveTodoRatingChanges: (changes: TodoSaveChanges) => Promise; saveTodoFileChanges: (changes: TodoSaveChanges) => Promise; uploadFile: (familyId: number, file: any) => Promise; createTodo: (params: { familyId: number; userId: number; name: string; description?: string; priority: number; assigneeId?: number | null; deadline?: string | null; repeatType?: number; remindAt?: string | null; }) => Promise; claimTodo: (params: { todoId: number; familyId: number; userId: number }) => Promise; rejectTodo: (params: { todoId: number; familyId: number; userId: number }) => Promise; transferTodo: (params: { todoId: number; familyId: number; userId: number; newAssigneeId: number | null; }) => Promise; completeTodo: (params: { todoId: number; familyId: number; userId: number; completeNote?: string; fileInserts?: Partial[]; }) => Promise; rateTodo: (params: { todoId: number; familyId: number; userId: number; rating: number; content?: string; }) => Promise; } function normalizeChanges(changes: TodoSaveChanges) { return { inserts: changes.inserts ?? [], updates: changes.updates ?? [], deletes: changes.deletes ?? [], }; } function nextDeadline( now: Date, repeatType: TodoRepeatType, oldDeadline?: string | null, ): string | null { if (repeatType === TodoRepeatType.None || !oldDeadline) return null; const d = new Date(oldDeadline); switch (repeatType) { case TodoRepeatType.Daily: d.setDate(d.getDate() + 1); break; case TodoRepeatType.Weekly: d.setDate(d.getDate() + 7); break; case TodoRepeatType.Monthly: d.setMonth(d.getMonth() + 1); break; } return d.toISOString(); } export const useTodoStore = create((set, get) => ({ todos: [], todoLogs: [], todoRatings: [], todoFiles: [], members: [], clear: () => set({ todos: [], todoLogs: [], todoRatings: [], todoFiles: [], members: [] }), fetchAll: async (familyId: number) => { if (!familyId) return; await Promise.all([ get().fetchTodos(familyId), get().fetchTodoLogs(familyId), get().fetchTodoRatings(familyId), get().fetchTodoFiles(familyId), get().fetchMembers(familyId), ]); }, fetchTodos: async (familyId: number) => { if (!familyId) return; const res = await loadDataApi({ view_name: TABLES.todo, search_condition: "family_id = ?", search_columns: [], order_by: "sort_number asc, id asc", args: [familyId], }); if (res.isSuccess) set({ todos: res.data as Todo[] }); }, fetchTodoLogs: async (familyId: number, todoId?: number) => { if (!familyId) return; const conditions = ["family_id = ?"]; const args: any[] = [familyId]; if (todoId !== undefined) { conditions.push("todo_id = ?"); args.push(todoId); } const res = await loadDataApi({ view_name: TABLES.todoLog, search_condition: conditions.join(" and "), search_columns: [], order_by: "create_time asc, id asc", args, }); if (res.isSuccess) set({ todoLogs: res.data as TodoLog[] }); }, fetchTodoRatings: async (familyId: number, todoId?: number) => { if (!familyId) return; const conditions = ["family_id = ?"]; const args: any[] = [familyId]; if (todoId !== undefined) { conditions.push("todo_id = ?"); args.push(todoId); } const res = await loadDataApi({ view_name: TABLES.todoRating, search_condition: conditions.join(" and "), search_columns: [], order_by: "id asc", args, }); if (res.isSuccess) set({ todoRatings: res.data as TodoRating[] }); }, fetchTodoFiles: async (familyId: number, todoId?: number) => { if (!familyId) return; const conditions = ["family_id = ?"]; const args: any[] = [familyId]; if (todoId !== undefined) { conditions.push("todo_id = ?"); args.push(todoId); } const res = await loadDataApi({ view_name: TABLES.todoFile, search_condition: conditions.join(" and "), search_columns: [], order_by: "sort_order asc, id asc", args, }); if (res.isSuccess) set({ todoFiles: res.data as TodoFile[] }); }, fetchMembers: async (familyId: number) => { if (!familyId) return; const res = await loadDataApi({ view_name: "v_family_member", search_condition: "family_id = ?", search_columns: ["user_id", "nickname", "avatar"], order_by: "id asc", args: [familyId], }); if (res.isSuccess) set({ members: (res.data as TodoFamilyMember[]) ?? [] }); }, saveTodoChanges: async (changes: TodoSaveChanges) => { const res = await saveDataApi([ { table_name: TABLES.todo, key_field: "id", ...normalizeChanges(changes) }, ]); return res.isSuccess; }, saveTodoLogChanges: async (changes: TodoSaveChanges) => { const res = await saveDataApi([ { table_name: TABLES.todoLog, key_field: "id", ...normalizeChanges(changes) }, ]); return res.isSuccess; }, saveTodoRatingChanges: async (changes: TodoSaveChanges) => { const res = await saveDataApi([ { table_name: TABLES.todoRating, key_field: "id", ...normalizeChanges(changes) }, ]); return res.isSuccess; }, saveTodoFileChanges: async (changes: TodoSaveChanges) => { const res = await saveDataApi([ { table_name: TABLES.todoFile, key_field: "id", ...normalizeChanges(changes) }, ]); return res.isSuccess; }, uploadFile: async (familyId: number, file: any) => { if (!familyId) return null; const res = await s3UploadFileApi([file], { familyId, module: "todo" }); if (!res.isSuccess) return null; return res.data?.[0] ?? null; }, createTodo: async ({ familyId, userId, name, description, priority, assigneeId, deadline, repeatType, remindAt, }) => { if (!familyId) return null; const [idRes, logIdRes] = await Promise.all([getUniqueIdApi(1), getUniqueIdApi(1)]); const todoId = idRes.data?.[0] ?? 0; const logId = logIdRes.data?.[0] ?? 0; if (!todoId) return null; const now = new Date().toISOString(); const ops: any[] = [ { table_name: TABLES.todo, key_field: "id", inserts: [ { id: todoId, family_id: familyId, name, description: description ?? "", priority, assignee_id: assigneeId ?? null, deadline: deadline ?? null, repeat_type: repeatType ?? 0, remind_at: remindAt ?? null, status: assigneeId ? TodoStatus.InProgress : TodoStatus.Unclaimed, create_time: now, create_by: userId, }, ], updates: [], deletes: [], }, ]; if (logId) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [ { id: logId, family_id: familyId, todo_id: todoId, type: assigneeId ? TodoLogType.Assign : TodoLogType.Create, content: assigneeId ? String(assigneeId) : name, create_by: userId, create_time: now, }, ], updates: [], deletes: [], }); const res = await saveDataApi(ops); if (res.isSuccess) { set((s) => ({ todos: [ ...s.todos, { id: todoId, family_id: familyId, name, description: description ?? "", status: assigneeId ? TodoStatus.InProgress : TodoStatus.Unclaimed, priority, repeat_type: repeatType ?? 0, remind_at: remindAt ?? null, assignee_id: assigneeId ?? null, deadline: deadline ?? null, sort_number: s.todos.length, create_time: now, create_by: userId, } as Todo, ], })); // 预定提醒通知 if (remindAt) { requestNotificationPermission().then((ok) => { if (ok) scheduleTodoReminder(todoId, name, remindAt); }); } return todoId; } return null; }, claimTodo: async ({ todoId, familyId, userId }) => { if (!familyId) return false; const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; const now = new Date().toISOString(); const ops: any[] = [ { table_name: TABLES.todo, key_field: "id", inserts: [], updates: [ { id: todoId, status: TodoStatus.InProgress, assignee_id: userId, update_time: now, update_by: userId, }, ], deletes: [], }, ]; if (logId) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [ { id: logId, family_id: familyId, todo_id: todoId, type: TodoLogType.Claim, create_by: userId, create_time: now, }, ], updates: [], deletes: [], }); const res = await saveDataApi(ops); if (res.isSuccess) set((s) => ({ todos: s.todos.map((t) => t.id === todoId ? { ...t, status: TodoStatus.InProgress, assignee_id: userId } : t, ), })); return res.isSuccess; }, rejectTodo: async ({ todoId, familyId, userId }) => { if (!familyId) return false; const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; const now = new Date().toISOString(); const ops: any[] = [ { table_name: TABLES.todo, key_field: "id", inserts: [], updates: [ { id: todoId, status: TodoStatus.Unclaimed, assignee_id: null, update_time: now, update_by: userId, }, ], deletes: [], }, ]; if (logId) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [ { id: logId, family_id: familyId, todo_id: todoId, type: TodoLogType.Reject, create_by: userId, create_time: now, }, ], updates: [], deletes: [], }); const res = await saveDataApi(ops); if (res.isSuccess) set((s) => ({ todos: s.todos.map((t) => t.id === todoId ? { ...t, status: TodoStatus.Unclaimed, assignee_id: null } : t, ), })); return res.isSuccess; }, transferTodo: async ({ todoId, familyId, userId, newAssigneeId }) => { if (!familyId) return false; const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; const now = new Date().toISOString(); const newStatus = newAssigneeId ? TodoStatus.InProgress : TodoStatus.Unclaimed; const ops: any[] = [ { table_name: TABLES.todo, key_field: "id", inserts: [], updates: [ { id: todoId, status: newStatus, assignee_id: newAssigneeId, update_time: now, update_by: userId, }, ], deletes: [], }, ]; if (logId) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [ { id: logId, family_id: familyId, todo_id: todoId, type: TodoLogType.Transfer, content: String(newAssigneeId ?? ""), create_by: userId, create_time: now, }, ], updates: [], deletes: [], }); const res = await saveDataApi(ops); if (res.isSuccess) set((s) => ({ todos: s.todos.map((t) => t.id === todoId ? { ...t, status: newStatus, assignee_id: newAssigneeId } : t, ), })); return res.isSuccess; }, completeTodo: async ({ todoId, familyId, userId, completeNote, fileInserts }) => { if (!familyId) return false; const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; const now = new Date().toISOString(); const ops: any[] = [ { table_name: TABLES.todo, key_field: "id", inserts: [], updates: [ { id: todoId, status: TodoStatus.Completed, completed_at: now, complete_note: completeNote ?? "", update_time: now, update_by: userId, }, ], deletes: [], }, ]; if (logId) { ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [ { id: logId, family_id: familyId, todo_id: todoId, type: TodoLogType.Complete, content: completeNote ?? "", create_by: userId, create_time: now, }, ], updates: [], deletes: [], }); } // 附件 if (fileInserts && fileInserts.length > 0) { ops.push({ table_name: TABLES.todoFile, key_field: "id", inserts: fileInserts.map((f) => ({ ...f, todo_log_id: logId, todo_id: todoId, create_by: userId, create_time: now, })), updates: [], deletes: [], }); } const res = await saveDataApi(ops); if (res.isSuccess) { set((s) => ({ todos: s.todos.map((t) => t.id === todoId ? { ...t, status: TodoStatus.Completed, completed_at: now, complete_note: completeNote ?? "", } : t, ), })); // 取消提醒通知 cancelTodoReminders(todoId); // 重复任务:自动创建下一个实例 const todo = get().todos.find((t) => t.id === todoId); if (todo && todo.repeat_type !== TodoRepeatType.None) { const nextDl = nextDeadline(new Date(now), todo.repeat_type, todo.deadline); const nextRemind = todo.remind_at ? nextDeadline(new Date(now), todo.repeat_type, todo.remind_at) : null; get().createTodo({ familyId, userId, name: todo.name, description: todo.description, priority: todo.priority, assigneeId: todo.assignee_id, deadline: nextDl, repeatType: todo.repeat_type, remindAt: nextRemind, }); } } return res.isSuccess; }, rateTodo: async ({ todoId, familyId, userId, rating, content }) => { if (!familyId) return false; const [rateId, logId] = await Promise.all([getUniqueIdApi(1), getUniqueIdApi(1)]); const rid = rateId.data?.[0] ?? 0; const lid = logId.data?.[0] ?? 0; if (!rid) return false; const now = new Date().toISOString(); const ops: any[] = [ { table_name: TABLES.todoRating, key_field: "id", inserts: [ { id: rid, family_id: familyId, todo_id: todoId, rater_id: userId, rating, content: content ?? "", create_by: userId, create_time: now, }, ], updates: [], deletes: [], }, ]; if (lid) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [ { id: lid, family_id: familyId, todo_id: todoId, type: TodoLogType.Rate, ref_id: rid, create_by: userId, create_time: now, }, ], updates: [], deletes: [], }); const res = await saveDataApi(ops); if (res.isSuccess) set((s) => ({ todoRatings: [ ...s.todoRatings, { id: rid, family_id: familyId, todo_id: todoId, rater_id: userId, rating, content: content ?? "", create_by: userId, create_time: now, } as TodoRating, ], })); return res.isSuccess; }, }));