920 lines
24 KiB
TypeScript
920 lines
24 KiB
TypeScript
import Mock from 'mockjs';
|
|
|
|
/** 文件职责:员工排班页面 Mock 接口。 */
|
|
interface MockRequestOptions {
|
|
body: null | string;
|
|
type: string;
|
|
url: string;
|
|
}
|
|
|
|
type StaffRoleType = 'cashier' | 'chef' | 'courier' | 'manager';
|
|
type StaffStatus = 'active' | 'leave' | 'resigned';
|
|
type ShiftType = 'evening' | 'full' | 'morning' | 'off';
|
|
|
|
interface StoreStaffMock {
|
|
avatarColor: string;
|
|
email: string;
|
|
hiredAt: string;
|
|
id: string;
|
|
name: string;
|
|
permissions: string[];
|
|
phone: string;
|
|
roleType: StaffRoleType;
|
|
status: StaffStatus;
|
|
}
|
|
|
|
interface ShiftTemplateItemMock {
|
|
endTime: string;
|
|
startTime: string;
|
|
}
|
|
|
|
interface StoreShiftTemplatesMock {
|
|
evening: ShiftTemplateItemMock;
|
|
full: ShiftTemplateItemMock;
|
|
morning: ShiftTemplateItemMock;
|
|
}
|
|
|
|
interface StaffDayShiftMock {
|
|
dayOfWeek: number;
|
|
endTime: string;
|
|
shiftType: ShiftType;
|
|
startTime: string;
|
|
}
|
|
|
|
interface StaffScheduleMock {
|
|
shifts: StaffDayShiftMock[];
|
|
staffId: string;
|
|
}
|
|
|
|
interface StoreStaffState {
|
|
schedules: StaffScheduleMock[];
|
|
staffs: StoreStaffMock[];
|
|
templates: StoreShiftTemplatesMock;
|
|
weekStartDate: string;
|
|
}
|
|
|
|
const ROLE_VALUES = new Set<StaffRoleType>([
|
|
'cashier',
|
|
'chef',
|
|
'courier',
|
|
'manager',
|
|
]);
|
|
const STATUS_VALUES = new Set<StaffStatus>(['active', 'leave', 'resigned']);
|
|
const SHIFT_VALUES = new Set<ShiftType>(['evening', 'full', 'morning', 'off']);
|
|
|
|
const AVATAR_COLORS = [
|
|
'#f56a00',
|
|
'#7265e6',
|
|
'#52c41a',
|
|
'#fa8c16',
|
|
'#1890ff',
|
|
'#bfbfbf',
|
|
'#13c2c2',
|
|
'#eb2f96',
|
|
];
|
|
|
|
const DEFAULT_TEMPLATES: StoreShiftTemplatesMock = {
|
|
morning: {
|
|
startTime: '09:00',
|
|
endTime: '14:00',
|
|
},
|
|
evening: {
|
|
startTime: '14:00',
|
|
endTime: '21:00',
|
|
},
|
|
full: {
|
|
startTime: '09:00',
|
|
endTime: '21:00',
|
|
},
|
|
};
|
|
|
|
const DEFAULT_STAFFS: StoreStaffMock[] = [
|
|
{
|
|
id: 'staff-001',
|
|
name: '张伟',
|
|
phone: '13800008001',
|
|
email: 'zhangwei@example.com',
|
|
roleType: 'manager',
|
|
status: 'active',
|
|
permissions: ['全部权限'],
|
|
hiredAt: '2024-01-15',
|
|
avatarColor: '#f56a00',
|
|
},
|
|
{
|
|
id: 'staff-002',
|
|
name: '李娜',
|
|
phone: '13800008002',
|
|
email: 'lina@example.com',
|
|
roleType: 'cashier',
|
|
status: 'active',
|
|
permissions: ['收银', '退款'],
|
|
hiredAt: '2024-03-20',
|
|
avatarColor: '#7265e6',
|
|
},
|
|
{
|
|
id: 'staff-003',
|
|
name: '王磊',
|
|
phone: '13800008003',
|
|
email: '',
|
|
roleType: 'courier',
|
|
status: 'active',
|
|
permissions: ['配送管理'],
|
|
hiredAt: '2024-06-01',
|
|
avatarColor: '#52c41a',
|
|
},
|
|
{
|
|
id: 'staff-004',
|
|
name: '赵敏',
|
|
phone: '13800008004',
|
|
email: '',
|
|
roleType: 'chef',
|
|
status: 'active',
|
|
permissions: ['订单查看'],
|
|
hiredAt: '2024-08-10',
|
|
avatarColor: '#fa8c16',
|
|
},
|
|
{
|
|
id: 'staff-005',
|
|
name: '刘洋',
|
|
phone: '13800008005',
|
|
email: 'liuyang@example.com',
|
|
roleType: 'courier',
|
|
status: 'leave',
|
|
permissions: ['配送管理'],
|
|
hiredAt: '2025-01-05',
|
|
avatarColor: '#1890ff',
|
|
},
|
|
{
|
|
id: 'staff-006',
|
|
name: '陈静',
|
|
phone: '13800008006',
|
|
email: '',
|
|
roleType: 'cashier',
|
|
status: 'resigned',
|
|
permissions: [],
|
|
hiredAt: '2024-11-20',
|
|
avatarColor: '#bfbfbf',
|
|
},
|
|
];
|
|
|
|
const storeStaffMap = new Map<string, StoreStaffState>();
|
|
|
|
/** 解析 URL 查询参数。 */
|
|
function parseUrlParams(url: string) {
|
|
const parsed = new URL(url, 'http://localhost');
|
|
const params: Record<string, string> = {};
|
|
parsed.searchParams.forEach((value, key) => {
|
|
params[key] = value;
|
|
});
|
|
return params;
|
|
}
|
|
|
|
/** 解析请求体 JSON。 */
|
|
function parseBody(options: MockRequestOptions) {
|
|
if (!options.body) return {};
|
|
try {
|
|
return JSON.parse(options.body) as Record<string, unknown>;
|
|
} catch (error) {
|
|
console.error('[mock-store-staff] parseBody error:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/** 获取当前周一日期。 */
|
|
function getCurrentWeekStartDate(baseDate = new Date()) {
|
|
const date = new Date(baseDate);
|
|
const weekDay = date.getDay();
|
|
const diff = weekDay === 0 ? -6 : 1 - weekDay;
|
|
date.setDate(date.getDate() + diff);
|
|
return toDateOnly(date);
|
|
}
|
|
|
|
/** 日期转 yyyy-MM-dd。 */
|
|
function toDateOnly(date: Date) {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
/** 归一化 HH:mm 时间。 */
|
|
function normalizeTime(value: unknown, fallback: string) {
|
|
const input = typeof value === 'string' ? value : '';
|
|
const matched = /^(\d{2}):(\d{2})$/.exec(input);
|
|
if (!matched) return fallback;
|
|
const hour = Number(matched[1]);
|
|
const minute = Number(matched[2]);
|
|
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
return fallback;
|
|
}
|
|
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
|
}
|
|
|
|
/** 归一化角色。 */
|
|
function normalizeRoleType(value: unknown, fallback: StaffRoleType) {
|
|
return ROLE_VALUES.has(value as StaffRoleType)
|
|
? (value as StaffRoleType)
|
|
: fallback;
|
|
}
|
|
|
|
/** 归一化状态。 */
|
|
function normalizeStatus(value: unknown, fallback: StaffStatus) {
|
|
return STATUS_VALUES.has(value as StaffStatus)
|
|
? (value as StaffStatus)
|
|
: fallback;
|
|
}
|
|
|
|
/** 归一化班次类型。 */
|
|
function normalizeShiftType(value: unknown, fallback: ShiftType) {
|
|
return SHIFT_VALUES.has(value as ShiftType) ? (value as ShiftType) : fallback;
|
|
}
|
|
|
|
/** 深拷贝员工列表。 */
|
|
function cloneStaffs(source: StoreStaffMock[]) {
|
|
return source.map((item) => ({
|
|
...item,
|
|
permissions: [...item.permissions],
|
|
}));
|
|
}
|
|
|
|
/** 深拷贝模板。 */
|
|
function cloneTemplates(
|
|
source: StoreShiftTemplatesMock,
|
|
): StoreShiftTemplatesMock {
|
|
return {
|
|
morning: { ...source.morning },
|
|
evening: { ...source.evening },
|
|
full: { ...source.full },
|
|
};
|
|
}
|
|
|
|
/** 深拷贝排班。 */
|
|
function cloneSchedules(source: StaffScheduleMock[]) {
|
|
return source.map((item) => ({
|
|
staffId: item.staffId,
|
|
shifts: item.shifts.map((shift) => ({ ...shift })),
|
|
}));
|
|
}
|
|
|
|
/** 按入职时间稳定排序员工。 */
|
|
function sortStaffs(source: StoreStaffMock[]) {
|
|
return cloneStaffs(source).toSorted((a, b) => {
|
|
const dateDiff = a.hiredAt.localeCompare(b.hiredAt);
|
|
if (dateDiff !== 0) return dateDiff;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
}
|
|
|
|
/** 通过班次类型生成单日排班。 */
|
|
function createDayShift(
|
|
dayOfWeek: number,
|
|
shiftType: ShiftType,
|
|
templates: StoreShiftTemplatesMock,
|
|
): StaffDayShiftMock {
|
|
if (shiftType === 'off') {
|
|
return {
|
|
dayOfWeek,
|
|
shiftType,
|
|
startTime: '',
|
|
endTime: '',
|
|
};
|
|
}
|
|
|
|
const template = templates[shiftType];
|
|
return {
|
|
dayOfWeek,
|
|
shiftType,
|
|
startTime: template.startTime,
|
|
endTime: template.endTime,
|
|
};
|
|
}
|
|
|
|
/** 生成默认 7 天排班。 */
|
|
function createDefaultWeekByRole(
|
|
roleType: StaffRoleType,
|
|
templates: StoreShiftTemplatesMock,
|
|
): StaffDayShiftMock[] {
|
|
const rolePatternMap: Record<StaffRoleType, ShiftType[]> = {
|
|
manager: ['full', 'full', 'full', 'full', 'full', 'morning', 'off'],
|
|
cashier: [
|
|
'morning',
|
|
'morning',
|
|
'off',
|
|
'morning',
|
|
'evening',
|
|
'full',
|
|
'full',
|
|
],
|
|
courier: [
|
|
'morning',
|
|
'evening',
|
|
'morning',
|
|
'evening',
|
|
'morning',
|
|
'evening',
|
|
'off',
|
|
],
|
|
chef: ['full', 'full', 'evening', 'off', 'full', 'full', 'morning'],
|
|
};
|
|
|
|
const pattern = rolePatternMap[roleType] ?? rolePatternMap.cashier;
|
|
return Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
|
createDayShift(dayOfWeek, pattern[dayOfWeek] ?? 'off', templates),
|
|
);
|
|
}
|
|
|
|
/** 创建默认门店状态。 */
|
|
function createDefaultState(): StoreStaffState {
|
|
const templates = cloneTemplates(DEFAULT_TEMPLATES);
|
|
const staffs = sortStaffs(cloneStaffs(DEFAULT_STAFFS));
|
|
const schedules = staffs.map((staff) => ({
|
|
staffId: staff.id,
|
|
shifts:
|
|
staff.status === 'resigned'
|
|
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
|
createDayShift(dayOfWeek, 'off', templates),
|
|
)
|
|
: createDefaultWeekByRole(staff.roleType, templates),
|
|
}));
|
|
|
|
return {
|
|
staffs,
|
|
templates,
|
|
schedules,
|
|
weekStartDate: getCurrentWeekStartDate(),
|
|
};
|
|
}
|
|
|
|
/** 确保门店状态存在。 */
|
|
function ensureStoreState(storeId = '') {
|
|
const key = storeId || 'default';
|
|
let state = storeStaffMap.get(key);
|
|
if (!state) {
|
|
state = createDefaultState();
|
|
storeStaffMap.set(key, state);
|
|
}
|
|
return state;
|
|
}
|
|
|
|
/** 构建员工排班索引。 */
|
|
function createScheduleMap(schedules: StaffScheduleMock[]) {
|
|
const scheduleMap = new Map<string, StaffDayShiftMock[]>();
|
|
for (const schedule of schedules) {
|
|
scheduleMap.set(
|
|
schedule.staffId,
|
|
schedule.shifts.map((shift) => ({ ...shift })),
|
|
);
|
|
}
|
|
return scheduleMap;
|
|
}
|
|
|
|
/** 同步模板后刷新已有排班的时间段。 */
|
|
function syncScheduleTimesWithTemplates(
|
|
schedules: StaffScheduleMock[],
|
|
templates: StoreShiftTemplatesMock,
|
|
) {
|
|
for (const schedule of schedules) {
|
|
schedule.shifts = schedule.shifts
|
|
.map((shift) => {
|
|
const normalizedType = normalizeShiftType(shift.shiftType, 'off');
|
|
if (normalizedType === 'off') {
|
|
return {
|
|
dayOfWeek: shift.dayOfWeek,
|
|
shiftType: 'off' as const,
|
|
startTime: '',
|
|
endTime: '',
|
|
};
|
|
}
|
|
return {
|
|
dayOfWeek: shift.dayOfWeek,
|
|
shiftType: normalizedType,
|
|
startTime: templates[normalizedType].startTime,
|
|
endTime: templates[normalizedType].endTime,
|
|
};
|
|
})
|
|
.toSorted((a, b) => a.dayOfWeek - b.dayOfWeek);
|
|
}
|
|
}
|
|
|
|
/** 归一化模板。 */
|
|
function normalizeTemplates(
|
|
input: unknown,
|
|
fallback: StoreShiftTemplatesMock,
|
|
): StoreShiftTemplatesMock {
|
|
const record = typeof input === 'object' && input ? input : {};
|
|
const morning =
|
|
typeof (record as { morning?: unknown }).morning === 'object' &&
|
|
(record as { morning?: unknown }).morning
|
|
? ((record as { morning: Record<string, unknown> }).morning as Record<
|
|
string,
|
|
unknown
|
|
>)
|
|
: {};
|
|
const evening =
|
|
typeof (record as { evening?: unknown }).evening === 'object' &&
|
|
(record as { evening?: unknown }).evening
|
|
? ((record as { evening: Record<string, unknown> }).evening as Record<
|
|
string,
|
|
unknown
|
|
>)
|
|
: {};
|
|
const full =
|
|
typeof (record as { full?: unknown }).full === 'object' &&
|
|
(record as { full?: unknown }).full
|
|
? ((record as { full: Record<string, unknown> }).full as Record<
|
|
string,
|
|
unknown
|
|
>)
|
|
: {};
|
|
|
|
return {
|
|
morning: {
|
|
startTime: normalizeTime(morning.startTime, fallback.morning.startTime),
|
|
endTime: normalizeTime(morning.endTime, fallback.morning.endTime),
|
|
},
|
|
evening: {
|
|
startTime: normalizeTime(evening.startTime, fallback.evening.startTime),
|
|
endTime: normalizeTime(evening.endTime, fallback.evening.endTime),
|
|
},
|
|
full: {
|
|
startTime: normalizeTime(full.startTime, fallback.full.startTime),
|
|
endTime: normalizeTime(full.endTime, fallback.full.endTime),
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 归一化单员工 7 天排班。 */
|
|
function normalizeShifts(
|
|
input: unknown,
|
|
templates: StoreShiftTemplatesMock,
|
|
fallback: StaffDayShiftMock[],
|
|
): StaffDayShiftMock[] {
|
|
const byDay = new Map<number, StaffDayShiftMock>();
|
|
|
|
if (Array.isArray(input)) {
|
|
for (const rawShift of input) {
|
|
const shiftRecord =
|
|
typeof rawShift === 'object' && rawShift
|
|
? (rawShift as Record<string, unknown>)
|
|
: {};
|
|
const dayOfWeek = Number(shiftRecord.dayOfWeek);
|
|
if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
|
|
continue;
|
|
}
|
|
|
|
const shiftType = normalizeShiftType(shiftRecord.shiftType, 'off');
|
|
if (shiftType === 'off') {
|
|
byDay.set(dayOfWeek, {
|
|
dayOfWeek,
|
|
shiftType,
|
|
startTime: '',
|
|
endTime: '',
|
|
});
|
|
continue;
|
|
}
|
|
|
|
byDay.set(dayOfWeek, {
|
|
dayOfWeek,
|
|
shiftType,
|
|
startTime: normalizeTime(
|
|
shiftRecord.startTime,
|
|
templates[shiftType].startTime,
|
|
),
|
|
endTime: normalizeTime(
|
|
shiftRecord.endTime,
|
|
templates[shiftType].endTime,
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
return Array.from({ length: 7 }).map((_, dayOfWeek) => {
|
|
const normalized = byDay.get(dayOfWeek);
|
|
if (normalized) {
|
|
return normalized;
|
|
}
|
|
|
|
const fallbackShift = fallback.find((item) => item.dayOfWeek === dayOfWeek);
|
|
if (fallbackShift) {
|
|
return { ...fallbackShift };
|
|
}
|
|
|
|
return createDayShift(dayOfWeek, 'off', templates);
|
|
});
|
|
}
|
|
|
|
/** 规范手机号格式。 */
|
|
function normalizePhone(value: unknown) {
|
|
return String(value || '')
|
|
.replaceAll(/\D/g, '')
|
|
.slice(0, 11);
|
|
}
|
|
|
|
/** 规范权限列表。 */
|
|
function normalizePermissions(value: unknown, roleType: StaffRoleType) {
|
|
if (!Array.isArray(value)) {
|
|
return roleType === 'manager' ? ['全部权限'] : [];
|
|
}
|
|
const unique = [
|
|
...new Set(value.map((item) => String(item || '').trim()).filter(Boolean)),
|
|
];
|
|
if (roleType === 'manager' && unique.length === 0) {
|
|
return ['全部权限'];
|
|
}
|
|
return unique;
|
|
}
|
|
|
|
/** 生成员工 ID。 */
|
|
function createStaffId() {
|
|
return `staff-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
}
|
|
|
|
/** 生成头像颜色。 */
|
|
function resolveAvatarColor(seed: string) {
|
|
let hash = 0;
|
|
for (const char of seed) {
|
|
hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0;
|
|
}
|
|
return AVATAR_COLORS[hash % AVATAR_COLORS.length] ?? '#1677ff';
|
|
}
|
|
|
|
/** 获取员工列表。 */
|
|
Mock.mock(/\/store\/staff(?:\?|$)/, 'get', (options: MockRequestOptions) => {
|
|
const params = parseUrlParams(options.url);
|
|
const storeId = String(params.storeId || '');
|
|
const state = ensureStoreState(storeId);
|
|
|
|
const keyword = String(params.keyword || '')
|
|
.trim()
|
|
.toLowerCase();
|
|
const roleType = params.roleType
|
|
? normalizeRoleType(params.roleType, 'cashier')
|
|
: '';
|
|
const status = params.status ? normalizeStatus(params.status, 'active') : '';
|
|
|
|
const page = Math.max(1, Number(params.page) || 1);
|
|
const pageSize = Math.max(1, Math.min(200, Number(params.pageSize) || 10));
|
|
|
|
const filtered = state.staffs.filter((staff) => {
|
|
if (keyword) {
|
|
const hitKeyword =
|
|
staff.name.toLowerCase().includes(keyword) ||
|
|
staff.phone.includes(keyword) ||
|
|
staff.email.toLowerCase().includes(keyword);
|
|
if (!hitKeyword) return false;
|
|
}
|
|
|
|
if (roleType && staff.roleType !== roleType) {
|
|
return false;
|
|
}
|
|
|
|
if (status && staff.status !== status) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
const start = (page - 1) * pageSize;
|
|
const items = filtered.slice(start, start + pageSize);
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
items: cloneStaffs(items),
|
|
total: filtered.length,
|
|
page,
|
|
pageSize,
|
|
},
|
|
};
|
|
});
|
|
|
|
/** 新增 / 编辑员工。 */
|
|
Mock.mock(/\/store\/staff\/save/, 'post', (options: MockRequestOptions) => {
|
|
const body = parseBody(options);
|
|
const storeId = String(body.storeId || '');
|
|
if (!storeId) {
|
|
return { code: 200, data: null };
|
|
}
|
|
|
|
const state = ensureStoreState(storeId);
|
|
const id = String(body.id || '').trim();
|
|
const existingIndex = state.staffs.findIndex((item) => item.id === id);
|
|
const roleType = normalizeRoleType(body.roleType, 'cashier');
|
|
const status = normalizeStatus(body.status, 'active');
|
|
|
|
const nextStaff: StoreStaffMock = {
|
|
id: id || createStaffId(),
|
|
name: String(body.name || '').trim(),
|
|
phone: normalizePhone(body.phone),
|
|
email: String(body.email || '').trim(),
|
|
roleType,
|
|
status,
|
|
permissions: normalizePermissions(body.permissions, roleType),
|
|
hiredAt:
|
|
existingIndex === -1
|
|
? toDateOnly(new Date())
|
|
: state.staffs[existingIndex]?.hiredAt || toDateOnly(new Date()),
|
|
avatarColor:
|
|
existingIndex === -1
|
|
? resolveAvatarColor(String(body.name || Date.now()))
|
|
: state.staffs[existingIndex]?.avatarColor || '#1677ff',
|
|
};
|
|
|
|
if (!nextStaff.name) {
|
|
return {
|
|
code: 400,
|
|
data: null,
|
|
message: '员工姓名不能为空',
|
|
};
|
|
}
|
|
|
|
if (!nextStaff.phone || nextStaff.phone.length < 6) {
|
|
return {
|
|
code: 400,
|
|
data: null,
|
|
message: '手机号格式不正确',
|
|
};
|
|
}
|
|
|
|
if (existingIndex === -1) {
|
|
state.staffs.push(nextStaff);
|
|
state.staffs = sortStaffs(state.staffs);
|
|
state.schedules.push({
|
|
staffId: nextStaff.id,
|
|
shifts:
|
|
status === 'resigned'
|
|
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
|
createDayShift(dayOfWeek, 'off', state.templates),
|
|
)
|
|
: createDefaultWeekByRole(roleType, state.templates),
|
|
});
|
|
} else {
|
|
state.staffs[existingIndex] = nextStaff;
|
|
state.staffs = sortStaffs(state.staffs);
|
|
|
|
const schedule = state.schedules.find(
|
|
(item) => item.staffId === nextStaff.id,
|
|
);
|
|
if (schedule && nextStaff.status === 'resigned') {
|
|
schedule.shifts = Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
|
createDayShift(dayOfWeek, 'off', state.templates),
|
|
);
|
|
}
|
|
if (!schedule) {
|
|
state.schedules.push({
|
|
staffId: nextStaff.id,
|
|
shifts: createDefaultWeekByRole(nextStaff.roleType, state.templates),
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
...nextStaff,
|
|
permissions: [...nextStaff.permissions],
|
|
},
|
|
};
|
|
});
|
|
|
|
/** 删除员工。 */
|
|
Mock.mock(/\/store\/staff\/delete/, 'post', (options: MockRequestOptions) => {
|
|
const body = parseBody(options);
|
|
const storeId = String(body.storeId || '');
|
|
const staffId = String(body.staffId || '');
|
|
if (!storeId || !staffId) {
|
|
return { code: 200, data: null };
|
|
}
|
|
|
|
const state = ensureStoreState(storeId);
|
|
state.staffs = state.staffs.filter((item) => item.id !== staffId);
|
|
state.schedules = state.schedules.filter((item) => item.staffId !== staffId);
|
|
|
|
return {
|
|
code: 200,
|
|
data: null,
|
|
};
|
|
});
|
|
|
|
/** 获取门店排班配置。 */
|
|
Mock.mock(
|
|
/\/store\/staff\/schedule(?:\?|$)/,
|
|
'get',
|
|
(options: MockRequestOptions) => {
|
|
const params = parseUrlParams(options.url);
|
|
const storeId = String(params.storeId || '');
|
|
const state = ensureStoreState(storeId);
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
storeId,
|
|
templates: cloneTemplates(state.templates),
|
|
schedules: cloneSchedules(state.schedules),
|
|
weekStartDate: String(params.weekStartDate || state.weekStartDate),
|
|
},
|
|
};
|
|
},
|
|
);
|
|
|
|
/** 保存班次模板。 */
|
|
Mock.mock(
|
|
/\/store\/staff\/template\/save/,
|
|
'post',
|
|
(options: MockRequestOptions) => {
|
|
const body = parseBody(options);
|
|
const storeId = String(body.storeId || '');
|
|
if (!storeId) {
|
|
return { code: 200, data: cloneTemplates(DEFAULT_TEMPLATES) };
|
|
}
|
|
|
|
const state = ensureStoreState(storeId);
|
|
state.templates = normalizeTemplates(body.templates, state.templates);
|
|
syncScheduleTimesWithTemplates(state.schedules, state.templates);
|
|
|
|
return {
|
|
code: 200,
|
|
data: cloneTemplates(state.templates),
|
|
};
|
|
},
|
|
);
|
|
|
|
/** 保存员工个人排班。 */
|
|
Mock.mock(
|
|
/\/store\/staff\/schedule\/personal\/save/,
|
|
'post',
|
|
(options: MockRequestOptions) => {
|
|
const body = parseBody(options);
|
|
const storeId = String(body.storeId || '');
|
|
const staffId = String(body.staffId || '');
|
|
if (!storeId || !staffId) {
|
|
return { code: 200, data: null };
|
|
}
|
|
|
|
const state = ensureStoreState(storeId);
|
|
const staff = state.staffs.find((item) => item.id === staffId);
|
|
if (!staff) {
|
|
return {
|
|
code: 400,
|
|
data: null,
|
|
message: '员工不存在',
|
|
};
|
|
}
|
|
|
|
const schedule = state.schedules.find((item) => item.staffId === staffId);
|
|
const fallbackShifts =
|
|
schedule?.shifts ??
|
|
createDefaultWeekByRole(staff.roleType, state.templates);
|
|
|
|
const nextShifts =
|
|
staff.status === 'resigned'
|
|
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
|
createDayShift(dayOfWeek, 'off', state.templates),
|
|
)
|
|
: normalizeShifts(body.shifts, state.templates, fallbackShifts);
|
|
|
|
if (schedule) {
|
|
schedule.shifts = nextShifts;
|
|
} else {
|
|
state.schedules.push({
|
|
staffId,
|
|
shifts: nextShifts,
|
|
});
|
|
}
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
staffId,
|
|
shifts: nextShifts.map((item) => ({ ...item })),
|
|
},
|
|
};
|
|
},
|
|
);
|
|
|
|
/** 保存周排班。 */
|
|
Mock.mock(
|
|
/\/store\/staff\/schedule\/weekly\/save/,
|
|
'post',
|
|
(options: MockRequestOptions) => {
|
|
const body = parseBody(options);
|
|
const storeId = String(body.storeId || '');
|
|
if (!storeId) {
|
|
return { code: 200, data: null };
|
|
}
|
|
|
|
const state = ensureStoreState(storeId);
|
|
const incomingList = Array.isArray(body.schedules) ? body.schedules : [];
|
|
const scheduleMap = createScheduleMap(state.schedules);
|
|
|
|
for (const incoming of incomingList) {
|
|
const record =
|
|
typeof incoming === 'object' && incoming
|
|
? (incoming as Record<string, unknown>)
|
|
: {};
|
|
const staffId = String(record.staffId || '');
|
|
if (!staffId) continue;
|
|
|
|
const staff = state.staffs.find((item) => item.id === staffId);
|
|
if (!staff || staff.status === 'resigned') continue;
|
|
|
|
const fallbackShifts =
|
|
scheduleMap.get(staffId) ??
|
|
createDefaultWeekByRole(staff.roleType, state.templates);
|
|
|
|
const nextShifts = normalizeShifts(
|
|
record.shifts,
|
|
state.templates,
|
|
fallbackShifts,
|
|
);
|
|
scheduleMap.set(staffId, nextShifts);
|
|
}
|
|
|
|
state.schedules = state.staffs.map((staff) => {
|
|
const baseShifts =
|
|
scheduleMap.get(staff.id) ??
|
|
createDefaultWeekByRole(staff.roleType, state.templates);
|
|
return {
|
|
staffId: staff.id,
|
|
shifts:
|
|
staff.status === 'resigned'
|
|
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
|
createDayShift(dayOfWeek, 'off', state.templates),
|
|
)
|
|
: baseShifts.map((shift) => ({ ...shift })),
|
|
};
|
|
});
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
storeId,
|
|
templates: cloneTemplates(state.templates),
|
|
schedules: cloneSchedules(state.schedules),
|
|
weekStartDate: state.weekStartDate,
|
|
},
|
|
};
|
|
},
|
|
);
|
|
|
|
/** 复制门店模板与排班。 */
|
|
Mock.mock(/\/store\/staff\/copy/, 'post', (options: MockRequestOptions) => {
|
|
const body = parseBody(options);
|
|
const sourceStoreId = String(body.sourceStoreId || '');
|
|
const copyScope = String(body.copyScope || '');
|
|
const targetStoreIds = Array.isArray(body.targetStoreIds)
|
|
? body.targetStoreIds.map(String).filter(Boolean)
|
|
: [];
|
|
|
|
if (
|
|
!sourceStoreId ||
|
|
copyScope !== 'template_and_schedule' ||
|
|
targetStoreIds.length === 0
|
|
) {
|
|
return {
|
|
code: 200,
|
|
data: { copiedCount: 0 },
|
|
};
|
|
}
|
|
|
|
const sourceState = ensureStoreState(sourceStoreId);
|
|
const sourceScheduleMap = createScheduleMap(sourceState.schedules);
|
|
const uniqueTargets = [...new Set(targetStoreIds)].filter(
|
|
(id) => id !== sourceStoreId,
|
|
);
|
|
|
|
for (const targetStoreId of uniqueTargets) {
|
|
const targetState = ensureStoreState(targetStoreId);
|
|
targetState.templates = cloneTemplates(sourceState.templates);
|
|
|
|
targetState.schedules = targetState.staffs.map((staff) => {
|
|
const targetShiftFallback =
|
|
sourceScheduleMap.get(staff.id) ??
|
|
createDefaultWeekByRole(staff.roleType, targetState.templates);
|
|
|
|
return {
|
|
staffId: staff.id,
|
|
shifts:
|
|
staff.status === 'resigned'
|
|
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
|
createDayShift(dayOfWeek, 'off', targetState.templates),
|
|
)
|
|
: normalizeShifts(
|
|
targetShiftFallback,
|
|
targetState.templates,
|
|
targetShiftFallback,
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
copiedCount: uniqueTargets.length,
|
|
copyScope: 'template_and_schedule',
|
|
},
|
|
};
|
|
});
|