index.tsx 4.96 KB
// REQ-USR-001 / REQ-USR-002: 用户单据页面容器(判 mode、装配 4 子组件、提交反馈与导航回流)
import { useEffect } from 'react';
import { Form, Spin, Button, Modal, App as AntdApp } from 'antd';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import type { UserVO } from '../../../api/types';
import UserDetailToolbar from './UserDetailToolbar';
import UserBasicForm from './UserBasicForm';
import PermissionTabs from './PermissionTabs';
import PermissionGroupList from './PermissionGroupList';
import { useUserDetail } from './useUserDetail';
import {
  MODE_CREATE,
  MODE_EDIT,
  MSG_CREATE_SUCCESS,
  MSG_EDIT_SUCCESS,
  MSG_ERR_USER_NOT_FOUND,
  MSG_LOAD_DETAIL_FAIL,
  MSG_CANCEL_CONFIRM,
  PATH_USER_LIST,
  PATH_USER_NEW,
  TEXT_BACK_TO_LIST,
  TEXT_RETRY,
  type UserFormValues,
} from './constants';
import styles from './UserDetail.module.css';

/** Checkbox 受控值为 boolean,提交映射需 0/1(BR8) */
function normalizeFormValues(raw: UserFormValues): UserFormValues {
  return {
    ...raw,
    iCanModifyBill: (raw.iCanModifyBill ? 1 : 0) as 0 | 1,
  };
}

export default function UserDetailPage() {
  const navigate = useNavigate();
  const params = useParams<{ id?: string }>();
  const location = useLocation();
  const { message } = AntdApp.useApp();
  const [form] = Form.useForm<UserFormValues>();

  const mode = params.id ? MODE_EDIT : MODE_CREATE;
  const userId = params.id ? Number(params.id) : undefined;
  const presetUser = (location.state as { user?: UserVO } | null)?.user ?? null;

  const detail = useUserDetail({ mode, userId, presetUser });
  const {
    formValues,
    employees,
    permissions,
    checkedPermissionIds,
    readonlyCreator,
    readonlyCreateTime,
    loading,
    submitting,
    loadFailed,
    notFound,
    selectEmployee,
    togglePermission,
    toggleAll,
    submit,
    reload,
  } = detail;

  // hook 持有的受控值回写到 AntD Form(create 默认 / edit 回填,BR1/BR2/BR6/BR17)
  useEffect(() => {
    form.setFieldsValue({
      ...formValues,
      iCanModifyBill: (formValues.iCanModifyBill ? 1 : 0) as 0 | 1,
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [formValues]);

  const handleSave = async () => {
    try {
      const values = await form.validateFields();
      const ret = await submit(normalizeFormValues({ ...formValues, ...values }));
      if (ret.ok) {
        message.success(mode === MODE_CREATE ? MSG_CREATE_SUCCESS : MSG_EDIT_SUCCESS);
        navigate(PATH_USER_LIST);
      } else if (ret.fieldError) {
        form.setFields([
          { name: ret.fieldError.field, errors: [ret.fieldError.message] },
        ]);
      }
    } catch {
      // validateFields 失败:就近字段已展示错误,不发请求(BR12)
    }
  };

  const handleCancel = () => {
    if (form.isFieldsTouched()) {
      Modal.confirm({
        title: MSG_CANCEL_CONFIRM,
        onOk: () => navigate(PATH_USER_LIST),
      });
    } else {
      navigate(PATH_USER_LIST);
    }
  };

  const handleNew = () => {
    navigate(PATH_USER_NEW);
  };

  const handleSelectEmployee = (value: number | null) => {
    selectEmployee(value);
  };

  // edit 详情不存在(40401):返回列表入口(spec § 4)
  if (notFound) {
    return (
      <div className={styles.page}>
        <div className={styles.loadError} data-testid="userdetail-notfound">
          <span className={styles.loadErrorText}>{MSG_ERR_USER_NOT_FOUND}</span>
          <Button type="primary" onClick={() => navigate(PATH_USER_LIST)}>
            {TEXT_BACK_TO_LIST}
          </Button>
        </div>
      </div>
    );
  }

  // 预取/详情取数失败:重试入口(spec § 4 loadError)
  if (loadFailed) {
    return (
      <div className={styles.page}>
        <div className={styles.loadError} data-testid="userdetail-loaderror">
          <span className={styles.loadErrorText}>{MSG_LOAD_DETAIL_FAIL}</span>
          <Button type="primary" onClick={() => reload()}>
            {TEXT_RETRY}
          </Button>
        </div>
      </div>
    );
  }

  return (
    <div className={styles.page} data-testid="userdetail-page">
      <UserDetailToolbar
        mode={mode}
        submitting={submitting}
        canSave={!loading}
        onSave={() => void handleSave()}
        onCancel={handleCancel}
        onNew={handleNew}
      />
      <Spin spinning={loading}>
        <Form form={form} layout="vertical" component={false}>
          <UserBasicForm
            form={form}
            mode={mode}
            employees={employees}
            readonlyCreator={readonlyCreator}
            readonlyCreateTime={readonlyCreateTime}
            onSelectEmployee={handleSelectEmployee}
          />
        </Form>
        <PermissionTabs>
          <PermissionGroupList
            permissions={permissions}
            checkedIds={checkedPermissionIds}
            onToggle={togglePermission}
            onToggleAll={toggleAll}
          />
        </PermissionTabs>
      </Spin>
    </div>
  );
}