feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
apps/web/stores/useMemberStore.ts
Normal file
135
apps/web/stores/useMemberStore.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { Department, Member, RoleItem, PasswordRule } from '@/lib/members';
|
||||
import { DEFAULT_PASSWORD_RULE, generatePassword } from '@/lib/members';
|
||||
|
||||
const STORAGE_KEY = 'ftb_members_v1';
|
||||
|
||||
const PRESET_DEPARTMENTS: Department[] = [
|
||||
{ id: 'dept-1', name: '产品部', order: 1, createdAt: '2024-01-01' },
|
||||
{ id: 'dept-2', name: '技术部', order: 2, createdAt: '2024-01-01' },
|
||||
{ id: 'dept-2-1', name: '前端组', parentId: 'dept-2', order: 1, createdAt: '2024-01-01' },
|
||||
{ id: 'dept-2-2', name: '后端组', parentId: 'dept-2', order: 2, createdAt: '2024-01-01' },
|
||||
{ id: 'dept-2-3', name: '测试组', parentId: 'dept-2', order: 3, createdAt: '2024-01-01' },
|
||||
{ id: 'dept-3', name: '设计部', order: 3, createdAt: '2024-01-01' },
|
||||
{ id: 'dept-4', name: '运营部', order: 4, createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
const PRESET_ROLES: RoleItem[] = [
|
||||
{ id: 'role-admin', name: '超级管理员', description: '拥有系统全部权限', createdAt: '2024-01-01', isSystem: true },
|
||||
{ id: 'role-pm', name: '产品经理', description: '管理产品和需求', createdAt: '2024-01-01' },
|
||||
{ id: 'role-dev', name: '开发工程师', description: '负责开发任务', createdAt: '2024-01-01' },
|
||||
{ id: 'role-test', name: '测试工程师', description: '负责测试任务', createdAt: '2024-01-01' },
|
||||
{ id: 'role-design', name: '设计师', description: '负责UI/UX设计', createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
const MOCK_MEMBERS: Member[] = [
|
||||
{ id: 'm-1', name: '张三', departmentId: 'dept-1', roleId: 'role-pm', phone: '13800138001', email: 'zhangsan@company.com', password: 'Ftb@2024', createdAt: '2024-01-10' },
|
||||
{ id: 'm-2', name: '李四', departmentId: 'dept-2-1', roleId: 'role-dev', phone: '13900139002', email: 'lisi@company.com', password: 'Ftb@2024', createdAt: '2024-01-12' },
|
||||
{ id: 'm-3', name: '王五', departmentId: 'dept-2-2', roleId: 'role-dev', phone: '13700137003', email: 'wangwu@company.com', password: 'Ftb@2024', createdAt: '2024-01-15' },
|
||||
{ id: 'm-4', name: '赵六', departmentId: 'dept-2-3', roleId: 'role-test', phone: '13600136004', email: 'zhaoliu@company.com', password: 'Ftb@2024', createdAt: '2024-02-01' },
|
||||
{ id: 'm-5', name: '孙七', departmentId: 'dept-3', roleId: 'role-design', phone: '13500135005', email: 'sunqi@company.com', password: 'Ftb@2024', createdAt: '2024-02-10' },
|
||||
{ id: 'm-6', name: '周八', departmentId: 'dept-4', roleId: 'role-pm', phone: '13400134006', email: 'zhouba@company.com', password: 'Ftb@2024', createdAt: '2024-02-15' },
|
||||
{ id: 'm-7', name: '吴九', departmentId: 'dept-2-1', roleId: 'role-dev', phone: '13300133007', email: 'wujiu@company.com', password: 'Ftb@2024', createdAt: '2024-03-01' },
|
||||
{ id: 'm-8', name: '陈十', departmentId: 'dept-1', roleId: 'role-admin', phone: '13200132008', email: 'chenshi@company.com', password: 'Ftb@2024', createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
interface MemberState {
|
||||
departments: Department[];
|
||||
members: Member[];
|
||||
roles: RoleItem[];
|
||||
passwordRule: PasswordRule;
|
||||
loading: boolean;
|
||||
fetchMembers: () => void;
|
||||
updatePasswordRule: (rule: PasswordRule) => void;
|
||||
createDepartment: (data: Omit<Department, 'id' | 'createdAt'>) => void;
|
||||
updateDepartment: (id: string, data: Partial<Department>) => void;
|
||||
deleteDepartment: (id: string) => void;
|
||||
createMember: (data: Omit<Member, 'id' | 'createdAt'>) => void;
|
||||
updateMember: (id: string, data: Partial<Member>) => void;
|
||||
deleteMember: (id: string) => void;
|
||||
createRole: (data: Omit<RoleItem, 'id' | 'createdAt'>) => void;
|
||||
updateRole: (id: string, data: Partial<RoleItem>) => void;
|
||||
deleteRole: (id: string) => void;
|
||||
}
|
||||
|
||||
function saveLocal(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch {}
|
||||
}
|
||||
|
||||
function loadLocal(): { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const useMemberStore = create<MemberState>((set, get) => ({
|
||||
departments: PRESET_DEPARTMENTS,
|
||||
members: MOCK_MEMBERS,
|
||||
roles: PRESET_ROLES,
|
||||
passwordRule: DEFAULT_PASSWORD_RULE,
|
||||
loading: false,
|
||||
|
||||
fetchMembers: () => {
|
||||
const cached = loadLocal();
|
||||
if (cached) set({ departments: cached.departments, members: cached.members, roles: cached.roles, passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE });
|
||||
},
|
||||
|
||||
updatePasswordRule: (rule) => {
|
||||
set({ passwordRule: rule });
|
||||
saveLocal({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule });
|
||||
},
|
||||
|
||||
createDepartment: (data) => {
|
||||
const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const departments = [...get().departments, dept];
|
||||
set({ departments });
|
||||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
updateDepartment: (id, data) => {
|
||||
const departments = get().departments.map((d) => d.id === id ? { ...d, ...data } : d);
|
||||
set({ departments });
|
||||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
deleteDepartment: (id) => {
|
||||
const departments = get().departments.filter((d) => d.id !== id && d.parentId !== id);
|
||||
set({ departments });
|
||||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
|
||||
createMember: (data) => {
|
||||
const member: Member = { ...data, id: `m-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const members = [...get().members, member];
|
||||
set({ members });
|
||||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
updateMember: (id, data) => {
|
||||
const members = get().members.map((m) => m.id === id ? { ...m, ...data } : m);
|
||||
set({ members });
|
||||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
deleteMember: (id) => {
|
||||
const members = get().members.filter((m) => m.id !== id);
|
||||
set({ members });
|
||||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
|
||||
createRole: (data) => {
|
||||
const role: RoleItem = { ...data, id: `role-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const roles = [...get().roles, role];
|
||||
set({ roles });
|
||||
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
updateRole: (id, data) => {
|
||||
const roles = get().roles.map((r) => r.id === id ? { ...r, ...data } : r);
|
||||
set({ roles });
|
||||
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
deleteRole: (id) => {
|
||||
const roles = get().roles.filter((r) => r.id !== id);
|
||||
set({ roles });
|
||||
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
}));
|
||||
116
apps/web/stores/useOvertimeStore.ts
Normal file
116
apps/web/stores/useOvertimeStore.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { OvertimeRecord } from '@/lib/overtime';
|
||||
import type { DictItem } from '@/lib/requirement';
|
||||
import { calcDuration } from '@/lib/overtime';
|
||||
|
||||
const STORAGE_KEY = 'ftb_overtime_v1';
|
||||
|
||||
const PRESET_REASONS: DictItem[] = [
|
||||
{ id: 'reason-1', name: '需求变更', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-2', name: '需求新增', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-3', name: '版本冲刺', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-4', name: 'Bug修复', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-5', name: '线上问题修复', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-6', name: '测试问题整改', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-7', name: '技术难点攻关', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-8', name: '联调问题', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-9', name: '上游交付延误', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-10', name: '工作量超负荷', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-11', name: '返工修改', createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
const MOCK_RECORDS: OvertimeRecord[] = [
|
||||
{ id: 'ot-1', projectId: 'p1', versionId: 'v2', person: '张三', startTime: '2024-12-10T19:00', endTime: '2024-12-10T22:30', duration: 3.5, reasonId: 'reason-3', createdAt: '2024-12-10' },
|
||||
{ id: 'ot-2', projectId: 'p2', versionId: 'v5', person: '李四', startTime: '2024-12-11T18:30', endTime: '2024-12-11T21:00', duration: 2.5, reasonId: 'reason-1', createdAt: '2024-12-11' },
|
||||
{ id: 'ot-3', projectId: 'p1', versionId: 'v2', person: '王五', startTime: '2024-12-12T19:00', endTime: '2024-12-12T23:00', duration: 4, reasonId: 'reason-4', createdAt: '2024-12-12' },
|
||||
{ id: 'ot-4', projectId: 'p3', person: '张三', startTime: '2024-12-13T18:00', endTime: '2024-12-13T20:30', duration: 2.5, reasonId: 'reason-7', createdAt: '2024-12-13' },
|
||||
{ id: 'ot-5', projectId: 'p2', versionId: 'v5', person: '赵六', startTime: '2024-12-14T19:30', endTime: '2024-12-15T01:00', duration: 5.5, reasonId: 'reason-5', remark: '线上紧急事故', createdAt: '2024-12-14' },
|
||||
];
|
||||
|
||||
interface OvertimeState {
|
||||
records: OvertimeRecord[];
|
||||
reasons: DictItem[];
|
||||
loading: boolean;
|
||||
fetchRecords: () => void;
|
||||
createRecord: (data: Omit<OvertimeRecord, 'id' | 'duration' | 'createdAt'>) => void;
|
||||
updateRecord: (id: string, data: Partial<OvertimeRecord>) => void;
|
||||
deleteRecord: (id: string) => void;
|
||||
addReason: (name: string) => void;
|
||||
updateReason: (id: string, name: string) => void;
|
||||
deleteReason: (id: string) => void;
|
||||
}
|
||||
|
||||
function saveLocal(data: { records: OvertimeRecord[]; reasons: DictItem[] }) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
|
||||
}
|
||||
|
||||
function loadLocal(): { records: OvertimeRecord[]; reasons: DictItem[] } | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
||||
records: MOCK_RECORDS,
|
||||
reasons: PRESET_REASONS,
|
||||
loading: false,
|
||||
|
||||
fetchRecords: () => {
|
||||
const cached = loadLocal();
|
||||
if (cached) set({ records: cached.records, reasons: cached.reasons });
|
||||
},
|
||||
|
||||
createRecord: (data) => {
|
||||
const duration = calcDuration(data.startTime, data.endTime);
|
||||
const record: OvertimeRecord = {
|
||||
...data,
|
||||
id: `ot-${Date.now()}`,
|
||||
duration,
|
||||
createdAt: new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
const records = [...get().records, record];
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
updateRecord: (id, data) => {
|
||||
const records = get().records.map((r) => {
|
||||
if (r.id !== id) return r;
|
||||
const updated = { ...r, ...data };
|
||||
if (data.startTime || data.endTime) {
|
||||
updated.duration = calcDuration(updated.startTime, updated.endTime);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
deleteRecord: (id) => {
|
||||
const records = get().records.filter((r) => r.id !== id);
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
addReason: (name) => {
|
||||
const item: DictItem = { id: `reason-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const reasons = [...get().reasons, item];
|
||||
set({ reasons });
|
||||
saveLocal({ records: get().records, reasons });
|
||||
},
|
||||
|
||||
updateReason: (id, name) => {
|
||||
const reasons = get().reasons.map((r) => r.id === id ? { ...r, name } : r);
|
||||
set({ reasons });
|
||||
saveLocal({ records: get().records, reasons });
|
||||
},
|
||||
|
||||
deleteReason: (id) => {
|
||||
const reasons = get().reasons.filter((r) => r.id !== id);
|
||||
set({ reasons });
|
||||
saveLocal({ records: get().records, reasons });
|
||||
},
|
||||
}));
|
||||
@@ -4,6 +4,7 @@ import { create } from 'zustand';
|
||||
import { Product } from '@ftb/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Stage, Role } from '@/lib/stage';
|
||||
import type { Priority, VersionLinks } from '@/lib/derive';
|
||||
|
||||
interface VersionMember {
|
||||
role: Role;
|
||||
@@ -27,6 +28,8 @@ interface VersionItem {
|
||||
expectedReleaseDate?: string | null;
|
||||
members?: VersionMember[];
|
||||
progress?: RoleProgress[];
|
||||
priority?: Priority;
|
||||
links?: VersionLinks;
|
||||
}
|
||||
|
||||
const MOCK_OVERVIEW: ProductOverview[] = [
|
||||
@@ -43,14 +46,14 @@ const MOCK_OVERVIEW: ProductOverview[] = [
|
||||
],
|
||||
versions: [
|
||||
{ id: 'v1', name: '考勤V1.0', status: 'released', releaseDate: '2024-02-01', createdAt: '2024-01-20', currentStage: 'released', startDate: '2024-01-20', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'frontend', percent: 100, daysSpent: 5 }, { role: 'backend', percent: 100, daysSpent: 4 }, { role: 'testing', percent: 100, daysSpent: 2 }] },
|
||||
{ id: 'v2', name: '考勤V1.2', status: 'developing', releaseDate: null, createdAt: '2024-04-15', currentStage: 'dev', startDate: '2024-04-15', expectedReleaseDate: '2024-06-10', members: [{ role: 'product', name: '张三' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 5 }, { role: 'ui', percent: 100, daysSpent: 7 }, { role: 'frontend', percent: 60, daysSpent: 10 }, { role: 'backend', percent: 40, daysSpent: 12 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
|
||||
{ id: 'v2', name: '考勤V1.2', status: 'developing', releaseDate: null, createdAt: '2024-04-15', currentStage: 'dev', startDate: '2024-04-15', expectedReleaseDate: '2024-06-10', priority: 'P1', links: { research: 'https://docs.example.com/kaoqin-v12', prototype: 'https://axure.example.com/kaoqin-v12' }, members: [{ role: 'product', name: '张三' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 5 }, { role: 'ui', percent: 100, daysSpent: 7 }, { role: 'frontend', percent: 60, daysSpent: 10 }, { role: 'backend', percent: 40, daysSpent: 12 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
|
||||
{ id: 'v3', name: '考勤V1.3', status: 'planned', releaseDate: null, createdAt: '2024-06-01' },
|
||||
{ id: 'v4', name: '电子合同V1.0', status: 'released', releaseDate: '2024-03-20', createdAt: '2024-03-01', currentStage: 'released', startDate: '2024-03-01', members: [{ role: 'product', name: '李四' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 100, daysSpent: 8 }, { role: 'backend', percent: 100, daysSpent: 6 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
|
||||
{ id: 'v5', name: '电子合同V2.0', status: 'released', releaseDate: '2024-06-01', createdAt: '2024-05-20', currentStage: 'released', startDate: '2024-05-01', members: [{ role: 'product', name: '李四' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 5 }, { role: 'ui', percent: 100, daysSpent: 6 }, { role: 'frontend', percent: 100, daysSpent: 12 }, { role: 'backend', percent: 100, daysSpent: 10 }, { role: 'testing', percent: 100, daysSpent: 4 }] },
|
||||
{ id: 'v6', name: '电子合同V2.1', status: 'developing', releaseDate: null, createdAt: '2024-06-05', currentStage: 'ui_design', startDate: '2024-06-05', expectedReleaseDate: '2024-07-15', members: [{ role: 'product', name: '李四' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'ui', percent: 50, daysSpent: 4 }, { role: 'frontend', percent: 0, daysSpent: 0 }, { role: 'backend', percent: 0, daysSpent: 0 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
|
||||
{ id: 'v6', name: '电子合同V2.1', status: 'developing', releaseDate: null, createdAt: '2024-06-05', currentStage: 'ui_design', startDate: '2024-06-05', expectedReleaseDate: '2024-07-15', priority: 'P2', links: { research: 'https://docs.example.com/contract-v21' }, members: [{ role: 'product', name: '李四' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'ui', percent: 50, daysSpent: 4 }, { role: 'frontend', percent: 0, daysSpent: 0 }, { role: 'backend', percent: 0, daysSpent: 0 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
|
||||
{ id: 'v7', name: '值班V1.0', status: 'released', releaseDate: '2024-04-01', createdAt: '2024-03-15', currentStage: 'released', startDate: '2024-03-15', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'frontend', percent: 100, daysSpent: 8 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
|
||||
{ id: 'v8', name: '值班V1.1', status: 'released', releaseDate: '2024-05-15', createdAt: '2024-05-01', currentStage: 'released', startDate: '2024-05-01', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 2 }, { role: 'frontend', percent: 100, daysSpent: 5 }, { role: 'backend', percent: 100, daysSpent: 4 }, { role: 'testing', percent: 100, daysSpent: 2 }] },
|
||||
{ id: 'v9', name: '值班V1.2', status: 'developing', releaseDate: null, createdAt: '2024-06-02', currentStage: 'testing', startDate: '2024-06-02', expectedReleaseDate: '2024-06-20', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 2 }, { role: 'frontend', percent: 100, daysSpent: 6 }, { role: 'backend', percent: 100, daysSpent: 5 }, { role: 'testing', percent: 30, daysSpent: 2 }] },
|
||||
{ id: 'v9', name: '值班V1.2', status: 'developing', releaseDate: null, createdAt: '2024-06-02', currentStage: 'testing', startDate: '2024-06-02', expectedReleaseDate: '2024-06-20', priority: 'P0', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '孙八' }, { role: 'testing', name: '周九' }], progress: [{ role: 'product', percent: 100, daysSpent: 2 }, { role: 'frontend', percent: 100, daysSpent: 6 }, { role: 'backend', percent: 100, daysSpent: 5 }, { role: 'testing', percent: 30, daysSpent: 2 }] },
|
||||
{ id: 'v10', name: '值班V2.0', status: 'planned', releaseDate: null, createdAt: '2024-06-08' },
|
||||
],
|
||||
_count: { requirements: 12, projects: 3, versions: 10 },
|
||||
@@ -68,9 +71,9 @@ const MOCK_OVERVIEW: ProductOverview[] = [
|
||||
versions: [
|
||||
{ id: 'v11', name: '支付V1.0', status: 'released', releaseDate: '2024-04-20', createdAt: '2024-04-01', currentStage: 'released', startDate: '2024-04-01', members: [{ role: 'product', name: '李四' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 100, daysSpent: 8 }, { role: 'backend', percent: 100, daysSpent: 7 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
|
||||
{ id: 'v12', name: '支付V1.1', status: 'released', releaseDate: '2024-06-01', createdAt: '2024-05-25', currentStage: 'released', startDate: '2024-05-10', members: [{ role: 'product', name: '李四' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'frontend', percent: 100, daysSpent: 7 }, { role: 'backend', percent: 100, daysSpent: 6 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
|
||||
{ id: 'v13', name: '支付V2.0', status: 'developing', releaseDate: null, createdAt: '2024-06-05', currentStage: 'dev', startDate: '2024-06-05', expectedReleaseDate: '2024-07-20', members: [{ role: 'product', name: '李四' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'ui', percent: 100, daysSpent: 5 }, { role: 'frontend', percent: 30, daysSpent: 5 }, { role: 'backend', percent: 20, daysSpent: 4 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
|
||||
{ id: 'v13', name: '支付V2.0', status: 'developing', releaseDate: null, createdAt: '2024-06-05', currentStage: 'dev', startDate: '2024-06-05', expectedReleaseDate: '2024-07-20', priority: 'P1', links: { research: 'https://docs.example.com/pay-v20', prototype: 'https://axure.example.com/pay-v20', ui: 'https://figma.com/pay-v20' }, members: [{ role: 'product', name: '李四' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '钱七' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'ui', percent: 100, daysSpent: 5 }, { role: 'frontend', percent: 30, daysSpent: 5 }, { role: 'backend', percent: 20, daysSpent: 4 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
|
||||
{ id: 'v14', name: '会员V1.0', status: 'released', releaseDate: '2024-05-10', createdAt: '2024-04-20', currentStage: 'released', startDate: '2024-04-20', members: [{ role: 'product', name: '张三' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 100, daysSpent: 9 }, { role: 'backend', percent: 100, daysSpent: 7 }, { role: 'testing', percent: 100, daysSpent: 3 }] },
|
||||
{ id: 'v15', name: '会员V1.1', status: 'developing', releaseDate: null, createdAt: '2024-06-03', currentStage: 'integration', startDate: '2024-06-03', expectedReleaseDate: '2024-06-25', members: [{ role: 'product', name: '张三' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'ui', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 90, daysSpent: 8 }, { role: 'backend', percent: 85, daysSpent: 7 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
|
||||
{ id: 'v15', name: '会员V1.1', status: 'developing', releaseDate: null, createdAt: '2024-06-03', currentStage: 'integration', startDate: '2024-06-03', expectedReleaseDate: '2024-06-25', priority: 'P2', members: [{ role: 'product', name: '张三' }, { role: 'ui', name: '王五' }, { role: 'frontend', name: '赵六' }, { role: 'backend', name: '吴十' }, { role: 'testing', name: '郑十一' }], progress: [{ role: 'product', percent: 100, daysSpent: 3 }, { role: 'ui', percent: 100, daysSpent: 4 }, { role: 'frontend', percent: 90, daysSpent: 8 }, { role: 'backend', percent: 85, daysSpent: 7 }, { role: 'testing', percent: 0, daysSpent: 0 }] },
|
||||
{ id: 'v16', name: '会员V2.0', status: 'planned', releaseDate: null, createdAt: '2024-06-08' },
|
||||
],
|
||||
_count: { requirements: 8, projects: 2, versions: 6 },
|
||||
|
||||
@@ -1,66 +1,353 @@
|
||||
'use client';
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { Requirement, RequirementStatus } from '@ftb/shared';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface RequirementWithCreator extends Requirement {
|
||||
creator?: { id: string; name: string };
|
||||
}
|
||||
import type { Requirement, RequirementStatus, Effort, DictItem, SourceType, SourceTarget } from '@/lib/requirement';
|
||||
import type { Priority } from '@/lib/derive';
|
||||
|
||||
interface RequirementState {
|
||||
requirements: RequirementWithCreator[];
|
||||
requirements: Requirement[];
|
||||
sourceTargets: SourceTarget[];
|
||||
types: DictItem[];
|
||||
platforms: DictItem[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
statusFilter: RequirementStatus | null;
|
||||
fetchRequirements: (productId: string, status?: RequirementStatus) => Promise<void>;
|
||||
createRequirement: (productId: string, data: { title: string; description?: string; priority?: number; creatorId: string }) => Promise<void>;
|
||||
updateRequirement: (productId: string, id: string, data: { title?: string; description?: string; priority?: number }) => Promise<void>;
|
||||
updateStatus: (productId: string, id: string, status: RequirementStatus) => Promise<void>;
|
||||
deleteRequirement: (productId: string, id: string) => Promise<void>;
|
||||
setStatusFilter: (status: RequirementStatus | null) => void;
|
||||
fetchRequirements: () => void;
|
||||
createRequirement: (data: Omit<Requirement, 'id' | 'code' | 'createdAt'>) => void;
|
||||
updateRequirement: (id: string, data: Partial<Requirement>) => void;
|
||||
deleteRequirement: (id: string) => void;
|
||||
addSourceTarget: (name: string, sourceType: SourceType) => void;
|
||||
updateSourceTarget: (id: string, name: string) => void;
|
||||
deleteSourceTarget: (id: string) => void;
|
||||
addType: (name: string) => void;
|
||||
updateType: (id: string, name: string) => void;
|
||||
deleteType: (id: string) => void;
|
||||
addPlatform: (name: string) => void;
|
||||
updatePlatform: (id: string, name: string) => void;
|
||||
deletePlatform: (id: string) => void;
|
||||
}
|
||||
|
||||
export const useRequirementStore = create<RequirementState>((set, get) => ({
|
||||
requirements: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
statusFilter: null,
|
||||
// --- Preset dictionaries ---
|
||||
|
||||
fetchRequirements: async (productId, status) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const query = status ? `?status=${status}` : '';
|
||||
const requirements = await api.get<RequirementWithCreator[]>(
|
||||
`/products/${productId}/requirements${query}`,
|
||||
);
|
||||
set({ requirements, loading: false });
|
||||
} catch (e: any) {
|
||||
set({ error: e.message, loading: false });
|
||||
}
|
||||
},
|
||||
const PRESET_SOURCE_TARGETS: SourceTarget[] = [
|
||||
// customer type
|
||||
{ id: 'st-1', name: '海底捞', sourceType: 'customer', createdAt: '2024-01-01' },
|
||||
{ id: 'st-2', name: '西贝', sourceType: 'customer', createdAt: '2024-01-01' },
|
||||
{ id: 'st-3', name: '喜茶', sourceType: 'customer', createdAt: '2024-01-01' },
|
||||
{ id: 'st-4', name: '奈雪的茶', sourceType: 'customer', createdAt: '2024-01-01' },
|
||||
{ id: 'st-5', name: '麦当劳', sourceType: 'customer', createdAt: '2024-01-01' },
|
||||
// operation type
|
||||
{ id: 'st-6', name: '运营部', sourceType: 'operation', createdAt: '2024-01-01' },
|
||||
{ id: 'st-7', name: '销售部', sourceType: 'operation', createdAt: '2024-01-01' },
|
||||
{ id: 'st-8', name: '客服部', sourceType: 'operation', createdAt: '2024-01-01' },
|
||||
// internal
|
||||
{ id: 'st-9', name: '产品部', sourceType: 'internal', createdAt: '2024-01-01' },
|
||||
{ id: 'st-10', name: '技术部', sourceType: 'internal', createdAt: '2024-01-01' },
|
||||
// management
|
||||
{ id: 'st-11', name: '张总', sourceType: 'management', createdAt: '2024-01-01' },
|
||||
{ id: 'st-12', name: '李总', sourceType: 'management', createdAt: '2024-01-01' },
|
||||
// aftersale
|
||||
{ id: 'st-13', name: '实施部', sourceType: 'aftersale', createdAt: '2024-01-01' },
|
||||
{ id: 'st-14', name: '客服部', sourceType: 'aftersale', createdAt: '2024-01-01' },
|
||||
// market
|
||||
{ id: 'st-15', name: '市场部', sourceType: 'market', createdAt: '2024-01-01' },
|
||||
// competitor
|
||||
{ id: 'st-16', name: '美团收银', sourceType: 'competitor', createdAt: '2024-01-01' },
|
||||
{ id: 'st-17', name: '客如云', sourceType: 'competitor', createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
createRequirement: async (productId, data) => {
|
||||
await api.post(`/products/${productId}/requirements`, data);
|
||||
await get().fetchRequirements(productId, get().statusFilter || undefined);
|
||||
},
|
||||
const PRESET_TYPES: DictItem[] = [
|
||||
{ id: 'type-1', name: '新功能', createdAt: '2024-01-01' },
|
||||
{ id: 'type-2', name: 'BUG修复', createdAt: '2024-01-01' },
|
||||
{ id: 'type-3', name: '数据需求/埋点需求', createdAt: '2024-01-01' },
|
||||
{ id: 'type-4', name: '优化需求', createdAt: '2024-01-01' },
|
||||
{ id: 'type-5', name: '模块重构', createdAt: '2024-01-01' },
|
||||
{ id: 'type-6', name: '缺陷修复', createdAt: '2024-01-01' },
|
||||
{ id: 'type-7', name: '功能优化', createdAt: '2024-01-01' },
|
||||
{ id: 'type-8', name: '插单/紧急需求', createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
updateRequirement: async (productId, id, data) => {
|
||||
await api.patch(`/products/${productId}/requirements/${id}`, data);
|
||||
await get().fetchRequirements(productId, get().statusFilter || undefined);
|
||||
},
|
||||
const PRESET_PLATFORMS: DictItem[] = [
|
||||
{ id: 'platform-1', name: 'Web', createdAt: '2024-01-01' },
|
||||
{ id: 'platform-2', name: 'App(iOS)', createdAt: '2024-01-01' },
|
||||
{ id: 'platform-3', name: 'App(Android)', createdAt: '2024-01-01' },
|
||||
{ id: 'platform-4', name: '小程序', createdAt: '2024-01-01' },
|
||||
{ id: 'platform-5', name: 'H5', createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
updateStatus: async (productId, id, status) => {
|
||||
await api.patch(`/products/${productId}/requirements/${id}/status`, { status });
|
||||
await get().fetchRequirements(productId, get().statusFilter || undefined);
|
||||
},
|
||||
// --- Mock requirements ---
|
||||
|
||||
deleteRequirement: async (productId, id) => {
|
||||
await api.delete(`/products/${productId}/requirements/${id}`);
|
||||
set({ requirements: get().requirements.filter((r) => r.id !== id) });
|
||||
const MOCK_REQUIREMENTS: Requirement[] = [
|
||||
{
|
||||
id: 'req-1',
|
||||
code: 'REQ-001',
|
||||
title: '考勤打卡支持人脸识别',
|
||||
description: '员工打卡增加人脸识别验证方式,防止代打卡',
|
||||
productId: 'mock-1',
|
||||
projectId: 'p1',
|
||||
versionId: 'v2',
|
||||
sourceType: 'customer',
|
||||
sourceTarget: '海底捞',
|
||||
platforms: ['platform-2', 'platform-3'],
|
||||
typeId: 'type-1',
|
||||
status: 'developing',
|
||||
priority: 'P1',
|
||||
effort: 'L',
|
||||
creator: '张三',
|
||||
createdAt: '2024-04-10',
|
||||
},
|
||||
{
|
||||
id: 'req-2',
|
||||
code: 'REQ-002',
|
||||
title: '电子合同批量签署',
|
||||
description: '支持企业批量发起合同签署,提升入职效率',
|
||||
productId: 'mock-1',
|
||||
projectId: 'p2',
|
||||
versionId: 'v13',
|
||||
sourceType: 'operation',
|
||||
sourceTarget: '运营部',
|
||||
platforms: ['platform-1'],
|
||||
typeId: 'type-1',
|
||||
status: 'pending_review',
|
||||
priority: 'P2',
|
||||
effort: 'XL',
|
||||
creator: '李四',
|
||||
createdAt: '2024-05-02',
|
||||
},
|
||||
{
|
||||
id: 'req-3',
|
||||
code: 'REQ-003',
|
||||
title: '支付渠道对账异常修复',
|
||||
description: '修复微信支付对账金额不一致的问题',
|
||||
productId: 'mock-2',
|
||||
projectId: 'p4',
|
||||
sourceType: 'customer',
|
||||
sourceTarget: '西贝',
|
||||
platforms: ['platform-1', 'platform-4'],
|
||||
typeId: 'type-2',
|
||||
status: 'testing',
|
||||
priority: 'P0',
|
||||
effort: 'M',
|
||||
creator: '王五',
|
||||
createdAt: '2024-05-15',
|
||||
},
|
||||
{
|
||||
id: 'req-4',
|
||||
code: 'REQ-004',
|
||||
title: '会员积分过期提醒',
|
||||
description: '积分到期前7天推送小程序模板消息通知',
|
||||
productId: 'mock-2',
|
||||
projectId: 'p5',
|
||||
versionId: 'v13',
|
||||
sourceType: 'management',
|
||||
sourceTarget: '张总',
|
||||
platforms: ['platform-4', 'platform-5'],
|
||||
typeId: 'type-4',
|
||||
status: 'planned',
|
||||
priority: 'P2',
|
||||
effort: 'S',
|
||||
creator: '张三',
|
||||
createdAt: '2024-05-20',
|
||||
},
|
||||
{
|
||||
id: 'req-5',
|
||||
code: 'REQ-005',
|
||||
title: '值班交接单增加拍照上传',
|
||||
description: '值班交接时支持拍照上传现场情况',
|
||||
productId: 'mock-1',
|
||||
projectId: 'p3',
|
||||
sourceType: 'internal',
|
||||
sourceTarget: '产品部',
|
||||
platforms: ['platform-2', 'platform-3'],
|
||||
typeId: 'type-1',
|
||||
status: 'adopted',
|
||||
priority: 'P3',
|
||||
effort: 'M',
|
||||
creator: '李四',
|
||||
createdAt: '2024-05-28',
|
||||
},
|
||||
{
|
||||
id: 'req-6',
|
||||
code: 'REQ-006',
|
||||
title: '考勤报表导出性能优化',
|
||||
description: '大数据量下考勤报表导出超时,需优化查询和导出逻辑',
|
||||
productId: 'mock-1',
|
||||
projectId: 'p1',
|
||||
versionId: 'v2',
|
||||
sourceType: 'customer',
|
||||
sourceTarget: '喜茶',
|
||||
platforms: ['platform-1'],
|
||||
typeId: 'type-7',
|
||||
status: 'developing',
|
||||
priority: 'P1',
|
||||
effort: 'M',
|
||||
creator: '王五',
|
||||
createdAt: '2024-06-01',
|
||||
},
|
||||
{
|
||||
id: 'req-7',
|
||||
code: 'REQ-007',
|
||||
title: '支付模块埋点需求',
|
||||
description: '支付成功率、失败原因分布等核心指标埋点',
|
||||
productId: 'mock-2',
|
||||
projectId: 'p4',
|
||||
sourceType: 'aftersale',
|
||||
sourceTarget: '客服部',
|
||||
platforms: ['platform-1', 'platform-4', 'platform-5'],
|
||||
typeId: 'type-3',
|
||||
status: 'released',
|
||||
priority: 'P2',
|
||||
effort: 'S',
|
||||
creator: '张三',
|
||||
createdAt: '2024-04-25',
|
||||
},
|
||||
{
|
||||
id: 'req-8',
|
||||
code: 'REQ-008',
|
||||
title: '会员模块重构-拆分储值与积分服务',
|
||||
description: '将储值卡和积分系统拆分为独立微服务,降低耦合度',
|
||||
productId: 'mock-2',
|
||||
projectId: 'p5',
|
||||
sourceType: 'competitor',
|
||||
sourceTarget: '美团收银',
|
||||
platforms: ['platform-1'],
|
||||
typeId: 'type-5',
|
||||
status: 'pending_review',
|
||||
priority: 'P3',
|
||||
effort: 'XL',
|
||||
creator: '李四',
|
||||
createdAt: '2024-06-05',
|
||||
},
|
||||
];
|
||||
|
||||
setStatusFilter: (status) => {
|
||||
set({ statusFilter: status });
|
||||
},
|
||||
}));
|
||||
// --- localStorage helpers ---
|
||||
|
||||
const STORAGE_KEY = 'ftb_requirements_v2';
|
||||
|
||||
function saveLocal(state: { requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] }) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function loadLocal(): { requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] } | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Store ---
|
||||
|
||||
export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
return {
|
||||
requirements: MOCK_REQUIREMENTS,
|
||||
sourceTargets: PRESET_SOURCE_TARGETS,
|
||||
types: PRESET_TYPES,
|
||||
platforms: PRESET_PLATFORMS,
|
||||
loading: false,
|
||||
|
||||
fetchRequirements: () => {
|
||||
const cached = loadLocal();
|
||||
if (cached) {
|
||||
set({ requirements: cached.requirements, sourceTargets: cached.sourceTargets, types: cached.types, platforms: cached.platforms });
|
||||
}
|
||||
},
|
||||
|
||||
createRequirement: (data) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const code = `REQ-${String(requirements.length + 1).padStart(3, '0')}`;
|
||||
const newReq: Requirement = {
|
||||
...data,
|
||||
id: `req-${Date.now()}`,
|
||||
code,
|
||||
createdAt: new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
const updated = [...requirements, newReq];
|
||||
set({ requirements: updated });
|
||||
saveLocal({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
updateRequirement: (id, data) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = requirements.map((r) => (r.id === id ? { ...r, ...data } : r));
|
||||
set({ requirements: updated });
|
||||
saveLocal({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
deleteRequirement: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = requirements.filter((r) => r.id !== id);
|
||||
set({ requirements: updated });
|
||||
saveLocal({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
// --- SourceTarget dict ---
|
||||
addSourceTarget: (name, sourceType) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const item: SourceTarget = { id: `st-${Date.now()}`, name, sourceType, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const updated = [...sourceTargets, item];
|
||||
set({ sourceTargets: updated });
|
||||
saveLocal({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
updateSourceTarget: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = sourceTargets.map((s) => (s.id === id ? { ...s, name } : s));
|
||||
set({ sourceTargets: updated });
|
||||
saveLocal({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
deleteSourceTarget: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = sourceTargets.filter((s) => s.id !== id);
|
||||
set({ sourceTargets: updated });
|
||||
saveLocal({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
// --- Type dict ---
|
||||
addType: (name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const item: DictItem = { id: `type-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const updated = [...types, item];
|
||||
set({ types: updated });
|
||||
saveLocal({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
updateType: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = types.map((t) => (t.id === id ? { ...t, name } : t));
|
||||
set({ types: updated });
|
||||
saveLocal({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
deleteType: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = types.filter((t) => t.id !== id);
|
||||
set({ types: updated });
|
||||
saveLocal({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
// --- Platform dict ---
|
||||
addPlatform: (name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const item: DictItem = { id: `platform-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const updated = [...platforms, item];
|
||||
set({ platforms: updated });
|
||||
saveLocal({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
|
||||
updatePlatform: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = platforms.map((p) => (p.id === id ? { ...p, name } : p));
|
||||
set({ platforms: updated });
|
||||
saveLocal({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
|
||||
deletePlatform: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = platforms.filter((p) => p.id !== id);
|
||||
set({ platforms: updated });
|
||||
saveLocal({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user