LabelGenerationList.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import React, { useMemo, useState, useEffect } from "react";
  2. import { AgGridReact } from "ag-grid-react";
  3. import {
  4. ClientSideRowModelModule,
  5. ColDef,
  6. ColGroupDef,
  7. ModuleRegistry,
  8. ICellRendererParams,
  9. SelectEditorModule,
  10. CellStyleModule
  11. } from "ag-grid-community";
  12. import "ag-grid-community/styles/ag-theme-alpine.css";
  13. import { DatePicker } from 'antd';
  14. import dayjs, { Dayjs } from 'dayjs';
  15. import { useDataProvider } from 'react-admin';
  16. import { useParams } from 'react-router-dom';
  17. // 注册AG Grid模块
  18. ModuleRegistry.registerModules([
  19. ClientSideRowModelModule,
  20. SelectEditorModule,
  21. CellStyleModule
  22. ]);
  23. // 部名选项
  24. const deptOptions = [
  25. { value: "", label: "選択しない" },
  26. { value: "営業部", label: "営業部" },
  27. { value: "物流部", label: "物流部" },
  28. { value: "管理部", label: "管理部" },
  29. { value: "技術部", label: "技術部" },
  30. { value: "人事部", label: "人事部" },
  31. { value: "販売第一部", label: "販売第一部" },
  32. ];
  33. // 地区名选项
  34. const areaOptions = [
  35. { value: "", label: "選択しない" },
  36. { value: "東  部", label: "東  部" },
  37. { value: "西  部", label: "西  部" },
  38. { value: "南  部", label: "南  部" },
  39. { value: "北  部", label: "北  部" },
  40. { value: "中  部", label: "中  部" }
  41. ];
  42. // 時台选项
  43. const hourOptions = [
  44. { value: "", label: "選択しない" },
  45. { value: "07", label: "07時台" },
  46. { value: "08", label: "08時台" },
  47. { value: "09", label: "09時台" },
  48. { value: "10", label: "10時台" },
  49. { value: "11", label: "11時台" },
  50. { value: "12", label: "12時台" },
  51. { value: "13", label: "13時台" }
  52. ];
  53. export const LabelGenerationList = () => {
  54. const containerStyle = useMemo(() => ({ width: "100%", overflow: "hidden" }), []);
  55. const [rowData, setRowData] = useState<any[]>([]);
  56. const [, setLoading] = useState<boolean>(true);
  57. const [selectedDept, setSelectedDept] = useState("");
  58. const [selectedArea, setSelectedArea] = useState("");
  59. const [selectedHour, setSelectedHour] = useState("");
  60. const [baseCode, setBaseCode] = useState("");
  61. const [baseName, setBaseName] = useState("");
  62. const [creationDate, setCreationDate] = useState<Dayjs | null>(null);
  63. const [isEditMode, setIsEditMode] = useState(false);
  64. const [selectedRows, setSelectedRows] = useState<any[]>([]);
  65. // React Admin 核心工具
  66. const dataProvider = useDataProvider();
  67. const params = useParams();
  68. // 初始化样式
  69. useEffect(() => {
  70. const style = document.createElement('style');
  71. style.textContent = `
  72. .ag-theme-alpine .header-center .ag-header-cell-label {
  73. justify-content: left !important;
  74. text-align: center !important;
  75. }
  76. .ag-theme-alpine .header-center.ag-header-group-cell {
  77. text-align: center !important;
  78. }
  79. .ag-theme-alpine .header-center.ag-header-group-cell .ag-header-group-cell-label {
  80. justify-content: center !important;
  81. display: flex !important;
  82. width: 100% !important;
  83. }
  84. .ag-header-cell-resize:after {
  85. display: none;
  86. }
  87. .ag-theme-alpine .ag-cell {
  88. border-right: 1px solid #dcdcdc;
  89. text-align: center;
  90. justify-content: left !important;
  91. }
  92. .ag-theme-alpine .ag-header-cell {
  93. border-right: 1px solid #dcdcdc;
  94. }
  95. .line-group-header {
  96. border-right: 1px solid #dcdcdc;
  97. }
  98. .date-group-header {
  99. border-right: 1px solid #dcdcdc !important;
  100. }
  101. /* 搜索区域样式 */
  102. .search-section {
  103. margin-bottom: 10px;
  104. }
  105. .search-section h3 {
  106. font-weight: bold;
  107. font-size: 16px;
  108. }
  109. .search-form {
  110. display: flex;
  111. flex-direction: column;
  112. }
  113. .form-row {
  114. display: flex;
  115. align-items: center;
  116. margin-bottom: 5px;
  117. }
  118. .form-row label {
  119. width: 100px;
  120. text-align: left;
  121. margin-right: 10px;
  122. }
  123. /* 统一所有下拉列表和输入框样式 */
  124. .form-row input,
  125. .form-row select,
  126. .form-row.ant-picker {
  127. padding: 5px;
  128. flex: 1;
  129. max-width: 200px;
  130. border: 1px solid #ccc;
  131. border-radius: 4px;
  132. box-sizing: border-box;
  133. }
  134. .top-section {
  135. display: flex;
  136. align-items: center;
  137. margin-bottom: 5px;
  138. }
  139. .top-section span {
  140. margin-right: 10px;
  141. font-weight: bold;
  142. font-size: 16px;
  143. }
  144. .correction-btn{
  145. background-color: #000;
  146. color: #fff;
  147. margin-left: auto;
  148. border: none;
  149. padding: 5px 20px;
  150. cursor: pointer;
  151. }
  152. .search-btn {
  153. background-color: #000;
  154. color: #fff;
  155. border: none;
  156. padding: 5px 40px;
  157. cursor: pointer;
  158. margin-left: 0;
  159. }
  160. .cancel-btn {
  161. background-color: #000;
  162. color: #fff;
  163. border: none;
  164. padding: 5px 20px;
  165. cursor: pointer;
  166. }
  167. .delete-btn {
  168. background-color: #000;
  169. color: #fff;
  170. border: none;
  171. padding: 5px 20px;
  172. cursor: pointer;
  173. }
  174. .loading-indicator {
  175. text-align: center;
  176. padding: 20px;
  177. font-size: 16px;
  178. }
  179. `;
  180. document.head.appendChild(style);
  181. return () => {
  182. document.head.removeChild(style);
  183. };
  184. }, []);
  185. // 从路由参数初始化查询条件
  186. useEffect(() => {
  187. if (params.baseCode) setBaseCode(params.baseCode);
  188. if (params.dateTm) setCreationDate(dayjs(params.dateTm));
  189. }, [params]);
  190. // 日期格式化
  191. const formatDateForBackend = () => {
  192. if (!creationDate) return '';
  193. const baseDate = creationDate.format('YYYY-MM-DD');
  194. const hour = selectedHour || '00';
  195. return `${baseDate} ${hour}:00:00`;
  196. };
  197. // 获取数据
  198. const fetchData = async () => {
  199. setLoading(true);
  200. try {
  201. const queryParams = {
  202. depName: selectedDept,
  203. regName: selectedArea,
  204. hour: selectedHour,
  205. baseCode,
  206. baseName,
  207. creationDate: formatDateForBackend()
  208. };
  209. const response = await dataProvider.getList('label/generations', {
  210. filter: queryParams
  211. });
  212. const formattedData = (response.data || []).map((item: any) => ({
  213. checkboxCol: "",
  214. id: item.id || "",
  215. depName: item.depName || "",
  216. regName: item.regName || "",
  217. baseCode: item.baseCode || "",
  218. baseName: item.baseName || "",
  219. zipCode: item.zipCode || "",
  220. inNumber: item.inNumber || "",
  221. classNumber: item.classNumber || "",
  222. creationDate: item.creationDate || "",
  223. implementation: item.implementation || ""
  224. }));
  225. setRowData(formattedData);
  226. } catch (error) {
  227. console.error("数据获取失败:", error);
  228. alert("データの取得に失敗しました。再試行してください。");
  229. } finally {
  230. setLoading(false);
  231. }
  232. };
  233. // 初始加载数据
  234. useEffect(() => {
  235. fetchData();
  236. }, []);
  237. // 构建列定义
  238. const columnDefs: (ColDef | ColGroupDef)[] = useMemo(() => {
  239. const baseColumns: ColDef[] = [
  240. {
  241. field: "depName",
  242. headerClass: "header-center",
  243. headerName: "部名",
  244. cellRenderer: (params: ICellRendererParams) => (
  245. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  246. ),
  247. width: 120,
  248. minWidth: 100,
  249. },
  250. {
  251. field: "regName",
  252. headerClass: "header-center",
  253. headerName: "地区名",
  254. cellRenderer: (params: ICellRendererParams) => (
  255. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  256. ),
  257. width: 130,
  258. minWidth: 100,
  259. },
  260. {
  261. field: "baseCode",
  262. headerClass: "header-center",
  263. headerName: "拠点コード",
  264. cellRenderer: (params: ICellRendererParams) => (
  265. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  266. ),
  267. flex: 1,
  268. minWidth: 110,
  269. },
  270. {
  271. field: "baseName",
  272. headerClass: "header-center",
  273. headerName: "拠点名",
  274. cellRenderer: (params: ICellRendererParams) => (
  275. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  276. ),
  277. flex: 1,
  278. minWidth: 170,
  279. },
  280. {
  281. field: "zipCode",
  282. headerClass: "header-center",
  283. headerName: "郵便番号",
  284. cellRenderer: (params: ICellRendererParams) => (
  285. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  286. ),
  287. flex: 1,
  288. minWidth: 110,
  289. },
  290. {
  291. field: "inNumber",
  292. headerClass: "header-center",
  293. headerName: "送り状番号",
  294. cellRenderer: (params: ICellRendererParams) => (
  295. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  296. ),
  297. flex: 1.5,
  298. minWidth: 130,
  299. },
  300. {
  301. field: "classNumber",
  302. headerClass: "header-center",
  303. headerName: "仕分け番号(ヤマト運輸)",
  304. cellRenderer: (params: ICellRendererParams) => (
  305. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  306. ),
  307. flex: 1.5,
  308. minWidth: 130,
  309. },
  310. {
  311. field: "creationDate",
  312. headerClass: "header-center",
  313. headerName: "生成日時",
  314. cellRenderer: (params: ICellRendererParams) => (
  315. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  316. ),
  317. flex: 1.5,
  318. minWidth: 140,
  319. },
  320. {
  321. field: "implementation",
  322. headerClass: "header-center",
  323. headerName: "実施者",
  324. cellRenderer: (params: ICellRendererParams) => (
  325. <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
  326. ),
  327. flex: 1,
  328. minWidth: 100,
  329. }
  330. ];
  331. // 编辑模式添加复选框列
  332. if (isEditMode) {
  333. return [
  334. {
  335. field: "checkboxCol",
  336. headerName: "削除",
  337. width: 70,
  338. cellRenderer: (params: ICellRendererParams) => {
  339. const currentRow = params.data;
  340. const isChecked = selectedRows.includes(currentRow);
  341. const handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
  342. setSelectedRows(prev =>
  343. e.target.checked
  344. ? [...prev, currentRow]
  345. : prev.filter(row => row!== currentRow)
  346. );
  347. };
  348. return (
  349. <div style={{ textAlign: 'center' }}>
  350. <input
  351. type="checkbox"
  352. checked={isChecked}
  353. onChange={handleCheck}
  354. />
  355. </div>
  356. );
  357. },
  358. headerClass: "header-center"
  359. },
  360. ...baseColumns
  361. ];
  362. }
  363. return baseColumns;
  364. }, [isEditMode, selectedRows]);
  365. // 取消编辑
  366. const handleCancel = () => {
  367. setIsEditMode(false);
  368. setSelectedRows([]);
  369. };
  370. // 批量删除
  371. const handleBatchDelete = async () => {
  372. if (selectedRows.length === 0) {
  373. alert("削除するデータを選択してください");
  374. return;
  375. }
  376. try {
  377. const ids = selectedRows.map(row => row.id);
  378. await dataProvider.deleteMany('label/generations', { ids });
  379. setRowData(prev => prev.filter(row =>!selectedRows.includes(row)));
  380. setSelectedRows([]);
  381. alert(`${selectedRows.length}件のデータを削除しました`);
  382. } catch (error) {
  383. console.error("删除失败:", error);
  384. alert("削除に失敗しました。再試行してください。");
  385. }
  386. };
  387. return (
  388. <div style={containerStyle}>
  389. {/* 检索区域 */}
  390. <div className="search-section">
  391. <h3>ラベル生成ログ検索</h3>
  392. <form className="search-form">
  393. <div className="form-row">
  394. <label>部名</label>
  395. <select
  396. value={selectedDept}
  397. onChange={(e) => setSelectedDept(e.target.value)}
  398. >
  399. {deptOptions.map(option => (
  400. <option key={option.value} value={option.value}>
  401. {option.label}
  402. </option>
  403. ))}
  404. </select>
  405. </div>
  406. <div className="form-row">
  407. <label>地区名</label>
  408. <select
  409. value={selectedArea}
  410. onChange={(e) => setSelectedArea(e.target.value)}
  411. >
  412. {areaOptions.map(option => (
  413. <option key={option.value} value={option.value}>
  414. {option.label}
  415. </option>
  416. ))}
  417. </select>
  418. </div>
  419. <div className="form-row">
  420. <label>拠点コード</label>
  421. <input
  422. type="text"
  423. value={baseCode}
  424. onChange={(e) => setBaseCode(e.target.value)}
  425. />
  426. </div>
  427. <div className="form-row">
  428. <label>拠点名</label>
  429. <input
  430. type="text"
  431. value={baseName}
  432. onChange={(e) => setBaseName(e.target.value)}
  433. />
  434. </div>
  435. <div className="form-row">
  436. <label>生成日</label>
  437. <DatePicker
  438. value={creationDate}
  439. onChange={(date) => setCreationDate(date)}
  440. format="YYYY-MM-DD"
  441. style={{ maxWidth: '200px', flex: 1 }}
  442. />
  443. <div style={{ marginLeft: '30px' }}>
  444. <select
  445. value={selectedHour}
  446. onChange={(e) => setSelectedHour(e.target.value)}
  447. >
  448. {hourOptions.map(option => (
  449. <option key={option.value} value={option.value}>
  450. {option.label}
  451. </option>
  452. ))}
  453. </select>
  454. <span>時台</span>
  455. </div>
  456. </div>
  457. <div className="form-row">
  458. <button
  459. type="button"
  460. className="search-btn"
  461. onClick={fetchData}
  462. >
  463. 検索
  464. </button>
  465. </div>
  466. </form>
  467. </div>
  468. {/* 表格标题区域 */}
  469. <div className="top-section">
  470. <span>ラベル生成ログ</span>
  471. <button
  472. className="correction-btn"
  473. onClick={() => setIsEditMode(true)}
  474. style={{ display: isEditMode? 'none' : 'block', marginLeft: 'auto' }}
  475. >
  476. 実績値修正
  477. </button>
  478. </div>
  479. {/* 数据表格*/}
  480. <div className="ag-theme-alpine" style={{ width: '100%', minWidth: '900px' }}>
  481. <AgGridReact
  482. rowData={rowData}
  483. columnDefs={columnDefs}
  484. domLayout="autoHeight"
  485. />
  486. </div>
  487. {/* 编辑模式操作按钮*/}
  488. {isEditMode && (
  489. <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '10px' }}>
  490. <button
  491. className="cancel-btn"
  492. style={{ marginRight: '40px' }}
  493. onClick={handleCancel}
  494. >
  495. キャンセル
  496. </button>
  497. <button
  498. className="delete-btn"
  499. onClick={handleBatchDelete}
  500. >
  501. 一括削除
  502. </button>
  503. </div>
  504. )}
  505. </div>
  506. );
  507. };
  508. export default LabelGenerationList;