20260706170339
This commit is contained in:
1 parent
ae99f50f6d
commit
4d1e6c8b8f
50 files changed
+3045
-2249
No files matched your search
@@ -0,0 +1,277 @@
|
||||
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);
|
||||
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;
|
||||
},
|
||||
}));
|
||||
Reference in new issue
Block a user