fix: 员工排班接入真实数据并移除兜底
This commit is contained in:
@@ -13,6 +13,7 @@ import dayjs from 'dayjs';
|
||||
|
||||
interface Props {
|
||||
isSaving: boolean;
|
||||
isTemplateConfigured: boolean;
|
||||
onSetTemplateTime: (payload: {
|
||||
field: 'endTime' | 'startTime';
|
||||
shiftType: Exclude<ShiftType, 'off'>;
|
||||
@@ -71,6 +72,10 @@ function handleTemplateTimeChange(payload: {
|
||||
<span class="section-title">班次模板</span>
|
||||
</template>
|
||||
|
||||
<div v-if="!props.isTemplateConfigured" class="template-guide">
|
||||
当前门店尚未配置班次模板,请先设置并保存模板,再进行员工排班。
|
||||
</div>
|
||||
|
||||
<div class="template-list">
|
||||
<div
|
||||
v-for="row in templateRows"
|
||||
@@ -113,7 +118,11 @@ function handleTemplateTimeChange(payload: {
|
||||
</div>
|
||||
|
||||
<div class="template-tip">
|
||||
调整模板后,个人排班和周排班中的同类型班次会同步到新的时间段。
|
||||
{{
|
||||
props.isTemplateConfigured
|
||||
? '调整模板后,个人排班和周排班中的同类型班次会同步到新的时间段。'
|
||||
: '模板保存成功后,员工列表中的“排班”和“编辑排班”会自动可用。'
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="staff-card-actions">
|
||||
|
||||
@@ -40,6 +40,12 @@ function getDayShift(dayOfWeek: number) {
|
||||
return props.form.shifts.find((item) => item.dayOfWeek === dayOfWeek);
|
||||
}
|
||||
|
||||
/** 判断某日是否展示时间编辑。 */
|
||||
function isTimeHidden(dayOfWeek: number) {
|
||||
const shift = getDayShift(dayOfWeek);
|
||||
return !shift || shift.shiftType === 'off';
|
||||
}
|
||||
|
||||
/** 转换时间组件值。 */
|
||||
function toPickerValue(time: string) {
|
||||
if (!time) return undefined;
|
||||
@@ -65,7 +71,7 @@ function toTimeText(value: Dayjs | null | string | undefined) {
|
||||
@update:open="(value) => emit('update:open', value)"
|
||||
>
|
||||
<div class="staff-schedule-hint">
|
||||
点击班次胶囊可自动填充时间,选择“休息”后该日不排班。
|
||||
点击班次胶囊可自动填充时间;显示“未配置”表示该日尚未设置排班。
|
||||
</div>
|
||||
|
||||
<div class="staff-schedule-list">
|
||||
@@ -94,14 +100,19 @@ function toTimeText(value: Dayjs | null | string | undefined) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span v-if="!getDayShift(day.dayOfWeek)" class="staff-schedule-empty">
|
||||
未配置
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="staff-schedule-time"
|
||||
:class="{ hidden: getDayShift(day.dayOfWeek)?.shiftType === 'off' }"
|
||||
:class="{ hidden: isTimeHidden(day.dayOfWeek) }"
|
||||
>
|
||||
<TimePicker
|
||||
:value="toPickerValue(getDayShift(day.dayOfWeek)?.startTime || '')"
|
||||
format="HH:mm"
|
||||
:allow-clear="false"
|
||||
:disabled="isTimeHidden(day.dayOfWeek)"
|
||||
@update:value="
|
||||
(value) => props.onSetShiftStart(day.dayOfWeek, toTimeText(value))
|
||||
"
|
||||
@@ -111,6 +122,7 @@ function toTimeText(value: Dayjs | null | string | undefined) {
|
||||
:value="toPickerValue(getDayShift(day.dayOfWeek)?.endTime || '')"
|
||||
format="HH:mm"
|
||||
:allow-clear="false"
|
||||
:disabled="isTimeHidden(day.dayOfWeek)"
|
||||
@update:value="
|
||||
(value) => props.onSetShiftEnd(day.dayOfWeek, toTimeText(value))
|
||||
"
|
||||
|
||||
@@ -18,6 +18,7 @@ interface Props {
|
||||
getShiftClass: (shiftType: ShiftType) => string;
|
||||
getShiftLabel: (shiftType: ShiftType) => string;
|
||||
getShiftTimeText: (shift: StaffDayShiftDto) => string;
|
||||
isTemplateConfigured: boolean;
|
||||
isSaving: boolean;
|
||||
legendItems: ShiftLegendItem[];
|
||||
open: boolean;
|
||||
@@ -33,19 +34,30 @@ const emit = defineEmits<{
|
||||
(event: 'update:open', value: boolean): void;
|
||||
}>();
|
||||
|
||||
/** 获取某行某天班次,缺失时兜底为休息。 */
|
||||
/** 获取某行某天班次,缺失时返回未配置。 */
|
||||
function resolveDayShift(
|
||||
row: WeekEditorRow,
|
||||
dayOfWeek: number,
|
||||
): StaffDayShiftDto {
|
||||
return (
|
||||
row.shifts.find((item) => item.dayOfWeek === dayOfWeek) ?? {
|
||||
dayOfWeek,
|
||||
shiftType: 'off',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
}
|
||||
);
|
||||
): StaffDayShiftDto | undefined {
|
||||
return row.shifts.find((item) => item.dayOfWeek === dayOfWeek);
|
||||
}
|
||||
|
||||
/** 获取单元格样式。 */
|
||||
function resolveCellClass(shift: StaffDayShiftDto | undefined) {
|
||||
if (!shift) return 'shift-unconfigured';
|
||||
return props.getShiftClass(shift.shiftType);
|
||||
}
|
||||
|
||||
/** 获取单元格班次文案。 */
|
||||
function resolveCellLabel(shift: StaffDayShiftDto | undefined) {
|
||||
if (!shift) return '未配置';
|
||||
return props.getShiftLabel(shift.shiftType);
|
||||
}
|
||||
|
||||
/** 获取单元格时间文案。 */
|
||||
function resolveCellTimeText(shift: StaffDayShiftDto | undefined) {
|
||||
if (!shift) return '未配置';
|
||||
return props.getShiftTimeText(shift);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -74,7 +86,11 @@ function resolveDayShift(
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="props.rows.length === 0" class="staff-week-empty">
|
||||
<div v-if="!props.isTemplateConfigured" class="staff-week-empty">
|
||||
<Empty description="未配置班次模板,请先配置模板后再编辑周排班" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="props.rows.length === 0" class="staff-week-empty">
|
||||
<Empty description="暂无可编辑排班" />
|
||||
</div>
|
||||
|
||||
@@ -105,11 +121,7 @@ function resolveDayShift(
|
||||
<button
|
||||
type="button"
|
||||
class="staff-week-cell"
|
||||
:class="
|
||||
props.getShiftClass(
|
||||
resolveDayShift(row, day.dayOfWeek).shiftType,
|
||||
)
|
||||
"
|
||||
:class="resolveCellClass(resolveDayShift(row, day.dayOfWeek))"
|
||||
@click="
|
||||
emit('cycleShift', {
|
||||
staffId: row.staffId,
|
||||
@@ -118,16 +130,10 @@ function resolveDayShift(
|
||||
"
|
||||
>
|
||||
<span class="staff-week-cell-label">
|
||||
{{
|
||||
props.getShiftLabel(
|
||||
resolveDayShift(row, day.dayOfWeek).shiftType,
|
||||
)
|
||||
}}
|
||||
{{ resolveCellLabel(resolveDayShift(row, day.dayOfWeek)) }}
|
||||
</span>
|
||||
<span class="staff-week-cell-time">
|
||||
{{
|
||||
props.getShiftTimeText(resolveDayShift(row, day.dayOfWeek))
|
||||
}}
|
||||
{{ resolveCellTimeText(resolveDayShift(row, day.dayOfWeek)) }}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
@@ -141,6 +147,7 @@ function resolveDayShift(
|
||||
<Button @click="emit('update:open', false)">取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:disabled="!props.isTemplateConfigured"
|
||||
:loading="props.isSaving"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
|
||||
@@ -14,6 +14,8 @@ interface Props {
|
||||
getShiftClass: (shiftType: ShiftType) => string;
|
||||
getShiftLabel: (shiftType: ShiftType) => string;
|
||||
getShiftTimeText: (shift: StaffDayShiftDto) => string;
|
||||
isScheduleConfigured: boolean;
|
||||
isTemplateConfigured: boolean;
|
||||
roleLabelResolver: (roleType: WeekEditorRow['roleType']) => string;
|
||||
rows: WeekEditorRow[];
|
||||
}
|
||||
@@ -24,19 +26,30 @@ const emit = defineEmits<{
|
||||
(event: 'editWeek'): void;
|
||||
}>();
|
||||
|
||||
/** 读取员工某日排班,缺失时兜底为休息。 */
|
||||
/** 读取员工某日排班,缺失时返回未配置。 */
|
||||
function resolveDayShift(
|
||||
row: WeekEditorRow,
|
||||
dayOfWeek: number,
|
||||
): StaffDayShiftDto {
|
||||
return (
|
||||
row.shifts.find((item) => item.dayOfWeek === dayOfWeek) ?? {
|
||||
dayOfWeek,
|
||||
shiftType: 'off',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
}
|
||||
);
|
||||
): StaffDayShiftDto | undefined {
|
||||
return row.shifts.find((item) => item.dayOfWeek === dayOfWeek);
|
||||
}
|
||||
|
||||
/** 读取某单元格样式。 */
|
||||
function resolveCellClass(shift: StaffDayShiftDto | undefined) {
|
||||
if (!shift) return 'shift-unconfigured';
|
||||
return props.getShiftClass(shift.shiftType);
|
||||
}
|
||||
|
||||
/** 读取某单元格时间文案。 */
|
||||
function resolveCellTimeText(shift: StaffDayShiftDto | undefined) {
|
||||
if (!shift) return '未配置';
|
||||
return props.getShiftTimeText(shift);
|
||||
}
|
||||
|
||||
/** 读取某单元格班次文案。 */
|
||||
function resolveCellLabel(shift: StaffDayShiftDto | undefined) {
|
||||
if (!shift) return '未配置';
|
||||
return props.getShiftLabel(shift.shiftType);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,14 +59,28 @@ function resolveDayShift(
|
||||
<span class="section-title">本周排班</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<Button @click="emit('editWeek')">编辑排班</Button>
|
||||
<Button
|
||||
:disabled="!props.isTemplateConfigured"
|
||||
:title="props.isTemplateConfigured ? '' : '请先配置班次模板'"
|
||||
@click="emit('editWeek')"
|
||||
>
|
||||
编辑排班
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<div v-if="props.rows.length === 0" class="schedule-empty">
|
||||
<div v-if="!props.isTemplateConfigured" class="schedule-empty">
|
||||
<Empty description="未配置班次模板,请先在上方完成模板设置" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="props.rows.length === 0" class="schedule-empty">
|
||||
<Empty description="暂无可排班员工" />
|
||||
</div>
|
||||
|
||||
<div v-else class="schedule-board-wrap">
|
||||
<div v-if="!props.isScheduleConfigured" class="schedule-board-tip">
|
||||
当前门店还没有排班数据,点击“编辑排班”开始配置。
|
||||
</div>
|
||||
|
||||
<table class="schedule-board-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -78,23 +105,13 @@ function resolveDayShift(
|
||||
>
|
||||
<div
|
||||
class="schedule-cell"
|
||||
:class="
|
||||
props.getShiftClass(
|
||||
resolveDayShift(row, day.dayOfWeek).shiftType,
|
||||
)
|
||||
"
|
||||
:class="resolveCellClass(resolveDayShift(row, day.dayOfWeek))"
|
||||
>
|
||||
<span class="schedule-cell-time">
|
||||
{{
|
||||
props.getShiftTimeText(resolveDayShift(row, day.dayOfWeek))
|
||||
}}
|
||||
{{ resolveCellTimeText(resolveDayShift(row, day.dayOfWeek)) }}
|
||||
</span>
|
||||
<span class="schedule-cell-label">
|
||||
{{
|
||||
props.getShiftLabel(
|
||||
resolveDayShift(row, day.dayOfWeek).shiftType,
|
||||
)
|
||||
}}
|
||||
{{ resolveCellLabel(resolveDayShift(row, day.dayOfWeek)) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -99,18 +99,18 @@ export const STAFF_PERMISSION_OPTIONS: StaffPermissionOption[] = [
|
||||
{ value: '数据统计', label: '数据统计' },
|
||||
];
|
||||
|
||||
export const DEFAULT_SHIFT_TEMPLATES: StoreShiftTemplatesDto = {
|
||||
export const EMPTY_SHIFT_TEMPLATES: StoreShiftTemplatesDto = {
|
||||
morning: {
|
||||
startTime: '09:00',
|
||||
endTime: '14:00',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
},
|
||||
evening: {
|
||||
startTime: '14:00',
|
||||
endTime: '21:00',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
},
|
||||
full: {
|
||||
startTime: '09:00',
|
||||
endTime: '21:00',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -22,16 +22,18 @@ import {
|
||||
getStoreStaffScheduleApi,
|
||||
} from '#/api/store-staff';
|
||||
|
||||
import { EMPTY_SHIFT_TEMPLATES } from './constants';
|
||||
import {
|
||||
cloneScheduleMap,
|
||||
cloneShifts,
|
||||
cloneTemplates,
|
||||
createEmptyWeekShifts,
|
||||
normalizeWeekShifts,
|
||||
normalizeSparseWeekShifts,
|
||||
} from './helpers';
|
||||
|
||||
interface CreateDataActionsOptions {
|
||||
filters: StaffFilterState;
|
||||
isScheduleConfigured: Ref<boolean>;
|
||||
isTemplateConfigured: Ref<boolean>;
|
||||
isPageLoading: Ref<boolean>;
|
||||
isStoreLoading: Ref<boolean>;
|
||||
scheduleMap: Ref<Record<string, StaffDayShiftDto[]>>;
|
||||
@@ -45,11 +47,21 @@ interface CreateDataActionsOptions {
|
||||
}
|
||||
|
||||
export function createDataActions(options: CreateDataActionsOptions) {
|
||||
/** 判断当前排班映射是否包含任意配置。 */
|
||||
function hasAnyConfiguredShift(
|
||||
scheduleMap: Record<string, StaffDayShiftDto[]>,
|
||||
) {
|
||||
return Object.values(scheduleMap).some((shifts) => shifts.length > 0);
|
||||
}
|
||||
|
||||
/** 清空当前门店相关数据。 */
|
||||
function clearStoreData() {
|
||||
options.staffRows.value = [];
|
||||
options.staffDirectory.value = [];
|
||||
options.staffTotal.value = 0;
|
||||
options.templates.value = cloneTemplates(EMPTY_SHIFT_TEMPLATES);
|
||||
options.isTemplateConfigured.value = false;
|
||||
options.isScheduleConfigured.value = false;
|
||||
options.scheduleMap.value = {};
|
||||
options.scheduleSnapshot.value = null;
|
||||
}
|
||||
@@ -97,6 +109,9 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
/** 加载班次模板与排班配置。 */
|
||||
async function loadScheduleSettings() {
|
||||
if (!options.selectedStoreId.value) {
|
||||
options.templates.value = cloneTemplates(EMPTY_SHIFT_TEMPLATES);
|
||||
options.isTemplateConfigured.value = false;
|
||||
options.isScheduleConfigured.value = false;
|
||||
options.scheduleMap.value = {};
|
||||
options.scheduleSnapshot.value = null;
|
||||
return;
|
||||
@@ -105,18 +120,19 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
const result = await getStoreStaffScheduleApi(
|
||||
options.selectedStoreId.value,
|
||||
);
|
||||
const templates = cloneTemplates(result.templates);
|
||||
const templates = cloneTemplates(result.templates ?? EMPTY_SHIFT_TEMPLATES);
|
||||
|
||||
const scheduleMap: Record<string, StaffDayShiftDto[]> = {};
|
||||
for (const schedule of result.schedules ?? []) {
|
||||
scheduleMap[schedule.staffId] = normalizeWeekShifts({
|
||||
scheduleMap[schedule.staffId] = normalizeSparseWeekShifts({
|
||||
shifts: schedule.shifts,
|
||||
fallback: createEmptyWeekShifts(templates),
|
||||
templates,
|
||||
});
|
||||
}
|
||||
|
||||
options.templates.value = templates;
|
||||
options.isTemplateConfigured.value = Boolean(result.isTemplateConfigured);
|
||||
options.isScheduleConfigured.value = Boolean(result.isScheduleConfigured);
|
||||
options.scheduleMap.value = scheduleMap;
|
||||
options.scheduleSnapshot.value = {
|
||||
templates: cloneTemplates(templates),
|
||||
@@ -195,6 +211,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
[staffId]: cloneShifts(shifts),
|
||||
};
|
||||
options.scheduleMap.value = nextMap;
|
||||
options.isScheduleConfigured.value = hasAnyConfiguredShift(nextMap);
|
||||
options.scheduleSnapshot.value = {
|
||||
templates: cloneTemplates(options.templates.value),
|
||||
scheduleMap: cloneScheduleMap(nextMap),
|
||||
@@ -204,6 +221,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
/** 批量更新排班映射。 */
|
||||
function patchScheduleMap(nextMap: Record<string, StaffDayShiftDto[]>) {
|
||||
options.scheduleMap.value = cloneScheduleMap(nextMap);
|
||||
options.isScheduleConfigured.value = hasAnyConfiguredShift(nextMap);
|
||||
options.scheduleSnapshot.value = {
|
||||
templates: cloneTemplates(options.templates.value),
|
||||
scheduleMap: cloneScheduleMap(nextMap),
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import type {
|
||||
ShiftType,
|
||||
StaffDayShiftDto,
|
||||
StaffScheduleDto,
|
||||
StoreShiftTemplatesDto,
|
||||
} from '#/api/store-staff';
|
||||
import type {
|
||||
@@ -15,8 +14,6 @@ import type {
|
||||
WeekEditorRow,
|
||||
} from '#/views/store/staff/types';
|
||||
|
||||
import { DAY_OPTIONS } from './constants';
|
||||
|
||||
/** 深拷贝模板。 */
|
||||
export function cloneTemplates(
|
||||
source: StoreShiftTemplatesDto,
|
||||
@@ -68,18 +65,6 @@ export function cloneStaffForm(
|
||||
};
|
||||
}
|
||||
|
||||
/** 按模板创建空白一周(默认休息)。 */
|
||||
export function createEmptyWeekShifts(
|
||||
_templates: StoreShiftTemplatesDto,
|
||||
): StaffDayShiftDto[] {
|
||||
return DAY_OPTIONS.map((day) => ({
|
||||
dayOfWeek: day.dayOfWeek,
|
||||
shiftType: 'off' as const,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
}));
|
||||
}
|
||||
|
||||
/** 生成指定班次默认时间。 */
|
||||
export function resolveShiftTimeByType(
|
||||
shiftType: ShiftType,
|
||||
@@ -124,58 +109,40 @@ export function normalizeDayShift(payload: {
|
||||
return {
|
||||
dayOfWeek: payload.dayOfWeek,
|
||||
shiftType,
|
||||
startTime: normalizeTime(payload.shift.startTime, templateTime.startTime),
|
||||
endTime: normalizeTime(payload.shift.endTime, templateTime.endTime),
|
||||
startTime: normalizeTime(
|
||||
payload.shift.startTime,
|
||||
payload.fallback?.startTime || templateTime.startTime,
|
||||
),
|
||||
endTime: normalizeTime(
|
||||
payload.shift.endTime,
|
||||
payload.fallback?.endTime || templateTime.endTime,
|
||||
),
|
||||
} satisfies StaffDayShiftDto;
|
||||
}
|
||||
|
||||
/** 归一化周排班数组,确保包含 0-6 共七天。 */
|
||||
export function normalizeWeekShifts(payload: {
|
||||
fallback: StaffDayShiftDto[];
|
||||
/** 归一化周排班数组(仅保留真实配置的天)。 */
|
||||
export function normalizeSparseWeekShifts(payload: {
|
||||
shifts: Partial<StaffDayShiftDto>[];
|
||||
templates: StoreShiftTemplatesDto;
|
||||
}) {
|
||||
const shiftMap = new Map<number, Partial<StaffDayShiftDto>>();
|
||||
const shiftMap = new Map<number, StaffDayShiftDto>();
|
||||
for (const shift of payload.shifts) {
|
||||
const dayOfWeek = Number(shift.dayOfWeek);
|
||||
if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
|
||||
continue;
|
||||
}
|
||||
shiftMap.set(dayOfWeek, shift);
|
||||
}
|
||||
|
||||
return DAY_OPTIONS.map((day) => {
|
||||
const fallbackShift = payload.fallback.find(
|
||||
(item) => item.dayOfWeek === day.dayOfWeek,
|
||||
);
|
||||
return normalizeDayShift({
|
||||
dayOfWeek: day.dayOfWeek,
|
||||
shift: shiftMap.get(day.dayOfWeek) ?? fallbackShift ?? {},
|
||||
fallback: fallbackShift,
|
||||
templates: payload.templates,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 将接口排班数组转换为映射。 */
|
||||
export function createScheduleMap(schedules: StaffScheduleDto[]) {
|
||||
const scheduleMap: Record<string, StaffDayShiftDto[]> = {};
|
||||
for (const schedule of schedules) {
|
||||
scheduleMap[schedule.staffId] = normalizeWeekShifts({
|
||||
shifts: schedule.shifts,
|
||||
fallback: createEmptyWeekShifts({
|
||||
morning: { startTime: '09:00', endTime: '14:00' },
|
||||
evening: { startTime: '14:00', endTime: '21:00' },
|
||||
full: { startTime: '09:00', endTime: '21:00' },
|
||||
shiftMap.set(
|
||||
dayOfWeek,
|
||||
normalizeDayShift({
|
||||
dayOfWeek,
|
||||
shift,
|
||||
templates: payload.templates,
|
||||
}),
|
||||
templates: {
|
||||
morning: { startTime: '09:00', endTime: '14:00' },
|
||||
evening: { startTime: '14:00', endTime: '21:00' },
|
||||
full: { startTime: '09:00', endTime: '21:00' },
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
return scheduleMap;
|
||||
|
||||
return [...shiftMap.values()].toSorted((a, b) => a.dayOfWeek - b.dayOfWeek);
|
||||
}
|
||||
|
||||
/** 将员工排班矩阵转为可编辑周视图行。 */
|
||||
@@ -187,16 +154,13 @@ export function buildWeekEditorRows(payload: {
|
||||
roleType: WeekEditorRow['roleType'];
|
||||
status: WeekEditorRow['status'];
|
||||
}>;
|
||||
templates: StoreShiftTemplatesDto;
|
||||
}) {
|
||||
return payload.staffs.map((staff) => ({
|
||||
staffId: staff.id,
|
||||
staffName: staff.name,
|
||||
roleType: staff.roleType,
|
||||
status: staff.status,
|
||||
shifts: cloneShifts(
|
||||
payload.scheduleMap[staff.id] ?? createEmptyWeekShifts(payload.templates),
|
||||
),
|
||||
shifts: cloneShifts(payload.scheduleMap[staff.id] ?? []),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -207,21 +171,27 @@ export function updateWeekRowShift(payload: {
|
||||
row: WeekEditorRow;
|
||||
templates: StoreShiftTemplatesDto;
|
||||
}) {
|
||||
const currentShift = payload.row.shifts.find(
|
||||
(item) => item.dayOfWeek === payload.dayOfWeek,
|
||||
);
|
||||
const nextShift = normalizeDayShift({
|
||||
dayOfWeek: payload.dayOfWeek,
|
||||
shift: {
|
||||
dayOfWeek: payload.dayOfWeek,
|
||||
shiftType: payload.nextShiftType,
|
||||
},
|
||||
fallback: currentShift,
|
||||
templates: payload.templates,
|
||||
});
|
||||
|
||||
return {
|
||||
...payload.row,
|
||||
shifts: payload.row.shifts.map((item) => {
|
||||
if (item.dayOfWeek !== payload.dayOfWeek) return { ...item };
|
||||
const normalized = normalizeDayShift({
|
||||
dayOfWeek: payload.dayOfWeek,
|
||||
shift: {
|
||||
dayOfWeek: payload.dayOfWeek,
|
||||
shiftType: payload.nextShiftType,
|
||||
},
|
||||
fallback: item,
|
||||
templates: payload.templates,
|
||||
});
|
||||
return normalized;
|
||||
}),
|
||||
shifts: [
|
||||
...payload.row.shifts
|
||||
.filter((item) => item.dayOfWeek !== payload.dayOfWeek)
|
||||
.map((item) => ({ ...item })),
|
||||
nextShift,
|
||||
].toSorted((a, b) => a.dayOfWeek - b.dayOfWeek),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ import {
|
||||
buildWeekEditorRows,
|
||||
cloneScheduleMap,
|
||||
cloneShifts,
|
||||
createEmptyWeekShifts,
|
||||
normalizeDayShift,
|
||||
normalizeSparseWeekShifts,
|
||||
normalizeTime,
|
||||
updateWeekRowShift,
|
||||
} from './helpers';
|
||||
@@ -57,50 +57,74 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
|
||||
options.personalForm.staffName = staff.name;
|
||||
options.personalForm.roleType = staff.roleType;
|
||||
options.personalForm.shifts = cloneShifts(
|
||||
options.scheduleMap.value[staff.id] ??
|
||||
createEmptyWeekShifts(options.templates.value),
|
||||
options.scheduleMap.value[staff.id] ?? [],
|
||||
);
|
||||
options.isPersonalDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 按天更新个人排班(不存在时自动补入)。 */
|
||||
function upsertPersonalShift(dayOfWeek: number, nextShift: StaffDayShiftDto) {
|
||||
options.personalForm.shifts = [
|
||||
...options.personalForm.shifts
|
||||
.filter((item) => item.dayOfWeek !== dayOfWeek)
|
||||
.map((item) => ({ ...item })),
|
||||
nextShift,
|
||||
].toSorted((a, b) => a.dayOfWeek - b.dayOfWeek);
|
||||
}
|
||||
|
||||
/** 更新个人排班班次类型。 */
|
||||
function setPersonalShiftType(dayOfWeek: number, shiftType: ShiftType) {
|
||||
options.personalForm.shifts = options.personalForm.shifts.map((item) => {
|
||||
if (item.dayOfWeek !== dayOfWeek) return { ...item };
|
||||
return normalizeDayShift({
|
||||
const currentShift = options.personalForm.shifts.find(
|
||||
(item) => item.dayOfWeek === dayOfWeek,
|
||||
);
|
||||
const nextShift = normalizeDayShift({
|
||||
dayOfWeek,
|
||||
shift: {
|
||||
dayOfWeek,
|
||||
shift: {
|
||||
...item,
|
||||
shiftType,
|
||||
},
|
||||
fallback: item,
|
||||
templates: options.templates.value,
|
||||
});
|
||||
shiftType,
|
||||
},
|
||||
fallback: currentShift,
|
||||
templates: options.templates.value,
|
||||
});
|
||||
upsertPersonalShift(dayOfWeek, nextShift);
|
||||
}
|
||||
|
||||
/** 更新个人排班开始时间。 */
|
||||
function setPersonalShiftStart(dayOfWeek: number, startTime: string) {
|
||||
options.personalForm.shifts = options.personalForm.shifts.map((item) => {
|
||||
if (item.dayOfWeek !== dayOfWeek) return { ...item };
|
||||
if (item.shiftType === 'off') return { ...item };
|
||||
return {
|
||||
...item,
|
||||
startTime: normalizeTime(startTime, item.startTime),
|
||||
};
|
||||
const currentShift = options.personalForm.shifts.find(
|
||||
(item) => item.dayOfWeek === dayOfWeek,
|
||||
);
|
||||
if (!currentShift || currentShift.shiftType === 'off') return;
|
||||
|
||||
const nextShift = normalizeDayShift({
|
||||
dayOfWeek,
|
||||
shift: {
|
||||
...currentShift,
|
||||
startTime: normalizeTime(startTime, currentShift.startTime),
|
||||
},
|
||||
fallback: currentShift,
|
||||
templates: options.templates.value,
|
||||
});
|
||||
upsertPersonalShift(dayOfWeek, nextShift);
|
||||
}
|
||||
|
||||
/** 更新个人排班结束时间。 */
|
||||
function setPersonalShiftEnd(dayOfWeek: number, endTime: string) {
|
||||
options.personalForm.shifts = options.personalForm.shifts.map((item) => {
|
||||
if (item.dayOfWeek !== dayOfWeek) return { ...item };
|
||||
if (item.shiftType === 'off') return { ...item };
|
||||
return {
|
||||
...item,
|
||||
endTime: normalizeTime(endTime, item.endTime),
|
||||
};
|
||||
const currentShift = options.personalForm.shifts.find(
|
||||
(item) => item.dayOfWeek === dayOfWeek,
|
||||
);
|
||||
if (!currentShift || currentShift.shiftType === 'off') return;
|
||||
|
||||
const nextShift = normalizeDayShift({
|
||||
dayOfWeek,
|
||||
shift: {
|
||||
...currentShift,
|
||||
endTime: normalizeTime(endTime, currentShift.endTime),
|
||||
},
|
||||
fallback: currentShift,
|
||||
templates: options.templates.value,
|
||||
});
|
||||
upsertPersonalShift(dayOfWeek, nextShift);
|
||||
}
|
||||
|
||||
/** 提交个人排班。 */
|
||||
@@ -115,7 +139,13 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
|
||||
shifts: cloneShifts(options.personalForm.shifts),
|
||||
});
|
||||
|
||||
options.patchStaffSchedule(result.staffId, result.shifts);
|
||||
options.patchStaffSchedule(
|
||||
result.staffId,
|
||||
normalizeSparseWeekShifts({
|
||||
shifts: result.shifts,
|
||||
templates: options.templates.value,
|
||||
}),
|
||||
);
|
||||
options.isPersonalDrawerOpen.value = false;
|
||||
message.success('员工排班已保存');
|
||||
} catch (error) {
|
||||
@@ -133,7 +163,6 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
|
||||
options.weekRows.value = buildWeekEditorRows({
|
||||
staffs: editableStaffs,
|
||||
scheduleMap: options.scheduleMap.value,
|
||||
templates: options.templates.value,
|
||||
});
|
||||
options.isWeekDrawerOpen.value = true;
|
||||
}
|
||||
@@ -178,7 +207,10 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
|
||||
|
||||
const nextMap = cloneScheduleMap(options.scheduleMap.value);
|
||||
for (const schedule of result.schedules ?? []) {
|
||||
nextMap[schedule.staffId] = cloneShifts(schedule.shifts);
|
||||
nextMap[schedule.staffId] = normalizeSparseWeekShifts({
|
||||
shifts: cloneShifts(schedule.shifts),
|
||||
templates: options.templates.value,
|
||||
});
|
||||
}
|
||||
options.patchScheduleMap(nextMap);
|
||||
|
||||
|
||||
@@ -24,11 +24,13 @@ import type {
|
||||
|
||||
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
DAY_OPTIONS,
|
||||
DEFAULT_FILTER_STATE,
|
||||
DEFAULT_SHIFT_TEMPLATES,
|
||||
DEFAULT_STAFF_FORM,
|
||||
EMPTY_SHIFT_TEMPLATES,
|
||||
PAGE_SIZE_OPTIONS,
|
||||
SHIFT_OPTIONS,
|
||||
STAFF_PERMISSION_OPTIONS,
|
||||
@@ -42,11 +44,9 @@ import {
|
||||
cloneShifts,
|
||||
cloneStaffForm,
|
||||
cloneTemplates,
|
||||
createEmptyWeekShifts,
|
||||
formatShiftTimeText,
|
||||
getCurrentWeekStartDate,
|
||||
maskPhone,
|
||||
normalizeWeekShifts,
|
||||
resolveAvatarText,
|
||||
} from './staff-page/helpers';
|
||||
import { createScheduleActions } from './staff-page/schedule-actions';
|
||||
@@ -75,9 +75,11 @@ export function useStoreStaffPage() {
|
||||
const staffRows = ref<StoreStaffDto[]>([]);
|
||||
const staffTotal = ref(0);
|
||||
const staffDirectory = ref<StoreStaffDto[]>([]);
|
||||
const isTemplateConfigured = ref(false);
|
||||
const isScheduleConfigured = ref(false);
|
||||
|
||||
const templates = ref<StoreShiftTemplatesDto>(
|
||||
cloneTemplates(DEFAULT_SHIFT_TEMPLATES),
|
||||
cloneTemplates(EMPTY_SHIFT_TEMPLATES),
|
||||
);
|
||||
const scheduleMap = ref<Record<string, StaffDayShiftDto[]>>({});
|
||||
const scheduleSnapshot = ref<null | {
|
||||
@@ -97,7 +99,7 @@ export function useStoreStaffPage() {
|
||||
staffId: '',
|
||||
staffName: '',
|
||||
roleType: 'cashier',
|
||||
shifts: createEmptyWeekShifts(DEFAULT_SHIFT_TEMPLATES),
|
||||
shifts: [],
|
||||
});
|
||||
|
||||
const isWeekDrawerOpen = ref(false);
|
||||
@@ -182,9 +184,7 @@ export function useStoreStaffPage() {
|
||||
staffName: staff.name,
|
||||
roleType: staff.roleType,
|
||||
status: staff.status,
|
||||
shifts: cloneShifts(
|
||||
scheduleMap.value[staff.id] ?? createEmptyWeekShifts(templates.value),
|
||||
),
|
||||
shifts: cloneShifts(scheduleMap.value[staff.id] ?? []),
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -195,7 +195,10 @@ export function useStoreStaffPage() {
|
||||
type: item.value as Exclude<ShiftType, 'off'>,
|
||||
label: item.label,
|
||||
className: item.className,
|
||||
timeText: `${template.startTime}-${template.endTime}`,
|
||||
timeText:
|
||||
template.startTime && template.endTime
|
||||
? `${template.startTime}-${template.endTime}`
|
||||
: '未配置',
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -212,6 +215,8 @@ export function useStoreStaffPage() {
|
||||
reloadStoreData,
|
||||
} = createDataActions({
|
||||
filters,
|
||||
isScheduleConfigured,
|
||||
isTemplateConfigured,
|
||||
isPageLoading,
|
||||
isStoreLoading,
|
||||
scheduleMap,
|
||||
@@ -349,7 +354,7 @@ export function useStoreStaffPage() {
|
||||
|
||||
/** 判断员工是否可进入排班。 */
|
||||
function isScheduleDisabled(staff: StoreStaffDto) {
|
||||
return staff.status === 'resigned';
|
||||
return staff.status === 'resigned' || !isTemplateConfigured.value;
|
||||
}
|
||||
|
||||
/** 打开个人排班抽屉。 */
|
||||
@@ -358,6 +363,15 @@ export function useStoreStaffPage() {
|
||||
openPersonalScheduleDrawer(staff);
|
||||
}
|
||||
|
||||
/** 打开周排班抽屉。 */
|
||||
function handleOpenWeekSchedule() {
|
||||
if (!isTemplateConfigured.value) {
|
||||
message.warning('请先配置班次模板,再编辑排班');
|
||||
return;
|
||||
}
|
||||
openWeekScheduleDrawer();
|
||||
}
|
||||
|
||||
/** 获取角色展示文案。 */
|
||||
function getRoleLabel(roleType: StaffRoleType) {
|
||||
return roleOptionMap.value.get(roleType)?.label ?? roleType;
|
||||
@@ -390,23 +404,6 @@ export function useStoreStaffPage() {
|
||||
return shiftOptionMap.value.get(shiftType)?.className ?? 'shift-off';
|
||||
}
|
||||
|
||||
/** 获取员工某天排班。 */
|
||||
function getStaffShift(row: WeekEditorRow, dayOfWeek: number) {
|
||||
return (
|
||||
row.shifts.find((item) => item.dayOfWeek === dayOfWeek) ??
|
||||
normalizeWeekShifts({
|
||||
shifts: [],
|
||||
fallback: createEmptyWeekShifts(templates.value),
|
||||
templates: templates.value,
|
||||
}).find((item) => item.dayOfWeek === dayOfWeek) ?? {
|
||||
dayOfWeek,
|
||||
shiftType: 'off',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 7. 监听门店切换。
|
||||
watch(selectedStoreId, async (storeId) => {
|
||||
if (!storeId) {
|
||||
@@ -437,11 +434,12 @@ export function useStoreStaffPage() {
|
||||
deletingStaffId,
|
||||
filters,
|
||||
getCurrentWeekStartDate,
|
||||
isScheduleConfigured,
|
||||
isTemplateConfigured,
|
||||
getRoleLabel,
|
||||
getRoleTagClass,
|
||||
getShiftClass,
|
||||
getShiftLabel,
|
||||
getStaffShift,
|
||||
getStatusDotClass,
|
||||
getStatusLabel,
|
||||
handleCopyCheckAll,
|
||||
@@ -470,7 +468,7 @@ export function useStoreStaffPage() {
|
||||
maskPhone,
|
||||
openCopyModal,
|
||||
openStaffDrawer,
|
||||
openWeekScheduleDrawer,
|
||||
handleOpenWeekSchedule,
|
||||
personalDrawerTitle,
|
||||
personalScheduleForm,
|
||||
resetFilters,
|
||||
|
||||
@@ -43,6 +43,7 @@ const {
|
||||
handleCopyCheckAll,
|
||||
handleCopySubmit,
|
||||
handleDeleteStaff,
|
||||
handleOpenWeekSchedule,
|
||||
handleOpenPersonalSchedule,
|
||||
handlePageChange,
|
||||
handleSavePersonalSchedule,
|
||||
@@ -56,17 +57,18 @@ const {
|
||||
isPageLoading,
|
||||
isPersonalDrawerOpen,
|
||||
isPersonalSaving,
|
||||
isScheduleConfigured,
|
||||
isScheduleDisabled,
|
||||
isStaffDrawerOpen,
|
||||
isStaffSaving,
|
||||
isStoreLoading,
|
||||
isTemplateSaving,
|
||||
isTemplateConfigured,
|
||||
isWeekDrawerOpen,
|
||||
isWeekSaving,
|
||||
maskPhone,
|
||||
openCopyModal,
|
||||
openStaffDrawer,
|
||||
openWeekScheduleDrawer,
|
||||
personalDrawerTitle,
|
||||
personalScheduleForm,
|
||||
resetFilters,
|
||||
@@ -164,6 +166,7 @@ const {
|
||||
<ShiftTemplateCard
|
||||
:templates="templates"
|
||||
:is-saving="isTemplateSaving"
|
||||
:is-template-configured="isTemplateConfigured"
|
||||
:on-set-template-time="setTemplateTime"
|
||||
@save="handleSaveTemplates"
|
||||
@reset="resetTemplates"
|
||||
@@ -175,8 +178,10 @@ const {
|
||||
:get-shift-class="getShiftClass"
|
||||
:get-shift-label="getShiftLabel"
|
||||
:get-shift-time-text="formatShiftTimeText"
|
||||
:is-template-configured="isTemplateConfigured"
|
||||
:is-schedule-configured="isScheduleConfigured"
|
||||
:role-label-resolver="getRoleLabel"
|
||||
@edit-week="openWeekScheduleDrawer"
|
||||
@edit-week="handleOpenWeekSchedule"
|
||||
/>
|
||||
</Spin>
|
||||
</template>
|
||||
@@ -223,6 +228,7 @@ const {
|
||||
:get-shift-class="getShiftClass"
|
||||
:get-shift-label="getShiftLabel"
|
||||
:get-shift-time-text="formatShiftTimeText"
|
||||
:is-template-configured="isTemplateConfigured"
|
||||
:role-label-resolver="getRoleLabel"
|
||||
@update:open="setWeekDrawerOpen"
|
||||
@cycle-shift="cycleWeekShift"
|
||||
|
||||
@@ -241,6 +241,12 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.staff-schedule-empty {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.staff-shift-pill {
|
||||
min-width: 46px;
|
||||
padding: 4px 10px;
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.schedule-board-tip {
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
color: #ad6800;
|
||||
background: #fffbe6;
|
||||
border-bottom: 1px solid #ffe58f;
|
||||
}
|
||||
|
||||
.schedule-board-table,
|
||||
.staff-week-table {
|
||||
width: 100%;
|
||||
@@ -67,6 +75,13 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.schedule-cell.shift-unconfigured,
|
||||
.staff-week-cell.shift-unconfigured {
|
||||
color: #8c8c8c;
|
||||
background: #fafafa;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.schedule-cell-time {
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/* 文件职责:班次模板区样式。 */
|
||||
.page-store-staff {
|
||||
.template-guide {
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #ad6800;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user