20260706170339
This commit is contained in:
1 parent
ae99f50f6d
commit
4d1e6c8b8f
50 files changed
+3045
-2249
No files matched your search
@@ -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;
|
||||
},
|
||||
}));
|
||||
@@ -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 ?? [],
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}));
|
||||
@@ -5,3 +5,5 @@ export * from "./favorite";
|
||||
export * from "./chat";
|
||||
export * from "./clothing";
|
||||
export * from "./meal";
|
||||
export * from "./todo";
|
||||
export * from "./anniversary";
|
||||
@@ -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