Files
workspace/code/app/app-rn/src/store/todo.ts
T
2026-07-19 21:57:40 +08:00

647 lines
17 KiB
TypeScript

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<void>;
fetchTodos: (familyId: number) => Promise<void>;
fetchTodoLogs: (familyId: number, todoId?: number) => Promise<void>;
fetchTodoRatings: (familyId: number, todoId?: number) => Promise<void>;
fetchTodoFiles: (familyId: number, todoId?: number) => Promise<void>;
fetchMembers: (familyId: number) => Promise<void>;
saveTodoChanges: (changes: TodoSaveChanges<Todo>) => Promise<boolean>;
saveTodoLogChanges: (changes: TodoSaveChanges<TodoLog>) => Promise<boolean>;
saveTodoRatingChanges: (changes: TodoSaveChanges<TodoRating>) => Promise<boolean>;
saveTodoFileChanges: (changes: TodoSaveChanges<TodoFile>) => Promise<boolean>;
uploadFile: (familyId: number, file: any) => Promise<string | null>;
createTodo: (params: {
familyId: number;
userId: number;
name: string;
description?: string;
priority: number;
assigneeId?: number | null;
deadline?: string | null;
repeatType?: number;
remindAt?: string | null;
}) => Promise<number | null>;
claimTodo: (params: { todoId: number; familyId: number; userId: number }) => Promise<boolean>;
rejectTodo: (params: { todoId: number; familyId: number; userId: number }) => Promise<boolean>;
transferTodo: (params: {
todoId: number;
familyId: number;
userId: number;
newAssigneeId: number | null;
}) => Promise<boolean>;
completeTodo: (params: {
todoId: number;
familyId: number;
userId: number;
completeNote?: string;
fileInserts?: Partial<TodoFile>[];
}) => Promise<boolean>;
rateTodo: (params: {
todoId: number;
familyId: number;
userId: number;
rating: number;
content?: string;
}) => Promise<boolean>;
}
function normalizeChanges<T>(changes: TodoSaveChanges<T>) {
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<TodoState>((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<Todo>) => {
const res = await saveDataApi([
{ table_name: TABLES.todo, key_field: "id", ...normalizeChanges(changes) },
]);
return res.isSuccess;
},
saveTodoLogChanges: async (changes: TodoSaveChanges<TodoLog>) => {
const res = await saveDataApi([
{ table_name: TABLES.todoLog, key_field: "id", ...normalizeChanges(changes) },
]);
return res.isSuccess;
},
saveTodoRatingChanges: async (changes: TodoSaveChanges<TodoRating>) => {
const res = await saveDataApi([
{ table_name: TABLES.todoRating, key_field: "id", ...normalizeChanges(changes) },
]);
return res.isSuccess;
},
saveTodoFileChanges: async (changes: TodoSaveChanges<TodoFile>) => {
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;
},
}));