|
|
@@ -0,0 +1,1178 @@
|
|
|
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
+import dayjs, { type Dayjs } from 'dayjs';
|
|
|
+import {
|
|
|
+ Button,
|
|
|
+ Table,
|
|
|
+ Modal,
|
|
|
+ Form,
|
|
|
+ Input,
|
|
|
+ Select,
|
|
|
+ DatePicker,
|
|
|
+ Tag,
|
|
|
+ Row,
|
|
|
+ Col,
|
|
|
+ Checkbox,
|
|
|
+ Space,
|
|
|
+} from 'antd';
|
|
|
+import type { ColumnsType } from 'antd/es/table';
|
|
|
+import { Plus, Search, RefreshCw } from 'lucide-react';
|
|
|
+import { toast } from 'sonner';
|
|
|
+import { useTranslation } from 'react-i18next';
|
|
|
+import { materialPlanApi, PlanVO } from '../../../api/material/plan';
|
|
|
+import { materialLockerApi } from '../../../api/material/lockers';
|
|
|
+import { workstationApi } from '../../../api/workstation';
|
|
|
+import { userApi } from '../../../api/user';
|
|
|
+import { handleTree } from '../../../utils/tree';
|
|
|
+import {
|
|
|
+ pickRecords,
|
|
|
+ pickTotal,
|
|
|
+ buildPageParams,
|
|
|
+ fetchAllWorkstations,
|
|
|
+ MATERIALS_TYPE_PAGE_SIZE_MAX,
|
|
|
+} from '../materialListUtils';
|
|
|
+import {
|
|
|
+ MaterialPageRoot,
|
|
|
+ MaterialTableCard,
|
|
|
+ MaterialPaginationBar,
|
|
|
+ MaterialEditButton,
|
|
|
+ MaterialDeleteButton,
|
|
|
+ MaterialViewButton,
|
|
|
+ materialConfirmDelete,
|
|
|
+ getMaterialFormModalProps,
|
|
|
+} from '../MaterialPageLayout';
|
|
|
+
|
|
|
+const MATERIALS_CABINET_LIST_PARAMS = buildPageParams(1, -1);
|
|
|
+/** 与旧版 inspectionplan/index.vue getAutoConfigInfo 一致 */
|
|
|
+const PLAN_AUTO_FETCH_PARAM = 'test_01';
|
|
|
+const PLAN_AUTO_TEMPLATE_CODE = 'CHECK_PLAN';
|
|
|
+
|
|
|
+function firstDefined(row: Record<string, any>, keys: string[]) {
|
|
|
+ for (const k of keys) {
|
|
|
+ const v = row[k];
|
|
|
+ if (v !== undefined && v !== null && v !== '') return v;
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+}
|
|
|
+
|
|
|
+function formatPlanDate(v: unknown): string {
|
|
|
+ if (v === undefined || v === null || v === '') return '—';
|
|
|
+ const d = dayjs(v as string | number | Date);
|
|
|
+ return d.isValid() ? d.format('YYYY-MM-DD') : String(v);
|
|
|
+}
|
|
|
+
|
|
|
+function mapCabinetListToFilterOptions(rows: any[]): { value: number; label: string }[] {
|
|
|
+ const mapped = rows
|
|
|
+ .map((c: any) => {
|
|
|
+ const rawId = c.id ?? c.cabinetId;
|
|
|
+ const value = Number(rawId);
|
|
|
+ const label = String(c.cabinetName ?? c.name ?? '').trim();
|
|
|
+ return { value, label: label || (Number.isFinite(value) ? `#${value}` : '') };
|
|
|
+ })
|
|
|
+ .filter((o) => Number.isFinite(o.value) && o.label);
|
|
|
+ const hasZero = mapped.some((o) => o.value === 0);
|
|
|
+ return hasZero ? mapped : [...mapped, { value: 0, label: '空' }];
|
|
|
+}
|
|
|
+
|
|
|
+/** 物资柜名称:字符串、数组或嵌套对象 */
|
|
|
+function renderCabinetNames(_: unknown, row: any) {
|
|
|
+ const v = firstDefined(row, [
|
|
|
+ 'cabinetNames',
|
|
|
+ 'cabinet_names',
|
|
|
+ 'cabinetName',
|
|
|
+ 'cabinetNameList',
|
|
|
+ 'materialsCabinetNames',
|
|
|
+ 'cabinetList',
|
|
|
+ 'cabinets',
|
|
|
+ 'cabinetNamesStr',
|
|
|
+ ]);
|
|
|
+ if (v == null || v === '') return <span className="text-gray-400">—</span>;
|
|
|
+ if (Array.isArray(v)) {
|
|
|
+ const text = v
|
|
|
+ .map((item) => {
|
|
|
+ if (item == null) return '';
|
|
|
+ if (typeof item === 'string' || typeof item === 'number') return String(item);
|
|
|
+ return String(
|
|
|
+ firstDefined(item as Record<string, any>, ['cabinetName', 'cabinet_name', 'name', 'materialsCabinetName']) ??
|
|
|
+ '',
|
|
|
+ );
|
|
|
+ })
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(',');
|
|
|
+ return text ? (
|
|
|
+ <span className="text-left inline-block max-w-full">{text}</span>
|
|
|
+ ) : (
|
|
|
+ <span className="text-gray-400">—</span>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return <span className="text-left inline-block max-w-full">{String(v)}</span>;
|
|
|
+}
|
|
|
+
|
|
|
+/** 与旧版 dict CHECKING_STATUS + 数值兼容 */
|
|
|
+function renderPlanStatus(_: unknown, row: any) {
|
|
|
+ const name = firstDefined(row, ['planStatusName', 'plan_status_name', 'statusName', 'status_name', 'planStatusStr']);
|
|
|
+ if (name != null && String(name).trim() !== '') {
|
|
|
+ return <Tag color="blue">{String(name).trim()}</Tag>;
|
|
|
+ }
|
|
|
+ const raw = firstDefined(row, ['planStatus', 'plan_status', 'status', 'planState', 'plan_state']);
|
|
|
+ if (raw === undefined || raw === null || raw === '') {
|
|
|
+ return <span className="text-gray-400">—</span>;
|
|
|
+ }
|
|
|
+ if (typeof raw === 'string' && Number.isNaN(Number(raw))) {
|
|
|
+ const byLabel: Record<string, string> = {
|
|
|
+ 未开始: 'blue',
|
|
|
+ 进行中: 'processing',
|
|
|
+ 已完成: 'success',
|
|
|
+ 已结束: 'default',
|
|
|
+ 已取消: 'default',
|
|
|
+ };
|
|
|
+ const t = raw.trim();
|
|
|
+ return <Tag color={byLabel[t] ?? 'default'}>{t}</Tag>;
|
|
|
+ }
|
|
|
+ const n = Number(raw);
|
|
|
+ const byNum: Record<number, { label: string; color: string }> = {
|
|
|
+ 0: { label: '未开始', color: 'blue' },
|
|
|
+ 1: { label: '进行中', color: 'processing' },
|
|
|
+ 2: { label: '已完成', color: 'success' },
|
|
|
+ 3: { label: '已结束', color: 'default' },
|
|
|
+ 4: { label: '已取消', color: 'default' },
|
|
|
+ };
|
|
|
+ if (Number.isFinite(n) && byNum[n]) {
|
|
|
+ const x = byNum[n];
|
|
|
+ return <Tag color={x.color}>{x.label}</Tag>;
|
|
|
+ }
|
|
|
+ return <Tag>{String(raw)}</Tag>;
|
|
|
+}
|
|
|
+
|
|
|
+function goInspectionRecordsForPlan(row: any) {
|
|
|
+ const planId = firstDefined(row, ['planId', 'plan_id', 'id', 'materialsCheckPlanId']);
|
|
|
+ try {
|
|
|
+ const n = planId != null && planId !== '' ? Number(planId) : NaN;
|
|
|
+ const planName = String(
|
|
|
+ firstDefined(row, ['planName', 'plan_name', 'materialsCheckPlanName', 'checkPlanName', 'name']) ?? '',
|
|
|
+ ).trim();
|
|
|
+ sessionStorage.setItem(
|
|
|
+ 'materialInspectionRecordPreset',
|
|
|
+ JSON.stringify({
|
|
|
+ planId: Number.isFinite(n) ? n : undefined,
|
|
|
+ materialsCheckPlanId: Number.isFinite(n) ? n : undefined,
|
|
|
+ ...(planName ? { planName } : {}),
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+ window.dispatchEvent(
|
|
|
+ new CustomEvent('switchToMenu', {
|
|
|
+ detail: { menu: 'materialManagement', subMenu: 'materialInspectionRecord' },
|
|
|
+ }),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function flattenWorkstations(nodes: any[]): { label: string; value: number }[] {
|
|
|
+ const out: { label: string; value: number }[] = [];
|
|
|
+ const walk = (arr: any[]) => {
|
|
|
+ for (const n of arr) {
|
|
|
+ const id = Number(n.id ?? n.workstationId);
|
|
|
+ if (Number.isFinite(id) && id > 0) {
|
|
|
+ const code = n.workstationCode ? String(n.workstationCode) : '';
|
|
|
+ const name = n.workstationName ? String(n.workstationName) : '';
|
|
|
+ const label = [code, name].filter(Boolean).join(' / ') || String(id);
|
|
|
+ out.push({ value: id, label });
|
|
|
+ }
|
|
|
+ if (n.children?.length) walk(n.children);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ walk(nodes);
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+function parseCabinetIdsFromRow(row: any): number[] {
|
|
|
+ const scalarKeys = [
|
|
|
+ 'cabinetIds',
|
|
|
+ 'cabinet_id_list',
|
|
|
+ 'cabinetIdList',
|
|
|
+ 'materialsCabinetIds',
|
|
|
+ 'materials_cabinet_ids',
|
|
|
+ 'cabinetList',
|
|
|
+ 'planCabinetIds',
|
|
|
+ 'cabinetId',
|
|
|
+ 'cabinet_id',
|
|
|
+ ];
|
|
|
+ for (const key of scalarKeys) {
|
|
|
+ const raw = row?.[key];
|
|
|
+ if (raw === undefined || raw === null || raw === '') continue;
|
|
|
+ if (Array.isArray(raw)) {
|
|
|
+ const nums = raw
|
|
|
+ .map((x) =>
|
|
|
+ Number(
|
|
|
+ typeof x === 'object'
|
|
|
+ ? (x as any).cabinetId ?? (x as any).id ?? (x as any).materialsCabinetId
|
|
|
+ : x,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .filter((n) => Number.isFinite(n) && n > 0);
|
|
|
+ if (nums.length) return nums;
|
|
|
+ }
|
|
|
+ if (typeof raw === 'string' && raw.trim()) {
|
|
|
+ const parts = raw.split(/[,,]/).map((s) => s.trim()).filter(Boolean);
|
|
|
+ const nums = parts.map((s) => Number(s)).filter((n) => Number.isFinite(n) && n > 0);
|
|
|
+ if (nums.length) return nums;
|
|
|
+ }
|
|
|
+ if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) return [raw];
|
|
|
+ }
|
|
|
+ const relList =
|
|
|
+ row?.planCabinetList ??
|
|
|
+ row?.planCabinetVOList ??
|
|
|
+ row?.materialsPlanCabinetList ??
|
|
|
+ row?.checkPlanCabinetList ??
|
|
|
+ row?.materialsCheckPlanCabinetList;
|
|
|
+ if (Array.isArray(relList) && relList.length) {
|
|
|
+ return relList
|
|
|
+ .map((x: any) => Number(x?.cabinetId ?? x?.id ?? x?.materialsCabinetId ?? x?.materialsCabinetId))
|
|
|
+ .filter((n) => Number.isFinite(n) && n > 0);
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+}
|
|
|
+
|
|
|
+/** 合并下拉选项:保证已选物资柜 id 在 options 中有展示标签(与旧版详情回显一致) */
|
|
|
+function mergeCabinetEchoOptions(
|
|
|
+ base: { value: number; label: string }[],
|
|
|
+ ids: number[],
|
|
|
+ row: any,
|
|
|
+): { value: number; label: string }[] {
|
|
|
+ const byValue = new Map<number, string>();
|
|
|
+ for (const o of base) {
|
|
|
+ if (Number.isFinite(o.value) && o.value > 0) byValue.set(o.value, String(o.label || '').trim());
|
|
|
+ }
|
|
|
+ for (const id of ids) {
|
|
|
+ if (!Number.isFinite(id) || id <= 0) continue;
|
|
|
+ if (!byValue.has(id)) byValue.set(id, '');
|
|
|
+ }
|
|
|
+ const namesStr = firstDefined(row, ['cabinetName', 'cabinetNames', 'cabinet_names']);
|
|
|
+ const names =
|
|
|
+ typeof namesStr === 'string' && namesStr.trim()
|
|
|
+ ? namesStr.split(/[,,]/).map((s) => s.trim()).filter(Boolean)
|
|
|
+ : [];
|
|
|
+ if (names.length === ids.length && ids.length > 0) {
|
|
|
+ ids.forEach((id, i) => {
|
|
|
+ if (Number.isFinite(id) && id > 0 && names[i]) byValue.set(id, names[i]);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const relList =
|
|
|
+ row?.planCabinetList ??
|
|
|
+ row?.planCabinetVOList ??
|
|
|
+ row?.materialsPlanCabinetList ??
|
|
|
+ row?.checkPlanCabinetList ??
|
|
|
+ row?.materialsCheckPlanCabinetList;
|
|
|
+ if (Array.isArray(relList)) {
|
|
|
+ for (const it of relList) {
|
|
|
+ const id = Number((it as any)?.cabinetId ?? (it as any)?.id ?? (it as any)?.materialsCabinetId);
|
|
|
+ const name = String((it as any)?.cabinetName ?? (it as any)?.materialsCabinetName ?? '').trim();
|
|
|
+ if (Number.isFinite(id) && id > 0 && name) byValue.set(id, name);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (const id of ids) {
|
|
|
+ if (!Number.isFinite(id) || id <= 0) continue;
|
|
|
+ const lb = byValue.get(id);
|
|
|
+ if (!lb) byValue.set(id, `物资柜 #${id}`);
|
|
|
+ }
|
|
|
+ return Array.from(byValue.entries()).map(([value, label]) => ({ value, label: label || `物资柜 #${value}` }));
|
|
|
+}
|
|
|
+
|
|
|
+function unwrapPlanDetail(res: any): Record<string, any> | null {
|
|
|
+ if (res == null) return null;
|
|
|
+ const d = res.data !== undefined ? res.data : res;
|
|
|
+ if (d == null) return null;
|
|
|
+ if (d.data != null && typeof d.data === 'object' && !Array.isArray(d.data)) return d.data as Record<string, any>;
|
|
|
+ return d as Record<string, any>;
|
|
|
+}
|
|
|
+
|
|
|
+function buildPlanDateOptions(frequency: string | null | undefined): { value: string | number; label: string }[] {
|
|
|
+ if (frequency === '月') {
|
|
|
+ return Array.from({ length: 31 }, (_, i) => ({
|
|
|
+ value: String(i + 1).padStart(2, '0'),
|
|
|
+ label: String(i + 1).padStart(2, '0'),
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ if (frequency === '周') {
|
|
|
+ return [
|
|
|
+ { value: 1, label: '周一' },
|
|
|
+ { value: 2, label: '周二' },
|
|
|
+ { value: 3, label: '周三' },
|
|
|
+ { value: 4, label: '周四' },
|
|
|
+ { value: 5, label: '周五' },
|
|
|
+ { value: 6, label: '周六' },
|
|
|
+ { value: 7, label: '周日' },
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+}
|
|
|
+
|
|
|
+/** 旧版 CHECKING_STATUS 常见取值(无字典接口时兜底) */
|
|
|
+const PLAN_STATUS_FILTER_OPTIONS = [
|
|
|
+ { value: '0', label: '未开始' },
|
|
|
+ { value: '1', label: '进行中' },
|
|
|
+ { value: '2', label: '已完成' },
|
|
|
+ { value: '3', label: '已结束' },
|
|
|
+ { value: '4', label: '已取消' },
|
|
|
+];
|
|
|
+
|
|
|
+const planFormLayout = {
|
|
|
+ layout: 'horizontal' as const,
|
|
|
+ labelCol: { span: 5 },
|
|
|
+ wrapperCol: { span: 19 },
|
|
|
+};
|
|
|
+
|
|
|
+type PlanDraft = {
|
|
|
+ planName: string;
|
|
|
+ cabinetId?: number;
|
|
|
+ planDateRange: [Dayjs, Dayjs] | null;
|
|
|
+ checkUserName?: string;
|
|
|
+ status?: string;
|
|
|
+};
|
|
|
+
|
|
|
+type AutoConfigState = {
|
|
|
+ startStatus: boolean;
|
|
|
+ planFrequency: string | null;
|
|
|
+ planDate: string | number | null;
|
|
|
+ checkUserId: number | null;
|
|
|
+ templateCode: string;
|
|
|
+};
|
|
|
+
|
|
|
+function emptyPlanDraft(fixedCabinetId?: number): PlanDraft {
|
|
|
+ return {
|
|
|
+ planName: '',
|
|
|
+ planDateRange: null,
|
|
|
+ checkUserName: undefined,
|
|
|
+ status: undefined,
|
|
|
+ ...(fixedCabinetId != null && Number.isFinite(Number(fixedCabinetId))
|
|
|
+ ? { cabinetId: Number(fixedCabinetId) }
|
|
|
+ : {}),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+export interface MaterialInspectionPlanPageProps {
|
|
|
+ fixedCabinetId?: number;
|
|
|
+ embedded?: boolean;
|
|
|
+ embeddedTabBar?: React.ReactNode;
|
|
|
+}
|
|
|
+
|
|
|
+export default function MaterialInspectionPlanPage({
|
|
|
+ fixedCabinetId,
|
|
|
+ embedded,
|
|
|
+ embeddedTabBar,
|
|
|
+}: MaterialInspectionPlanPageProps = {}) {
|
|
|
+ const { t } = useTranslation();
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [list, setList] = useState<any[]>([]);
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
+ const [pageNo, setPageNo] = useState(1);
|
|
|
+ const [pageSize] = useState(10);
|
|
|
+ const [draft, setDraft] = useState<PlanDraft>(() => emptyPlanDraft(fixedCabinetId));
|
|
|
+ const [applied, setApplied] = useState<PlanDraft>(() => emptyPlanDraft(fixedCabinetId));
|
|
|
+
|
|
|
+ const [modalOpen, setModalOpen] = useState(false);
|
|
|
+ const [form] = Form.useForm();
|
|
|
+ const [editing, setEditing] = useState<any | null>(null);
|
|
|
+ const [workstationOptions, setWorkstationOptions] = useState<{ label: string; value: number }[]>([]);
|
|
|
+ const [cabinetOptions, setCabinetOptions] = useState<{ label: string; value: number }[]>([]);
|
|
|
+ const [cabinetFilterOptions, setCabinetFilterOptions] = useState<{ label: string; value: number }[]>([]);
|
|
|
+ const [userOptions, setUserOptions] = useState<{ label: string; value: number }[]>([]);
|
|
|
+ const [inspectorFilterOptions, setInspectorFilterOptions] = useState<{ label: string; value: string }[]>([]);
|
|
|
+ const [cabinetsLoading, setCabinetsLoading] = useState(false);
|
|
|
+
|
|
|
+ const [autoConfig, setAutoConfig] = useState<AutoConfigState>({
|
|
|
+ startStatus: false,
|
|
|
+ planFrequency: null,
|
|
|
+ planDate: null,
|
|
|
+ checkUserId: null,
|
|
|
+ templateCode: PLAN_AUTO_TEMPLATE_CODE,
|
|
|
+ });
|
|
|
+ const [planDateOptions, setPlanDateOptions] = useState<{ value: string | number; label: string }[]>([]);
|
|
|
+
|
|
|
+ const hidePlanNameFilter = fixedCabinetId != null && Number.isFinite(Number(fixedCabinetId));
|
|
|
+ const cabinetSelectDisabled = hidePlanNameFilter;
|
|
|
+
|
|
|
+ const filterLabelCls =
|
|
|
+ 'shrink-0 text-right text-sm font-medium text-gray-600 whitespace-nowrap w-[100px] sm:w-[108px]';
|
|
|
+
|
|
|
+ const persistAutomaticConfig = async (next: AutoConfigState) => {
|
|
|
+ await materialPlanApi.updateAutomaticConfig({
|
|
|
+ planFrequency: next.planFrequency ?? undefined,
|
|
|
+ planDate: next.planDate ?? undefined,
|
|
|
+ checkUserId: next.checkUserId ?? undefined,
|
|
|
+ startStatus: next.startStatus ? 1 : 0,
|
|
|
+ templateCode: next.templateCode,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 分页拉物资柜(弹窗内按区域筛选;可在弹窗打开前调用以完成回显) */
|
|
|
+ const fetchCabinetSelectOptions = useCallback(async (workstationId?: number | null) => {
|
|
|
+ setCabinetsLoading(true);
|
|
|
+ try {
|
|
|
+ const wid =
|
|
|
+ workstationId != null && Number.isFinite(Number(workstationId)) && Number(workstationId) > 0
|
|
|
+ ? Number(workstationId)
|
|
|
+ : undefined;
|
|
|
+ const all: any[] = [];
|
|
|
+ let p = 1;
|
|
|
+ const ps = MATERIALS_TYPE_PAGE_SIZE_MAX;
|
|
|
+ while (p <= 50) {
|
|
|
+ const res = await materialLockerApi.listMaterialsCabinet({
|
|
|
+ ...buildPageParams(p, ps),
|
|
|
+ ...(wid != null ? { workstationId: wid } : {}),
|
|
|
+ });
|
|
|
+ const batch = pickRecords(res);
|
|
|
+ all.push(...batch);
|
|
|
+ if (batch.length < ps) break;
|
|
|
+ const tot = pickTotal(res);
|
|
|
+ if (tot > 0 && all.length >= tot) break;
|
|
|
+ p += 1;
|
|
|
+ }
|
|
|
+ const opts = all.map((r: any) => ({
|
|
|
+ value: Number(r.cabinetId ?? r.id),
|
|
|
+ label: String(r.cabinetName ?? r.cabinetCode ?? r.cabinetId ?? r.id ?? ''),
|
|
|
+ }));
|
|
|
+ setCabinetOptions(opts);
|
|
|
+ return opts;
|
|
|
+ } catch (e: any) {
|
|
|
+ toast.error(e?.message || '加载物资柜失败');
|
|
|
+ setCabinetOptions([]);
|
|
|
+ return [];
|
|
|
+ } finally {
|
|
|
+ setCabinetsLoading(false);
|
|
|
+ }
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ (async () => {
|
|
|
+ try {
|
|
|
+ const res = await materialLockerApi.listMaterialsCabinet(MATERIALS_CABINET_LIST_PARAMS);
|
|
|
+ setCabinetFilterOptions(mapCabinetListToFilterOptions(pickRecords(res)));
|
|
|
+ } catch {
|
|
|
+ try {
|
|
|
+ const res2 = await materialLockerApi.listMaterialsCabinet(buildPageParams(1, 500));
|
|
|
+ setCabinetFilterOptions(mapCabinetListToFilterOptions(pickRecords(res2)));
|
|
|
+ } catch {
|
|
|
+ setCabinetFilterOptions([]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ (async () => {
|
|
|
+ try {
|
|
|
+ const users = await userApi.getRoleUser('check');
|
|
|
+ const arr = Array.isArray(users) ? users : [];
|
|
|
+ setUserOptions(
|
|
|
+ arr.map((u: any) => ({
|
|
|
+ value: Number(u.id),
|
|
|
+ label: String(u.nickname || u.username || u.id || ''),
|
|
|
+ })),
|
|
|
+ );
|
|
|
+ setInspectorFilterOptions(
|
|
|
+ arr.map((u: any) => {
|
|
|
+ const nick = String(u.nickname || u.username || '').trim();
|
|
|
+ return { value: nick, label: nick || String(u.id) };
|
|
|
+ }).filter((o) => o.value),
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ setUserOptions([]);
|
|
|
+ setInspectorFilterOptions([]);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const loadAutoConfigFromServer = useCallback(async () => {
|
|
|
+ try {
|
|
|
+ const data: any = await materialPlanApi.selectIsMailNotifyConfigByCode({
|
|
|
+ templateCode: PLAN_AUTO_FETCH_PARAM,
|
|
|
+ configCode: PLAN_AUTO_FETCH_PARAM,
|
|
|
+ });
|
|
|
+ const frequency = data?.planFrequency ?? null;
|
|
|
+ const opts = buildPlanDateOptions(frequency);
|
|
|
+ setPlanDateOptions(opts);
|
|
|
+ setAutoConfig({
|
|
|
+ startStatus: data?.startStatus === 1 || data?.startStatus === '1',
|
|
|
+ planFrequency: frequency,
|
|
|
+ planDate: data?.planDate ?? null,
|
|
|
+ checkUserId:
|
|
|
+ data?.checkUserId != null && data?.checkUserId !== ''
|
|
|
+ ? Number(data.checkUserId)
|
|
|
+ : data?.checkUserId === 0
|
|
|
+ ? 0
|
|
|
+ : null,
|
|
|
+ templateCode: PLAN_AUTO_TEMPLATE_CODE,
|
|
|
+ });
|
|
|
+ } catch {
|
|
|
+ setPlanDateOptions([]);
|
|
|
+ }
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ void loadAutoConfigFromServer();
|
|
|
+ }, [loadAutoConfigFromServer]);
|
|
|
+
|
|
|
+ const buildListParams = useCallback(() => {
|
|
|
+ const q: Record<string, any> = {
|
|
|
+ ...buildPageParams(pageNo, pageSize),
|
|
|
+ planName: applied.planName?.trim() || undefined,
|
|
|
+ cabinetId:
|
|
|
+ fixedCabinetId != null && Number.isFinite(Number(fixedCabinetId))
|
|
|
+ ? Number(fixedCabinetId)
|
|
|
+ : applied.cabinetId != null && Number.isFinite(Number(applied.cabinetId)) && Number(applied.cabinetId) > 0
|
|
|
+ ? Number(applied.cabinetId)
|
|
|
+ : undefined,
|
|
|
+ checkUserName: applied.checkUserName?.trim() || undefined,
|
|
|
+ status: applied.status != null && applied.status !== '' ? applied.status : undefined,
|
|
|
+ };
|
|
|
+ if (applied.planDateRange?.[0] && applied.planDateRange[1]) {
|
|
|
+ q.startTime = applied.planDateRange[0].format('YYYY-MM-DD HH:mm:ss');
|
|
|
+ q.endTime = applied.planDateRange[1].format('YYYY-MM-DD HH:mm:ss');
|
|
|
+ }
|
|
|
+ return q;
|
|
|
+ }, [pageNo, pageSize, applied, fixedCabinetId]);
|
|
|
+
|
|
|
+ const fetchList = useCallback(async () => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const res = await materialPlanApi.listPlan(buildListParams());
|
|
|
+ setList(pickRecords(res));
|
|
|
+ setTotal(pickTotal(res));
|
|
|
+ } catch (e: any) {
|
|
|
+ toast.error(e?.message || t('common.operationFailed'));
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ }, [buildListParams, t]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ void fetchList();
|
|
|
+ }, [fetchList]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (fixedCabinetId != null && Number.isFinite(Number(fixedCabinetId))) {
|
|
|
+ const id = Number(fixedCabinetId);
|
|
|
+ setDraft((d) => ({ ...d, cabinetId: id }));
|
|
|
+ setApplied((a) => ({ ...a, cabinetId: id }));
|
|
|
+ }
|
|
|
+ }, [fixedCabinetId]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!modalOpen) return;
|
|
|
+ let cancelled = false;
|
|
|
+ (async () => {
|
|
|
+ try {
|
|
|
+ const flat = await fetchAllWorkstations(workstationApi.listMarsDept);
|
|
|
+ const normalized = flat.map((w: any) => {
|
|
|
+ const id = Number(w.workstationId ?? w.id);
|
|
|
+ return {
|
|
|
+ ...w,
|
|
|
+ id: Number.isFinite(id) ? id : 0,
|
|
|
+ parentId: w.parentId != null && w.parentId !== '' ? Number(w.parentId) : 0,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ const tree = handleTree(normalized, 'id', 'parentId', 'children');
|
|
|
+ if (!cancelled) setWorkstationOptions(flattenWorkstations(tree));
|
|
|
+ } catch {
|
|
|
+ if (!cancelled) setWorkstationOptions([]);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ return () => {
|
|
|
+ cancelled = true;
|
|
|
+ };
|
|
|
+ }, [modalOpen]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!modalOpen) return;
|
|
|
+ const timer = window.setTimeout(() => {
|
|
|
+ const wid = form.getFieldValue('workstationId');
|
|
|
+ const n = wid != null && wid !== '' ? Number(wid) : NaN;
|
|
|
+ void fetchCabinetSelectOptions(Number.isFinite(n) && n > 0 ? n : null);
|
|
|
+ }, 0);
|
|
|
+ return () => clearTimeout(timer);
|
|
|
+ }, [modalOpen, fetchCabinetSelectOptions, form]);
|
|
|
+
|
|
|
+ const setDraftField = <K extends keyof PlanDraft>(key: K, value: PlanDraft[K]) => {
|
|
|
+ setDraft((d) => ({ ...d, [key]: value }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleQuery = () => {
|
|
|
+ setApplied({ ...draft });
|
|
|
+ setPageNo(1);
|
|
|
+ };
|
|
|
+
|
|
|
+ const resetQuery = () => {
|
|
|
+ const e = emptyPlanDraft(fixedCabinetId);
|
|
|
+ setDraft(e);
|
|
|
+ setApplied(e);
|
|
|
+ setPageNo(1);
|
|
|
+ };
|
|
|
+
|
|
|
+ const onAutoStartChange = (checked: boolean) => {
|
|
|
+ setAutoConfig((prev) => {
|
|
|
+ const previous = prev.startStatus;
|
|
|
+ const next = { ...prev, startStatus: checked };
|
|
|
+ void (async () => {
|
|
|
+ try {
|
|
|
+ await persistAutomaticConfig(next);
|
|
|
+ toast.success('更新成功');
|
|
|
+ } catch (e: any) {
|
|
|
+ setAutoConfig((c) => ({ ...c, startStatus: previous }));
|
|
|
+ toast.error(e?.message || t('common.operationFailed'));
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ return next;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const onAutoFieldChange = (patch: Partial<AutoConfigState>) => {
|
|
|
+ setAutoConfig((prev) => {
|
|
|
+ const next = { ...prev, ...patch };
|
|
|
+ void persistAutomaticConfig(next).catch((e: any) => {
|
|
|
+ toast.error(e?.message || t('common.operationFailed'));
|
|
|
+ });
|
|
|
+ return next;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const onFrequencySelectChange = (v: string | null) => {
|
|
|
+ const freq = v ?? null;
|
|
|
+ setPlanDateOptions(buildPlanDateOptions(freq));
|
|
|
+ setAutoConfig((prev) => {
|
|
|
+ const next = { ...prev, planFrequency: freq, planDate: null };
|
|
|
+ void persistAutomaticConfig(next).catch((e: any) => {
|
|
|
+ toast.error(e?.message || t('common.operationFailed'));
|
|
|
+ });
|
|
|
+ return next;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const openForm = async (row?: any) => {
|
|
|
+ form.resetFields();
|
|
|
+ setEditing(null);
|
|
|
+
|
|
|
+ if (!row) {
|
|
|
+ setModalOpen(true);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const planIdRaw = firstDefined(row, ['planId', 'plan_id', 'id', 'materialsCheckPlanId']);
|
|
|
+ let dataRow: any = { ...row };
|
|
|
+
|
|
|
+ if (planIdRaw != null && planIdRaw !== '') {
|
|
|
+ try {
|
|
|
+ const res = await materialPlanApi.getPlanInfo(Number(planIdRaw));
|
|
|
+ const detail = unwrapPlanDetail(res);
|
|
|
+ if (detail && typeof detail === 'object') {
|
|
|
+ dataRow = { ...row, ...detail };
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ /* 列表行继续用 row */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let ids = parseCabinetIdsFromRow(dataRow);
|
|
|
+ if (ids.length === 0 && planIdRaw != null && planIdRaw !== '') {
|
|
|
+ try {
|
|
|
+ const cr = await materialPlanApi.getCheckPlanCabinetList({
|
|
|
+ ...buildPageParams(1, -1),
|
|
|
+ planId: Number(planIdRaw),
|
|
|
+ materialsCheckPlanId: Number(planIdRaw),
|
|
|
+ });
|
|
|
+ const rows = pickRecords(cr);
|
|
|
+ if (rows.length) {
|
|
|
+ dataRow = { ...dataRow, planCabinetList: rows };
|
|
|
+ ids = parseCabinetIdsFromRow(dataRow);
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const workstationRaw = firstDefined(dataRow, ['workstationId', 'workstation_id', 'deptId', 'areaId', 'dept_id']);
|
|
|
+ const widNum =
|
|
|
+ workstationRaw != null && workstationRaw !== '' ? Number(workstationRaw) : NaN;
|
|
|
+ const widOpt = Number.isFinite(widNum) && widNum > 0 ? widNum : null;
|
|
|
+
|
|
|
+ const baseOpts = await fetchCabinetSelectOptions(widOpt);
|
|
|
+ setCabinetOptions(mergeCabinetEchoOptions(baseOpts, ids, dataRow));
|
|
|
+
|
|
|
+ const planDateRaw = firstDefined(dataRow, [
|
|
|
+ 'planDate',
|
|
|
+ 'plan_date',
|
|
|
+ 'checkPlanDate',
|
|
|
+ 'scheduleDate',
|
|
|
+ 'startTime',
|
|
|
+ 'start_time',
|
|
|
+ ]);
|
|
|
+ const planDateVal = planDateRaw ? dayjs(planDateRaw as string | number) : undefined;
|
|
|
+ const inspectorRaw = firstDefined(dataRow, [
|
|
|
+ 'checkUserId',
|
|
|
+ 'check_user_id',
|
|
|
+ 'inspectorUserId',
|
|
|
+ 'inspector_user_id',
|
|
|
+ 'checkerId',
|
|
|
+ 'checker_id',
|
|
|
+ 'userId',
|
|
|
+ 'user_id',
|
|
|
+ 'planCheckerId',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ setEditing(dataRow);
|
|
|
+ form.setFieldsValue({
|
|
|
+ planName: firstDefined(dataRow, ['planName', 'plan_name', 'materialsCheckPlanName', 'checkPlanName', 'name']),
|
|
|
+ workstationId: widOpt ?? undefined,
|
|
|
+ cabinetIds: ids,
|
|
|
+ planDate: planDateVal?.isValid() ? planDateVal : undefined,
|
|
|
+ inspectorUserId: (() => {
|
|
|
+ if (inspectorRaw == null || inspectorRaw === '') return undefined;
|
|
|
+ const n = Number(inspectorRaw);
|
|
|
+ return Number.isFinite(n) ? n : undefined;
|
|
|
+ })(),
|
|
|
+ });
|
|
|
+ setModalOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ const submit = async () => {
|
|
|
+ const v = await form.validateFields();
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const d = v.planDate as Dayjs | undefined;
|
|
|
+ const dateStr =
|
|
|
+ d && dayjs.isDayjs(d) && d.isValid() ? d.format('YYYY-MM-DD') : dayjs(v.planDate).format('YYYY-MM-DD');
|
|
|
+ const cabinetIdsArr: number[] = Array.isArray(v.cabinetIds)
|
|
|
+ ? v.cabinetIds.map((x: unknown) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
|
|
+ : [];
|
|
|
+ const cabinetIdsStr = cabinetIdsArr.join(',');
|
|
|
+
|
|
|
+ const commonPayload: Record<string, any> = {
|
|
|
+ planName: v.planName,
|
|
|
+ workstationId: v.workstationId,
|
|
|
+ workstation_id: v.workstationId,
|
|
|
+ cabinetIds: cabinetIdsStr,
|
|
|
+ cabinetIdList: cabinetIdsArr,
|
|
|
+ cabinet_ids: cabinetIdsStr,
|
|
|
+ planDate: dateStr,
|
|
|
+ startTime: `${dateStr}T00:00:00`,
|
|
|
+ endTime: `${dateStr}T23:59:59`,
|
|
|
+ inspectorUserId: v.inspectorUserId,
|
|
|
+ checkUserId: v.inspectorUserId,
|
|
|
+ checkerId: v.inspectorUserId,
|
|
|
+ planCheckerId: v.inspectorUserId,
|
|
|
+ planStatus: editing?.planStatus ?? 0,
|
|
|
+ checkCycle: editing?.checkCycle ?? 1,
|
|
|
+ checkCycleUnit: editing?.checkCycleUnit || 'day',
|
|
|
+ remark: editing?.remark,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (editing) {
|
|
|
+ await materialPlanApi.updatePlan({
|
|
|
+ ...editing,
|
|
|
+ ...commonPayload,
|
|
|
+ planId: firstDefined(editing, ['planId', 'plan_id', 'id', 'materialsCheckPlanId']),
|
|
|
+ } as PlanVO);
|
|
|
+ toast.success(t('common.updateSuccess'));
|
|
|
+ } else {
|
|
|
+ await materialPlanApi.addPlan(commonPayload as PlanVO);
|
|
|
+ toast.success(t('common.addSuccess'));
|
|
|
+ }
|
|
|
+ setModalOpen(false);
|
|
|
+ void fetchList();
|
|
|
+ } catch (e: any) {
|
|
|
+ if (!e?.errorFields) toast.error(e?.message || t('common.operationFailed'));
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const onDelete = (row: any) => {
|
|
|
+ const id = firstDefined(row, ['planId', 'plan_id', 'id', 'materialsCheckPlanId']);
|
|
|
+ materialConfirmDelete(t, {
|
|
|
+ count: 1,
|
|
|
+ onOk: async () => {
|
|
|
+ await materialPlanApi.deletePlan(Number(id));
|
|
|
+ toast.success(t('common.deleteSuccess'));
|
|
|
+ void fetchList();
|
|
|
+ },
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const isEditDisabled = useCallback((row: any) => {
|
|
|
+ const st = firstDefined(row, ['status', 'planStatus', 'plan_status']);
|
|
|
+ return st === '1' || st === 1;
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const columns: ColumnsType<any> = useMemo(
|
|
|
+ () => [
|
|
|
+ {
|
|
|
+ title: '计划编号',
|
|
|
+ key: 'planId',
|
|
|
+ width: 88,
|
|
|
+ align: 'center',
|
|
|
+ render: (_, row) => String(firstDefined(row, ['planId', 'plan_id', 'id', 'materialsCheckPlanId']) ?? '—'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '计划名称',
|
|
|
+ key: 'planName',
|
|
|
+ width: 200,
|
|
|
+ align: 'center',
|
|
|
+ ellipsis: true,
|
|
|
+ render: (_, row) =>
|
|
|
+ String(
|
|
|
+ firstDefined(row, ['planName', 'plan_name', 'materialsCheckPlanName', 'checkPlanName', 'name']) ?? '—',
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '物资柜',
|
|
|
+ key: 'cabinets',
|
|
|
+ width: 220,
|
|
|
+ align: 'center',
|
|
|
+ ellipsis: true,
|
|
|
+ render: (_, row) => renderCabinetNames(_, row),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '计划日期',
|
|
|
+ key: 'planDate',
|
|
|
+ width: 118,
|
|
|
+ align: 'center',
|
|
|
+ render: (_, row) =>
|
|
|
+ formatPlanDate(
|
|
|
+ firstDefined(row, [
|
|
|
+ 'planDate',
|
|
|
+ 'plan_date',
|
|
|
+ 'checkPlanDate',
|
|
|
+ 'scheduleDate',
|
|
|
+ 'planDay',
|
|
|
+ 'startTime',
|
|
|
+ 'start_time',
|
|
|
+ ]),
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '检察员',
|
|
|
+ key: 'inspector',
|
|
|
+ width: 112,
|
|
|
+ align: 'center',
|
|
|
+ render: (_, row) =>
|
|
|
+ String(
|
|
|
+ firstDefined(row, [
|
|
|
+ 'checkUserName',
|
|
|
+ 'check_user_name',
|
|
|
+ 'inspectorName',
|
|
|
+ 'inspector_name',
|
|
|
+ 'checkerName',
|
|
|
+ 'checker_name',
|
|
|
+ 'planChecker',
|
|
|
+ 'plan_checker',
|
|
|
+ 'nickName',
|
|
|
+ 'nick_name',
|
|
|
+ 'userName',
|
|
|
+ 'username',
|
|
|
+ ]) ?? '—',
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ key: 'planStatus',
|
|
|
+ width: 104,
|
|
|
+ align: 'center',
|
|
|
+ render: (_, row) => renderPlanStatus(_, row),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '检查记录',
|
|
|
+ key: 'records',
|
|
|
+ width: 100,
|
|
|
+ align: 'center',
|
|
|
+ className: '!align-middle',
|
|
|
+ render: (_, row) => <MaterialViewButton label="查看" onClick={() => goInspectionRecordsForPlan(row)} />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: t('table.operation'),
|
|
|
+ width: 140,
|
|
|
+ align: 'center',
|
|
|
+ fixed: 'right',
|
|
|
+ render: (_, row) => (
|
|
|
+ <div className="flex items-center gap-2 justify-center">
|
|
|
+ {isEditDisabled(row) ? (
|
|
|
+ <Button type="link" disabled className="!text-gray-400">
|
|
|
+ 修改
|
|
|
+ </Button>
|
|
|
+ ) : (
|
|
|
+ <MaterialEditButton label="修改" onClick={() => void openForm(row)} />
|
|
|
+ )}
|
|
|
+ <MaterialDeleteButton label={t('common.delete')} onClick={() => onDelete(row)} />
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ [t, isEditDisabled],
|
|
|
+ );
|
|
|
+
|
|
|
+ const filterCard = (
|
|
|
+ <div className="overflow-hidden rounded-xl border border-gray-200/80 bg-white shadow-sm ring-1 ring-gray-100/80">
|
|
|
+ {embeddedTabBar ? (
|
|
|
+ <div className="border-b border-gray-100/80 px-2 pb-3 pt-4 sm:px-3 sm:pb-4 sm:pt-5">{embeddedTabBar}</div>
|
|
|
+ ) : null}
|
|
|
+ <div className="box-border w-full min-w-0 px-5" style={{ paddingTop: 28, paddingBottom: 20 }}>
|
|
|
+ <div className="w-full min-w-0 space-y-4">
|
|
|
+ <Row gutter={[16, 16]}>
|
|
|
+ {!hidePlanNameFilter ? (
|
|
|
+ <Col xs={24} sm={12} lg={8}>
|
|
|
+ <div className="flex min-w-0 items-center gap-2.5">
|
|
|
+ <span className={filterLabelCls}>计划名称</span>
|
|
|
+ <Input
|
|
|
+ allowClear
|
|
|
+ placeholder="请输入计划名称"
|
|
|
+ className="min-w-0 flex-1"
|
|
|
+ value={draft.planName}
|
|
|
+ onChange={(e) => setDraftField('planName', e.target.value)}
|
|
|
+ onPressEnter={handleQuery}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ ) : null}
|
|
|
+ <Col xs={24} sm={12} lg={hidePlanNameFilter ? 8 : 8}>
|
|
|
+ <div className="flex min-w-0 items-center gap-2.5">
|
|
|
+ <span className={filterLabelCls}>物资柜</span>
|
|
|
+ <Select
|
|
|
+ allowClear={!cabinetSelectDisabled}
|
|
|
+ disabled={cabinetSelectDisabled}
|
|
|
+ placeholder="请选择检查的物资柜"
|
|
|
+ className="min-w-0 flex-1"
|
|
|
+ options={cabinetFilterOptions}
|
|
|
+ value={draft.cabinetId}
|
|
|
+ onChange={(v) => setDraftField('cabinetId', v ?? undefined)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12} lg={8}>
|
|
|
+ <div className="flex min-w-0 items-center gap-2.5">
|
|
|
+ <span className={filterLabelCls}>计划日期</span>
|
|
|
+ <DatePicker.RangePicker
|
|
|
+ showTime
|
|
|
+ className="min-w-0 flex-1 [&_.ant-picker-input]:min-w-0"
|
|
|
+ value={draft.planDateRange}
|
|
|
+ onChange={(vals) => {
|
|
|
+ const a = vals?.[0];
|
|
|
+ const b = vals?.[1];
|
|
|
+ setDraftField('planDateRange', a && b ? [a, b] : null);
|
|
|
+ }}
|
|
|
+ placeholder={['开始日期', '结束日期']}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ <Row gutter={[16, 16]} align="middle">
|
|
|
+ <Col xs={24} sm={12} lg={6}>
|
|
|
+ <div className="flex min-w-0 items-center gap-2.5">
|
|
|
+ <span className={filterLabelCls}>检察员</span>
|
|
|
+ <Select
|
|
|
+ allowClear
|
|
|
+ showSearch
|
|
|
+ optionFilterProp="label"
|
|
|
+ placeholder="请选择检察员"
|
|
|
+ className="min-w-0 flex-1"
|
|
|
+ options={inspectorFilterOptions}
|
|
|
+ value={draft.checkUserName}
|
|
|
+ onChange={(v) => setDraftField('checkUserName', v ?? undefined)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12} lg={6}>
|
|
|
+ <div className="flex min-w-0 items-center gap-2.5">
|
|
|
+ <span className={filterLabelCls}>状态</span>
|
|
|
+ <Select
|
|
|
+ allowClear
|
|
|
+ placeholder="请选择状态"
|
|
|
+ className="min-w-0 flex-1"
|
|
|
+ options={PLAN_STATUS_FILTER_OPTIONS}
|
|
|
+ value={draft.status}
|
|
|
+ onChange={(v) => setDraftField('status', v ?? undefined)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={24} lg={12}>
|
|
|
+ <Space size="small" wrap>
|
|
|
+ <Button type="primary" icon={<Search className="h-4 w-4" />} onClick={handleQuery}>
|
|
|
+ {t('common.search')}
|
|
|
+ </Button>
|
|
|
+ <Button icon={<RefreshCw className="h-4 w-4" />} onClick={resetQuery}>
|
|
|
+ {t('common.reset')}
|
|
|
+ </Button>
|
|
|
+ <Button type="primary" ghost icon={<Plus className="h-4 w-4" />} onClick={() => void openForm()}>
|
|
|
+ {t('common.addNew')}
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+
|
|
|
+ const autoConfigCard = (
|
|
|
+ <div className="overflow-hidden rounded-xl border border-gray-200/80 bg-white shadow-sm ring-1 ring-gray-100/80">
|
|
|
+ <div
|
|
|
+ className="box-border flex min-h-[100px] w-full min-w-0 items-center px-5 py-8 sm:px-6 sm:py-10"
|
|
|
+ >
|
|
|
+ <Row gutter={[16, 16]} align="middle" className="w-full">
|
|
|
+ <Col xs={24} lg={4}>
|
|
|
+ <div className="flex flex-wrap items-center gap-3">
|
|
|
+ <Checkbox
|
|
|
+ checked={autoConfig.startStatus}
|
|
|
+ onChange={(e) => onAutoStartChange(e.target.checked)}
|
|
|
+ className="scale-125"
|
|
|
+ />
|
|
|
+ <span className="text-sm text-gray-800">启动自动创建</span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12} lg={5}>
|
|
|
+ <div className="flex min-w-0 items-center gap-2.5">
|
|
|
+ <span className={filterLabelCls}>计划频率</span>
|
|
|
+ <Select
|
|
|
+ allowClear
|
|
|
+ disabled={!autoConfig.startStatus}
|
|
|
+ placeholder="请选择计划频率"
|
|
|
+ className="min-w-0 flex-1"
|
|
|
+ value={autoConfig.planFrequency ?? undefined}
|
|
|
+ onChange={(v) => onFrequencySelectChange(v ?? null)}
|
|
|
+ options={[
|
|
|
+ { value: '月', label: '月' },
|
|
|
+ { value: '周', label: '周' },
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12} lg={5}>
|
|
|
+ <div className="flex min-w-0 items-center gap-2.5">
|
|
|
+ <span className={filterLabelCls}>计划日期</span>
|
|
|
+ <Select
|
|
|
+ allowClear
|
|
|
+ disabled={!autoConfig.startStatus}
|
|
|
+ placeholder="请选择计划日期"
|
|
|
+ className="min-w-0 flex-1"
|
|
|
+ value={autoConfig.planDate ?? undefined}
|
|
|
+ options={planDateOptions}
|
|
|
+ onChange={(v) => void onAutoFieldChange({ planDate: v ?? null })}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12} lg={5}>
|
|
|
+ <div className="flex min-w-0 items-center gap-2.5">
|
|
|
+ <span className={filterLabelCls}>检察员</span>
|
|
|
+ <Select
|
|
|
+ allowClear
|
|
|
+ showSearch
|
|
|
+ optionFilterProp="label"
|
|
|
+ disabled={!autoConfig.startStatus}
|
|
|
+ placeholder="请选择检察员"
|
|
|
+ className="min-w-0 flex-1"
|
|
|
+ options={userOptions}
|
|
|
+ value={autoConfig.checkUserId ?? undefined}
|
|
|
+ onChange={(v) => void onAutoFieldChange({ checkUserId: v != null ? Number(v) : null })}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} lg={5}>
|
|
|
+ <span className="text-sm text-gray-500">(*自动创建的检查计划,覆盖所有物资柜)</span>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+
|
|
|
+ const tableInner = (
|
|
|
+ <>
|
|
|
+ <MaterialTableCard flush={Boolean(embedded)}>
|
|
|
+ <Table
|
|
|
+ rowKey={(r, i) => String(firstDefined(r, ['planId', 'plan_id', 'id', 'materialsCheckPlanId']) ?? i)}
|
|
|
+ loading={loading}
|
|
|
+ columns={columns}
|
|
|
+ dataSource={list}
|
|
|
+ pagination={false}
|
|
|
+ tableLayout="fixed"
|
|
|
+ scroll={{ x: 1100 }}
|
|
|
+ rowClassName={(_, index) => (index !== undefined && index % 2 === 1 ? 'isolation-work-list-row-alt' : '')}
|
|
|
+ />
|
|
|
+ </MaterialTableCard>
|
|
|
+ <MaterialPaginationBar
|
|
|
+ flushTop={Boolean(embedded)}
|
|
|
+ total={total}
|
|
|
+ current={pageNo}
|
|
|
+ pageSize={pageSize}
|
|
|
+ onPageChange={setPageNo}
|
|
|
+ loading={loading}
|
|
|
+ listLength={list.length}
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ );
|
|
|
+
|
|
|
+ const listSection = embedded ? (
|
|
|
+ <div className="rounded-xl border border-gray-200/80 bg-white shadow-sm ring-1 ring-gray-100/80">{tableInner}</div>
|
|
|
+ ) : (
|
|
|
+ tableInner
|
|
|
+ );
|
|
|
+
|
|
|
+ const pageBody = (
|
|
|
+ <>
|
|
|
+ {filterCard}
|
|
|
+ {autoConfigCard}
|
|
|
+ {listSection}
|
|
|
+
|
|
|
+ <Modal
|
|
|
+ title={editing ? '编辑检查计划' : '新增'}
|
|
|
+ open={modalOpen}
|
|
|
+ onOk={submit}
|
|
|
+ onCancel={() => setModalOpen(false)}
|
|
|
+ confirmLoading={loading}
|
|
|
+ {...getMaterialFormModalProps(t, 620)}
|
|
|
+ destroyOnClose={false}
|
|
|
+ >
|
|
|
+ <Form form={form} {...planFormLayout} preserve={false}>
|
|
|
+ <Form.Item name="planName" label="计划名称" rules={[{ required: true, message: '请输入计划名称' }]}>
|
|
|
+ <Input allowClear placeholder="请输入计划名称" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item name="workstationId" label="所属区域">
|
|
|
+ <Select
|
|
|
+ allowClear
|
|
|
+ showSearch
|
|
|
+ optionFilterProp="label"
|
|
|
+ placeholder="请选择所属区域"
|
|
|
+ options={workstationOptions}
|
|
|
+ onChange={(v) => {
|
|
|
+ form.setFieldsValue({ cabinetIds: undefined });
|
|
|
+ const n = v != null && v !== '' ? Number(v) : NaN;
|
|
|
+ void fetchCabinetSelectOptions(Number.isFinite(n) && n > 0 ? n : null);
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ name="cabinetIds"
|
|
|
+ label="物资柜"
|
|
|
+ rules={[{ required: true, message: '请选择需要检查的物资柜' }]}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ mode="multiple"
|
|
|
+ allowClear
|
|
|
+ showSearch
|
|
|
+ optionFilterProp="label"
|
|
|
+ placeholder="请选择需要检查的物资柜"
|
|
|
+ options={cabinetOptions}
|
|
|
+ loading={cabinetsLoading}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item name="planDate" label="计划日期" rules={[{ required: true, message: '请选择日期' }]}>
|
|
|
+ <DatePicker placeholder="请选择日期" style={{ width: 280 }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item name="inspectorUserId" label="检察员" rules={[{ required: true, message: '请选择检察员' }]}>
|
|
|
+ <Select showSearch optionFilterProp="label" placeholder="请选择检察员" options={userOptions} />
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+
|
|
|
+ return embedded ? <div className="space-y-4">{pageBody}</div> : <MaterialPageRoot>{pageBody}</MaterialPageRoot>;
|
|
|
+}
|