|
|
@@ -0,0 +1,553 @@
|
|
|
+import React, { useMemo, useState, useEffect } from "react";
|
|
|
+import { AgGridReact } from "ag-grid-react";
|
|
|
+import {
|
|
|
+ ClientSideRowModelModule,
|
|
|
+ ColDef,
|
|
|
+ ColGroupDef,
|
|
|
+ ModuleRegistry,
|
|
|
+ ICellRendererParams,
|
|
|
+ SelectEditorModule,
|
|
|
+ CellStyleModule
|
|
|
+} from "ag-grid-community";
|
|
|
+import "ag-grid-community/styles/ag-theme-alpine.css";
|
|
|
+import { DatePicker } from 'antd';
|
|
|
+import dayjs, { Dayjs } from 'dayjs';
|
|
|
+import { useDataProvider } from 'react-admin';
|
|
|
+import { useParams } from 'react-router-dom';
|
|
|
+
|
|
|
+// 注册AG Grid模块
|
|
|
+ModuleRegistry.registerModules([
|
|
|
+ ClientSideRowModelModule,
|
|
|
+ SelectEditorModule,
|
|
|
+ CellStyleModule
|
|
|
+]);
|
|
|
+
|
|
|
+// 部名选项
|
|
|
+const deptOptions = [
|
|
|
+ { value: "", label: "選択しない" },
|
|
|
+ { value: "営業部", label: "営業部" },
|
|
|
+ { value: "物流部", label: "物流部" },
|
|
|
+ { value: "管理部", label: "管理部" },
|
|
|
+ { value: "技術部", label: "技術部" },
|
|
|
+ { value: "人事部", label: "人事部" },
|
|
|
+ { value: "販売第一部", label: "販売第一部" },
|
|
|
+];
|
|
|
+
|
|
|
+// 地区名选项
|
|
|
+const areaOptions = [
|
|
|
+ { value: "", label: "選択しない" },
|
|
|
+ { value: "東 部", label: "東 部" },
|
|
|
+ { value: "西 部", label: "西 部" },
|
|
|
+ { value: "南 部", label: "南 部" },
|
|
|
+ { value: "北 部", label: "北 部" },
|
|
|
+ { value: "中 部", label: "中 部" }
|
|
|
+];
|
|
|
+
|
|
|
+// 時台选项
|
|
|
+const hourOptions = [
|
|
|
+ { value: "", label: "選択しない" },
|
|
|
+ { value: "07", label: "07時台" },
|
|
|
+ { value: "08", label: "08時台" },
|
|
|
+ { value: "09", label: "09時台" },
|
|
|
+ { value: "10", label: "10時台" },
|
|
|
+ { value: "11", label: "11時台" },
|
|
|
+ { value: "12", label: "12時台" },
|
|
|
+ { value: "13", label: "13時台" }
|
|
|
+];
|
|
|
+
|
|
|
+export const LabelGenerationList = () => {
|
|
|
+ const containerStyle = useMemo(() => ({ width: "100%", overflow: "hidden" }), []);
|
|
|
+ const [rowData, setRowData] = useState<any[]>([]);
|
|
|
+ const [, setLoading] = useState<boolean>(true);
|
|
|
+ const [selectedDept, setSelectedDept] = useState("");
|
|
|
+ const [selectedArea, setSelectedArea] = useState("");
|
|
|
+ const [selectedHour, setSelectedHour] = useState("");
|
|
|
+ const [baseCode, setBaseCode] = useState("");
|
|
|
+ const [baseName, setBaseName] = useState("");
|
|
|
+ const [creationDate, setCreationDate] = useState<Dayjs | null>(null);
|
|
|
+ const [isEditMode, setIsEditMode] = useState(false);
|
|
|
+ const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
|
+
|
|
|
+ // React Admin 核心工具
|
|
|
+ const dataProvider = useDataProvider();
|
|
|
+ const params = useParams();
|
|
|
+
|
|
|
+ // 初始化样式
|
|
|
+ useEffect(() => {
|
|
|
+ const style = document.createElement('style');
|
|
|
+ style.textContent = `
|
|
|
+ .ag-theme-alpine .header-center .ag-header-cell-label {
|
|
|
+ justify-content: left !important;
|
|
|
+ text-align: center !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ag-theme-alpine .header-center.ag-header-group-cell {
|
|
|
+ text-align: center !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ag-theme-alpine .header-center.ag-header-group-cell .ag-header-group-cell-label {
|
|
|
+ justify-content: center !important;
|
|
|
+ display: flex !important;
|
|
|
+ width: 100% !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ag-header-cell-resize:after {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ag-theme-alpine .ag-cell {
|
|
|
+ border-right: 1px solid #dcdcdc;
|
|
|
+ text-align: center;
|
|
|
+ justify-content: left !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ag-theme-alpine .ag-header-cell {
|
|
|
+ border-right: 1px solid #dcdcdc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .line-group-header {
|
|
|
+ border-right: 1px solid #dcdcdc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .date-group-header {
|
|
|
+ border-right: 1px solid #dcdcdc !important;
|
|
|
+ }
|
|
|
+ /* 搜索区域样式 */
|
|
|
+ .search-section {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+ .search-section h3 {
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+ .search-form {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+ .form-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+ .form-row label {
|
|
|
+ width: 100px;
|
|
|
+ text-align: left;
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+ /* 统一所有下拉列表和输入框样式 */
|
|
|
+ .form-row input,
|
|
|
+ .form-row select,
|
|
|
+ .form-row.ant-picker {
|
|
|
+ padding: 5px;
|
|
|
+ flex: 1;
|
|
|
+ max-width: 200px;
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+ .top-section {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+ .top-section span {
|
|
|
+ margin-right: 10px;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+ .correction-btn{
|
|
|
+ background-color: #000;
|
|
|
+ color: #fff;
|
|
|
+ margin-left: auto;
|
|
|
+ border: none;
|
|
|
+ padding: 5px 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ .search-btn {
|
|
|
+ background-color: #000;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ padding: 5px 40px;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-left: 0;
|
|
|
+ }
|
|
|
+ .cancel-btn {
|
|
|
+ background-color: #000;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ padding: 5px 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ .delete-btn {
|
|
|
+ background-color: #000;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ padding: 5px 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ .loading-indicator {
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+ `;
|
|
|
+ document.head.appendChild(style);
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ document.head.removeChild(style);
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 从路由参数初始化查询条件
|
|
|
+ useEffect(() => {
|
|
|
+ if (params.baseCode) setBaseCode(params.baseCode);
|
|
|
+ if (params.dateTm) setCreationDate(dayjs(params.dateTm));
|
|
|
+ }, [params]);
|
|
|
+
|
|
|
+ // 日期格式化
|
|
|
+ const formatDateForBackend = () => {
|
|
|
+ if (!creationDate) return '';
|
|
|
+ const baseDate = creationDate.format('YYYY-MM-DD');
|
|
|
+ const hour = selectedHour || '00';
|
|
|
+ return `${baseDate} ${hour}:00:00`;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取数据
|
|
|
+ const fetchData = async () => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const queryParams = {
|
|
|
+ depName: selectedDept,
|
|
|
+ regName: selectedArea,
|
|
|
+ hour: selectedHour,
|
|
|
+ baseCode,
|
|
|
+ baseName,
|
|
|
+ creationDate: formatDateForBackend()
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await dataProvider.getList('label/generations', {
|
|
|
+ pagination: { page: 1, perPage: 100 },
|
|
|
+ sort: { field: 'creationDate', order: 'DESC' },
|
|
|
+ filter: queryParams
|
|
|
+ });
|
|
|
+
|
|
|
+ const formattedData = (response.data || []).map((item: any) => ({
|
|
|
+ checkboxCol: "",
|
|
|
+ id: item.id || "",
|
|
|
+ depName: item.depName || "",
|
|
|
+ regName: item.regName || "",
|
|
|
+ baseCode: item.baseCode || "",
|
|
|
+ baseName: item.baseName || "",
|
|
|
+ zipCode: item.zipCode || "",
|
|
|
+ inNumber: item.inNumber || "",
|
|
|
+ classNumber: item.classNumber || "",
|
|
|
+ creationDate: item.creationDate || "",
|
|
|
+ implementation: item.implementation || ""
|
|
|
+ }));
|
|
|
+ setRowData(formattedData);
|
|
|
+ } catch (error) {
|
|
|
+ console.error("数据获取失败:", error);
|
|
|
+ alert("データの取得に失敗しました。再試行してください。");
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 初始加载数据
|
|
|
+ useEffect(() => {
|
|
|
+ fetchData();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 构建列定义
|
|
|
+ const columnDefs: (ColDef | ColGroupDef)[] = useMemo(() => {
|
|
|
+ const baseColumns: ColDef[] = [
|
|
|
+ {
|
|
|
+ field: "depName",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "部名",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ width: 120,
|
|
|
+ minWidth: 100,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: "regName",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "地区名",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ width: 130,
|
|
|
+ minWidth: 100,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: "baseCode",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "拠点コード",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ flex: 1,
|
|
|
+ minWidth: 110,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: "baseName",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "拠点名",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ flex: 1,
|
|
|
+ minWidth: 170,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: "zipCode",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "郵便番号",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ flex: 1,
|
|
|
+ minWidth: 110,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: "inNumber",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "送り状番号",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ flex: 1.5,
|
|
|
+ minWidth: 130,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: "classNumber",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "仕分け番号(ヤマト運輸)",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ flex: 1.5,
|
|
|
+ minWidth: 130,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: "creationDate",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "生成日時",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ flex: 1.5,
|
|
|
+ minWidth: 140,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: "implementation",
|
|
|
+ headerClass: "header-center",
|
|
|
+ headerName: "実施者",
|
|
|
+ cellRenderer: (params: ICellRendererParams) => (
|
|
|
+ <div style={{ textAlign: 'left'}}>{params.value || ''}</div>
|
|
|
+ ),
|
|
|
+ flex: 1,
|
|
|
+ minWidth: 100,
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 编辑模式添加复选框列
|
|
|
+ if (isEditMode) {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ field: "checkboxCol",
|
|
|
+ headerName: "削除",
|
|
|
+ width: 70,
|
|
|
+ cellRenderer: (params: ICellRendererParams) => {
|
|
|
+ const currentRow = params.data;
|
|
|
+ const isChecked = selectedRows.includes(currentRow);
|
|
|
+
|
|
|
+ const handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
+ setSelectedRows(prev =>
|
|
|
+ e.target.checked
|
|
|
+ ? [...prev, currentRow]
|
|
|
+ : prev.filter(row => row!== currentRow)
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div style={{ textAlign: 'center' }}>
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ checked={isChecked}
|
|
|
+ onChange={handleCheck}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ headerClass: "header-center"
|
|
|
+ },
|
|
|
+ ...baseColumns
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return baseColumns;
|
|
|
+ }, [isEditMode, selectedRows]);
|
|
|
+
|
|
|
+ // 取消编辑
|
|
|
+ const handleCancel = () => {
|
|
|
+ setIsEditMode(false);
|
|
|
+ setSelectedRows([]);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 批量删除
|
|
|
+ const handleBatchDelete = async () => {
|
|
|
+ if (selectedRows.length === 0) {
|
|
|
+ alert("削除するデータを選択してください");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const ids = selectedRows.map(row => row.id);
|
|
|
+ await dataProvider.deleteMany('label/generations', { ids });
|
|
|
+
|
|
|
+ setRowData(prev => prev.filter(row =>!selectedRows.includes(row)));
|
|
|
+ setSelectedRows([]);
|
|
|
+ alert(`${selectedRows.length}件のデータを削除しました`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error("删除失败:", error);
|
|
|
+ alert("削除に失敗しました。再試行してください。");
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div style={containerStyle}>
|
|
|
+ {/* 检索区域 */}
|
|
|
+ <div className="search-section">
|
|
|
+ <h3>ラベル生成ログ検索</h3>
|
|
|
+ <form className="search-form">
|
|
|
+ <div className="form-row">
|
|
|
+ <label>部名</label>
|
|
|
+ <select
|
|
|
+ value={selectedDept}
|
|
|
+ onChange={(e) => setSelectedDept(e.target.value)}
|
|
|
+ >
|
|
|
+ {deptOptions.map(option => (
|
|
|
+ <option key={option.value} value={option.value}>
|
|
|
+ {option.label}
|
|
|
+ </option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="form-row">
|
|
|
+ <label>地区名</label>
|
|
|
+ <select
|
|
|
+ value={selectedArea}
|
|
|
+ onChange={(e) => setSelectedArea(e.target.value)}
|
|
|
+ >
|
|
|
+ {areaOptions.map(option => (
|
|
|
+ <option key={option.value} value={option.value}>
|
|
|
+ {option.label}
|
|
|
+ </option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="form-row">
|
|
|
+ <label>拠点コード</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={baseCode}
|
|
|
+ onChange={(e) => setBaseCode(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="form-row">
|
|
|
+ <label>拠点名</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={baseName}
|
|
|
+ onChange={(e) => setBaseName(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="form-row">
|
|
|
+ <label>生成日</label>
|
|
|
+ <DatePicker
|
|
|
+ value={creationDate}
|
|
|
+ onChange={(date) => setCreationDate(date)}
|
|
|
+ format="YYYY-MM-DD"
|
|
|
+ style={{ maxWidth: '200px', flex: 1 }}
|
|
|
+ />
|
|
|
+ <div style={{ marginLeft: '30px' }}>
|
|
|
+ <select
|
|
|
+ value={selectedHour}
|
|
|
+ onChange={(e) => setSelectedHour(e.target.value)}
|
|
|
+ >
|
|
|
+ {hourOptions.map(option => (
|
|
|
+ <option key={option.value} value={option.value}>
|
|
|
+ {option.label}
|
|
|
+ </option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ <span>時台</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="form-row">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className="search-btn"
|
|
|
+ onClick={fetchData}
|
|
|
+ >
|
|
|
+ 検索
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 表格标题区域 */}
|
|
|
+ <div className="top-section">
|
|
|
+ <span>ラベル生成ログ</span>
|
|
|
+ <button
|
|
|
+ className="correction-btn"
|
|
|
+ onClick={() => setIsEditMode(true)}
|
|
|
+ style={{ display: isEditMode? 'none' : 'block', marginLeft: 'auto' }}
|
|
|
+ >
|
|
|
+ 実績値修正
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 数据表格*/}
|
|
|
+ <div className="ag-theme-alpine" style={{ width: '100%', minWidth: '900px' }}>
|
|
|
+ <AgGridReact
|
|
|
+ rowData={rowData}
|
|
|
+ columnDefs={columnDefs}
|
|
|
+ domLayout="autoHeight"
|
|
|
+ suppressHorizontalScroll={false}
|
|
|
+ rowHeight={35}
|
|
|
+ headerHeight={35}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 编辑模式操作按钮*/}
|
|
|
+ {isEditMode && (
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '10px' }}>
|
|
|
+ <button
|
|
|
+ className="cancel-btn"
|
|
|
+ style={{ marginRight: '40px' }}
|
|
|
+ onClick={handleCancel}
|
|
|
+ >
|
|
|
+ キャンセル
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ className="delete-btn"
|
|
|
+ onClick={handleBatchDelete}
|
|
|
+ >
|
|
|
+ 一括削除
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default LabelGenerationList;
|