20260706170339

This commit is contained in:
oneao committed 2026-07-06 17:03:39 +08:00
1 parent ae99f50f6d
commit 4d1e6c8b8f
50 files changed
+3045 -2249

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
import { create } from "zustand";
import { loadDataApi, saveDataApi, getUniqueIdApi } from "@/request/api";
import { Anniversary, AnniversarySaveChanges } from "@/types/anniversary";
const TABLES = { anniversary: "b_anniversary" } as const;
interface AnniversaryState {
items: Anniversary[];
clear: () => void;
fetchAll: (familyId: number) => Promise<void>;
saveChanges: (changes: AnniversarySaveChanges<Anniversary>) => Promise<boolean>;
create: (params: { familyId: number; name: string; month: number; day: number; type: number; remindBefore: number; userId: number }) => Promise<number | null>;
}
function normalizeChanges<T>(changes: AnniversarySaveChanges<T>) {
return { inserts: changes.inserts ?? [], updates: changes.updates ?? [], deletes: changes.deletes ?? [] };
}
export const useAnniversaryStore = create<AnniversaryState>((set, get) => ({
items: [],
clear: () => set({ items: [] }),
fetchAll: async (familyId: number) => {
if (!familyId) return;
const res = await loadDataApi({ view_name: TABLES.anniversary, search_condition: "family_id = ?", search_columns: [], order_by: "month asc, day asc, id asc", args: [familyId] });
if (res.isSuccess) set({ items: res.data as Anniversary[] });
},
saveChanges: async (changes: AnniversarySaveChanges<Anniversary>) => {
const res = await saveDataApi([{ table_name: TABLES.anniversary, key_field: "id", ...normalizeChanges(changes) }]);
return res.isSuccess;
},
create: async ({ familyId, name, month, day, type, remindBefore, userId }) => {
if (!familyId) return null;
const idRes = await getUniqueIdApi(1);
const newId = idRes.data?.[0] ?? 0;
if (!newId) return null;
const now = new Date().toISOString();
const ok = await get().saveChanges({
inserts: [{ id: newId, family_id: familyId, name, month, day, type, remind_before: remindBefore, create_by: userId, create_time: now }],
});
if (ok) {
set((s) => ({ items: [...s.items, { id: newId, family_id: familyId, name, month, day, type, remind_before: remindBefore, sort_number: 0, create_by: userId, create_time: now } as Anniversary] }));
return newId;
}
return null;
},
}));
+91 -36
View File
@@ -18,7 +18,8 @@ const TABLES = {
outfitClothes: "b_clothing_outfit_clothes",
category: "b_clothing_category",
tag: "b_clothing_tag",
tagRelation: "b_clothing_tag_relation",
clothesTag: "b_clothing_clothes_tag",
outfitTag: "b_clothing_outfit_tag",
familyMember: "b_family_member",
} as const;
@@ -259,30 +260,54 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
) => {
if (!familyId) return;
const conditions = ["family_id = ?"];
const args: any[] = [familyId];
const queries: Promise<any>[] = [];
if (targetType !== undefined) {
conditions.push("target_type = ?");
args.push(targetType);
if (targetType === undefined || targetType === ClothingTargetType.Clothes) {
const conds = ["family_id = ?"];
const args: any[] = [familyId];
if (targetId !== undefined && targetType === ClothingTargetType.Clothes) {
conds.push("clothes_id = ?");
args.push(targetId);
}
queries.push(
loadDataApi({
view_name: TABLES.clothesTag,
search_condition: conds.join(" and "),
search_columns: [],
order_by: "clothes_id asc, id asc",
args,
}).then((res) =>
res.isSuccess
? (res.data as any[]).map((r: any) => ({ ...r, clothes_id: r.clothes_id ?? null }))
: [],
),
);
}
if (targetId !== undefined) {
conditions.push("target_id = ?");
args.push(targetId);
if (targetType === undefined || targetType === ClothingTargetType.Outfit) {
const conds = ["family_id = ?"];
const args: any[] = [familyId];
if (targetId !== undefined && targetType === ClothingTargetType.Outfit) {
conds.push("outfit_id = ?");
args.push(targetId);
}
queries.push(
loadDataApi({
view_name: TABLES.outfitTag,
search_condition: conds.join(" and "),
search_columns: [],
order_by: "outfit_id asc, id asc",
args,
}).then((res) =>
res.isSuccess
? (res.data as any[]).map((r: any) => ({ ...r, outfit_id: r.outfit_id ?? null }))
: [],
),
);
}
const res = await loadDataApi({
view_name: TABLES.tagRelation,
search_condition: conditions.join(" and "),
search_columns: [],
order_by: "target_type asc, target_id asc, id asc",
args,
});
if (res.isSuccess) {
set({ tagRelations: res.data as ClothingTagRelation[] });
}
const results = await Promise.all(queries);
set({ tagRelations: results.flat() as ClothingTagRelation[] });
},
fetchMembers: async (familyId: number) => {
@@ -391,14 +416,27 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
},
saveTagRelationChanges: async (changes: ClothingSaveChanges<ClothingTagRelation>) => {
const res = await saveDataApi([
{ table_name: TABLES.tagRelation, key_field: "id", ...normalizeChanges(changes) },
]);
return res.isSuccess;
const { inserts, updates, deletes } = normalizeChanges(changes);
const hasClothes = [...inserts, ...updates].some((r) => r.clothes_id !== undefined);
const hasOutfit = [...inserts, ...updates].some((r) => r.outfit_id !== undefined);
if (hasClothes) {
const res = await saveDataApi([
{ table_name: TABLES.clothesTag, key_field: "id", inserts, updates, deletes },
]);
if (!res.isSuccess) return false;
}
if (hasOutfit) {
const res = await saveDataApi([
{ table_name: TABLES.outfitTag, key_field: "id", inserts, updates, deletes },
]);
if (!res.isSuccess) return false;
}
return true;
},
saveMetadataBundle: async (payload) => {
const res = await saveDataApi([
const ops: any[] = [
{
table_name: TABLES.category,
key_field: "id",
@@ -413,15 +451,32 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
updates: payload.tagUpdates ?? [],
deletes: payload.tagDeletes ?? [],
},
{
table_name: TABLES.tagRelation,
key_field: "id",
inserts: payload.tagRelationInserts ?? [],
updates: payload.tagRelationUpdates ?? [],
deletes: payload.tagRelationDeletes ?? [],
},
]);
];
const tagInserts = payload.tagRelationInserts ?? [];
const tagUpdates = payload.tagRelationUpdates ?? [];
const tagDeletes = payload.tagRelationDeletes ?? [];
if (tagInserts.some((r) => r.clothes_id !== undefined) || tagDeletes.length > 0) {
ops.push({
table_name: TABLES.clothesTag,
key_field: "id",
inserts: tagInserts.filter((r) => r.clothes_id !== undefined),
updates: tagUpdates.filter((r) => r.clothes_id !== undefined),
deletes: tagDeletes,
});
}
if (tagInserts.some((r) => r.outfit_id !== undefined)) {
ops.push({
table_name: TABLES.outfitTag,
key_field: "id",
inserts: tagInserts.filter((r) => r.outfit_id !== undefined),
updates: tagUpdates.filter((r) => r.outfit_id !== undefined),
deletes: [],
});
}
const res = await saveDataApi(ops);
return res.isSuccess;
},
@@ -435,7 +490,7 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
deletes: payload.clothesDeletes ?? [],
},
{
table_name: TABLES.tagRelation,
table_name: TABLES.clothesTag,
key_field: "id",
inserts: payload.tagRelationInserts ?? [],
updates: payload.tagRelationUpdates ?? [],
@@ -470,7 +525,7 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
deletes: payload.outfitClothesDeletes ?? [],
},
{
table_name: TABLES.tagRelation,
table_name: TABLES.clothesTag,
key_field: "id",
inserts: [],
updates: [],
@@ -498,7 +553,7 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
deletes: payload.clothesDeletes ?? [],
},
{
table_name: TABLES.tagRelation,
table_name: TABLES.outfitTag,
key_field: "id",
inserts: payload.tagRelationInserts ?? [],
updates: payload.tagRelationUpdates ?? [],
+107 -15
View File
@@ -1,9 +1,17 @@
import { create } from "zustand";
import { loadDataApi } from "@/request/api";
import { FinanceCategory, FinanceCategoryStatus, FinanceType } from "@/types/finance";
import { loadDataApi, saveDataApi, getUniqueIdApi } from "@/request/api";
import {
FinanceAuto,
FinanceAutoRepeatType,
FinanceCategory,
FinanceCategoryStatus,
FinanceType,
} from "@/types/finance";
import { shouldRun, getMissedDates } from "@/helpers/financeAuto";
interface FinanceState {
categories: FinanceCategory[];
autoRules: FinanceAuto[];
setCategories: (categories: FinanceCategory[]) => void;
clearCategories: () => void;
@@ -13,12 +21,18 @@ interface FinanceState {
loadCategories: (familyId: number) => Promise<void>;
upsertCategory: (category: FinanceCategory) => void;
removeCategory: (id: number) => void;
// 自动记账
fetchAutoRules: (familyId: number) => Promise<void>;
saveAutoRule: (rule: Partial<FinanceAuto>, isDelete?: boolean) => Promise<boolean>;
runAutoRules: (familyId: number, userId: number) => Promise<number>;
}
export const useFinanceStore = create<FinanceState>((set, get) => ({
categories: [],
autoRules: [],
setCategories: (categories: FinanceCategory[]) => set({ categories }),
clearCategories: () => set({ categories: [] }),
clearCategories: () => set({ categories: [], autoRules: [] }),
getCategories: () => get().categories,
getExpenseCategories: () =>
@@ -27,7 +41,6 @@ export const useFinanceStore = create<FinanceState>((set, get) => ({
get().categories.filter((category) => category.type === FinanceType.Income),
loadCategories: async (familyId: number) => {
if (!familyId) return;
const res = await loadDataApi({
view_name: "b_finance_category",
search_columns: [],
@@ -35,24 +48,103 @@ export const useFinanceStore = create<FinanceState>((set, get) => ({
order_by: "type asc, sort_number asc, id asc",
args: [familyId, FinanceCategoryStatus.Enabled],
});
if (res.isSuccess) {
set({ categories: res.data as FinanceCategory[] });
}
if (res.isSuccess) set({ categories: res.data as FinanceCategory[] });
},
upsertCategory: (category: FinanceCategory) =>
set((state) => {
const index = state.categories.findIndex((item) => item.id === category.id);
if (index < 0) {
return { categories: [...state.categories, category] };
}
if (index < 0) return { categories: [...state.categories, category] };
const categories = [...state.categories];
categories[index] = category;
return { categories };
}),
removeCategory: (id: number) =>
set((state) => ({
categories: state.categories.filter((category) => category.id !== id),
})),
set((state) => ({ categories: state.categories.filter((category) => category.id !== id) })),
// ======== 自动记账 ========
fetchAutoRules: async (familyId: number) => {
if (!familyId) return;
const res = await loadDataApi({
view_name: "b_finance_auto",
search_condition: "family_id = ? and status = 1",
search_columns: [],
order_by: "id asc",
args: [familyId],
});
if (res.isSuccess) set({ autoRules: res.data as FinanceAuto[] });
},
saveAutoRule: async (rule: Partial<FinanceAuto>, isDelete = false) => {
if (isDelete && rule.id) {
const res = await saveDataApi([{
table_name: "b_finance_auto", key_field: "id",
inserts: [], updates: [], deletes: [{ id: rule.id }],
}]);
if (res.isSuccess) set((s) => ({ autoRules: s.autoRules.filter((r) => r.id !== rule.id) }));
return res.isSuccess;
}
if (rule.id) {
const res = await saveDataApi([{
table_name: "b_finance_auto", key_field: "id",
inserts: [], updates: [rule], deletes: [],
}]);
return res.isSuccess;
}
// 新建
const idRes = await getUniqueIdApi(1);
const newId = idRes.data?.[0] ?? 0;
if (!newId) return false;
const res = await saveDataApi([{
table_name: "b_finance_auto", key_field: "id",
inserts: [{ ...rule, id: newId }], updates: [], deletes: [],
}]);
if (res.isSuccess) {
set((s) => ({ autoRules: [...s.autoRules, { ...rule, id: newId } as FinanceAuto] }));
}
return res.isSuccess;
},
runAutoRules: async (familyId: number, userId: number) => {
const rules = get().autoRules;
if (rules.length === 0) return 0;
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
let count = 0;
for (const rule of rules) {
if (!shouldRun(rule.repeat_type, rule.repeat_date ?? "", rule.last_run)) continue;
const missedDates = getMissedDates(rule.repeat_type, rule.repeat_date ?? "", rule.last_run);
if (missedDates.length === 0) continue;
const inserts: any[] = [];
const ids = (await getUniqueIdApi(missedDates.length)).data ?? [];
for (let i = 0; i < missedDates.length; i++) {
if (!ids[i]) continue;
inserts.push({
id: ids[i], family_id: familyId, category_id: rule.category_id,
amount: rule.amount, remark: rule.remark || "",
type: rule.type, record_time: missedDates[i],
create_by: userId, update_by: userId,
});
}
if (inserts.length > 0) {
await saveDataApi([{ table_name: "b_finance", key_field: "id", inserts, updates: [], deletes: [] }]);
// 更新 last_run 为最近的一天
const lastDate = missedDates[missedDates.length - 1];
await saveDataApi([{
table_name: "b_finance_auto", key_field: "id",
inserts: [], updates: [{ id: rule.id, last_run: lastDate }], deletes: [],
}]);
set((s) => ({
autoRules: s.autoRules.map((r) => r.id === rule.id ? { ...r, last_run: lastDate } : r),
}));
count += inserts.length;
}
}
return count;
},
}));
+2
View File
@@ -5,3 +5,5 @@ export * from "./favorite";
export * from "./chat";
export * from "./clothing";
export * from "./meal";
export * from "./todo";
export * from "./anniversary";
+277
View File
@@ -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;
},
}));