liuxf 3 months ago
parent
commit
c1ed4676ac

+ 420 - 0
src/pages/Base/Action/Chat.tsx

@@ -0,0 +1,420 @@
+import React, { useState, useRef, useEffect } from "react";
+
+// 聊天组件 props 类型
+type ChatProps = {
+  onClose: () => void;
+};
+
+// 聊天消息类型定义
+type ChatMessage = {
+  id: string;
+  sender: "other" | "me";
+  content: string;
+  timestamp: string;
+  attachment?: {
+    name: string;
+    url: string;
+  };
+};
+
+// 聊天组件 props 类型
+type ChatComponentProps = {
+  messageList: ChatMessage[];
+  onClose: () => void;
+};
+
+const ChatComponent: React.FC<ChatComponentProps> = ({ messageList, onClose }) => {
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+
+  const baseMessageStyle = {
+    display: "flex",
+    marginBottom: "12px",
+    width: "100%",
+  } as React.CSSProperties;
+
+  const senderInfoStyle = {
+    display: "flex",
+    alignItems: "center",
+    marginBottom: "4px",
+  } as React.CSSProperties;
+
+  const iconStyle = {
+    width: "20px",
+    height: "20px",
+    marginRight: "4px",
+  } as React.CSSProperties;
+
+  const senderNameStyle = {
+    fontSize: "14px",
+    fontWeight: "bold",
+  } as React.CSSProperties;
+
+  const bubbleStyle = {
+    padding: "10px 14px",
+    borderRadius: "8px",
+    boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
+    maxWidth: "70%",
+    wordWrap: "break-word",
+  } as React.CSSProperties;
+
+  const contentStyle = {
+    marginBottom: "0",
+    lineHeight: "1.5",
+    fontSize: "14px",
+    textAlign: "left",
+  } as React.CSSProperties;
+
+  const attachmentContainerStyle = {
+    display: "flex",
+    alignItems: "center",
+    marginBottom: "4px",
+    gap: "8px",
+  } as React.CSSProperties;
+
+  const fileNameWrapStyle = {
+    border: "1px solid #ccc",
+    borderRadius: "4px",
+    padding: "6px 8px",
+    display: "inline-block",
+  } as React.CSSProperties;
+
+  const attachmentNameStyle = {
+    fontSize: "14px",
+    color: "#000",
+    whiteSpace: "nowrap",
+    overflow: "hidden",
+    textOverflow: "ellipsis",
+    maxWidth: "200px",
+  } as React.CSSProperties;
+
+  const downloadLinkStyle = {
+    fontSize: "14px",
+    color: "#0066cc",
+    textDecoration: "underline",
+    cursor: "pointer",
+    whiteSpace: "nowrap",
+  } as React.CSSProperties;
+
+  const timestampStyle = {
+    fontSize: "14px",
+    color: "#000",
+  } as React.CSSProperties;
+
+  useEffect(() => {
+    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+  }, [messageList]);
+
+  return (
+    <div>
+      <div
+        style={{
+          marginBottom: "10px",
+          display: "flex",
+          alignItems: "center",
+          marginLeft: "-10px",
+        }}
+      >
+        <img
+          src="/return.svg"
+          alt="返回箭头"
+          style={{ width: "25px", height: "25px" }}
+        />
+        <span
+          style={{ fontSize: "14px", cursor: "pointer" }}
+          onClick={onClose}
+        > 戻る </span>
+      </div>
+
+      {messageList.map((msg) => (
+        <div
+          key={msg.id}
+          style={{
+            ...baseMessageStyle,
+            justifyContent: msg.sender === "other"? "flex-start" : "flex-end",
+            display: "flex",
+            flexDirection: "column",
+            alignItems: msg.sender === "other"? "flex-start" : "flex-end",
+            paddingLeft: "40px",
+          }}
+        >
+          {/* 根据发送方显示对应的名称和图标 */}
+          {msg.sender === "other" && (
+            <div style={senderInfoStyle}>
+              <img
+                src="/usr.png"
+                alt="AAA株式会社图标"
+                style={iconStyle}
+              />
+              <span style={senderNameStyle}>AAA株式会社</span>
+            </div>
+          )}
+          {msg.sender === "me" && (
+            <div style={senderInfoStyle}>
+              <span style={senderNameStyle}>羽田ベース</span>
+              <img
+                src="/cat.png"
+                alt="羽田ベース图标"
+                style={{...iconStyle, marginLeft: "4px", marginRight: "0" }}
+              />
+            </div>
+          )}
+
+          {/*聊天内容气泡:内容非空时才显示 */}
+          {msg.content.trim()!== "" && (
+            <div
+              style={{
+                ...bubbleStyle,
+                backgroundColor: msg.sender === "other"? "#d9d9d9" : "#deebf7",
+                marginBottom: "4px",
+              }}
+            >
+              <div style={contentStyle}>
+                {msg.content.split("\n").map((line, index) => (
+                  <React.Fragment key={index}>
+                    {line}
+                    {index < msg.content.split("\n").length - 1 && <br />}
+                  </React.Fragment>
+                ))}
+              </div>
+            </div>
+          )}
+
+          {/* 附件区域 */}
+          {msg.attachment && (
+            <div style={attachmentContainerStyle}>
+              <div style={fileNameWrapStyle}>
+                <span style={attachmentNameStyle}>{msg.attachment.name}</span>
+              </div>
+              <a
+                href={msg.attachment.url}
+                download={msg.attachment.name}
+                style={downloadLinkStyle}
+              >
+                ダウンロード
+              </a>
+            </div>
+          )}
+
+          {/* 时间戳 */}
+          <div
+            style={{
+              ...timestampStyle,
+              marginLeft: msg.sender === "other"? "10px" : "0",
+              marginRight: msg.sender === "me"? "10px" : "0",
+              textAlign: msg.sender === "other"? "left" : "right",
+            }}
+          >
+            {msg.timestamp}
+          </div>
+        </div>
+      ))}
+
+      {/* 用于自动滚动的参考点 */}
+      <div ref={messagesEndRef} />
+    </div>
+  );
+};
+
+// 输入区域组件
+const InputArea: React.FC<{ onSendMessage: (content: string, file: File | null) => void }> = ({ onSendMessage }) => {
+  const [inputValue, setInputValue] = useState("");
+  const [selectedFile, setSelectedFile] = useState<File | null>(null);
+
+  const handleSend = () => {
+    if (inputValue.trim() || selectedFile) {
+      onSendMessage(inputValue.trim(), selectedFile);
+      setInputValue("");
+      setSelectedFile(null);
+    }
+  };
+
+  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files && e.target.files.length > 0) {
+      setSelectedFile(e.target.files[0]);
+    }
+  };
+
+  const handleRemoveFile = () => {
+    setSelectedFile(null);
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    if (e.key === 'Enter' &&!e.shiftKey) {
+      e.preventDefault();
+      handleSend();
+    }
+  };
+
+  return (
+    <div
+      style={{
+        width: "100%",
+        maxWidth: "600px",
+        margin: "0 auto",
+        padding: "16px",
+        display: "flex",
+        flexDirection: "column",
+        gap: "8px",
+      }}
+    >
+      <textarea
+        value={inputValue}
+        onChange={(e) => setInputValue(e.target.value)}
+        onKeyDown={handleKeyDown}
+        placeholder="メッセージを入力する..."
+        style={{
+          width: "100%",
+          height: "180px",
+          padding: "8px",
+          borderRadius: "4px",
+          border: "1px solid #ccc",
+          resize: "none",
+        }}
+      />
+      {selectedFile && (
+        <div
+          style={{
+            display: "flex",
+            alignItems: "center",
+            justifyContent: "space-between",
+            backgroundColor: "#f9f9f9",
+            padding: "8px",
+            borderRadius: "4px",
+          }}
+        >
+          <div style={{ display: "flex", alignItems: "center" }}>
+            <span style={{ marginRight: "8px", color: "#0066cc" }}>
+              {selectedFile.name.split('.').pop()?.toUpperCase() || 'FILE'}
+            </span>
+            <span>{selectedFile.name}</span>
+          </div>
+          <span
+            style={{
+              cursor: "pointer",
+              color: "#000",
+              fontWeight: "bold",
+              fontSize: "16px",
+            }}
+            onClick={handleRemoveFile}
+          >
+            ×
+          </span>
+        </div>
+      )}
+      <div
+        style={{
+          display: "flex",
+          alignItems: "center",
+          justifyContent: "space-between",
+        }}
+      >
+        <label
+          htmlFor="fileInput"
+          style={{
+            color: "#0066cc",
+            textDecoration: "underline",
+            cursor: "pointer",
+          }}
+        >
+          ファイルアップロード
+        </label>
+        <input
+          type="file"
+          id="fileInput"
+          onChange={handleFileChange}
+          style={{
+            display: "none",
+          }}
+        />
+        <button
+          style={{
+            padding: "8px 16px",
+            backgroundColor: "#000",
+            color: "#fff",
+            border: "none",
+            borderRadius: "4px",
+            cursor: "pointer",
+          }}
+          onClick={handleSend}
+        >
+          送信
+        </button>
+      </div>
+    </div>
+  );
+};
+
+// 示例数据
+const initialMessages: ChatMessage[] = [
+  {
+    id: "1",
+    sender: "other",
+    content: "実積を確認しました。\n21日は追加で稼働していませんが、実積に反映されていません。",
+    timestamp: "2025-7-1 12:21",
+  },
+  {
+    id: "2",
+    sender: "me",
+    content: "ご確認ありがとうございます。\n恐れ入りますが、運行の証跡がかかるものをアップロードいただけますでしょうか。",
+    timestamp: "2025-7-1 12:25",
+  },
+  {
+    id: "3",
+    sender: "other",
+    content: "1ファイルを送ります。\nこちらでご確認お願いします。",
+    timestamp: "2025-7-1 13:07",
+    attachment: {
+      name: "6月運行証明.xlsx",
+      url: "#",
+    },
+  },
+  {
+    id: "4",
+    sender: "me",
+    content: "ありがとうございます。\n内容確認して、再度ご連絡させていただきます。",
+    timestamp: "2025-7-1 13:31",
+  },
+];
+
+const Chat: React.FC<ChatProps> = ({ onClose }) => {
+  const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
+
+  const handleSendMessage = (content: string, file: File | null) => {
+    const newMessage: ChatMessage = {
+      id: Date.now().toString(),
+      sender: "me",
+      content,
+      timestamp: new Date()
+        .toLocaleString("ja-JP", {
+          year: "numeric",
+          month: "long",
+          day: "numeric",
+          hour: "2-digit",
+          minute: "2-digit",
+        })
+        .replace(/[年月]/g, "-")
+        .replace("日", ""),
+    };
+
+    if (file) {
+      newMessage.attachment = {
+        name: file.name,
+        url: "#", // 实际需替换为后端返回的下载链接
+      };
+    }
+
+    setMessages([...messages, newMessage]);
+  };
+
+  return (
+    <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
+      <div className="chat-messages-container">
+        <ChatComponent messageList={messages} onClose={onClose} />
+      </div>
+      <InputArea onSendMessage={handleSendMessage} />
+    </div>
+  );
+};
+
+export default Chat;
+

+ 154 - 0
src/pages/Base/Action/ChatStyles.css

@@ -0,0 +1,154 @@
+/* 聊天容器 */
+.chat-container {
+  width: 100%;
+  max-width: 700px;
+  margin: 0 auto;
+  padding: 16px;
+  font-family: "Helvetica Neue", Arial, sans-serif;
+}
+
+/* 返回按钮 */
+.back-button {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  margin-bottom: 20px;
+}
+.back-icon {
+  width: 24px;
+  height: 24px;
+  margin-right: 8px;
+}
+.back-text {
+  font-size: 14px;
+  color: #333;
+}
+
+/* 消息列表 */
+.messages-wrapper {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+/* 消息行(包含头像 + 内容) */
+.message-row {
+  display: flex;
+  align-items: flex-start;
+  width: 100%;
+}
+
+/* 对方消息:头像在左,内容在右 */
+.other-message {
+  justify-content: flex-start;
+}
+
+/* 我方消息:头像在右,内容在左 */
+.my-message {
+  justify-content: flex-end;
+}
+
+/* 头像样式 */
+.message-avatar {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  margin: 8px 12px 0 0;
+}
+.my-avatar {
+  margin: 8px 0 0 12px;
+}
+
+/* 内容包裹层 */
+.message-content-wrapper {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+/* 聊天气泡 */
+.message-bubble {
+  max-width: 60%;
+  padding: 12px 16px;
+  border-radius: 12px;
+  margin-bottom: 4px;
+  position: relative;
+  line-height: 1.4;
+}
+
+/* 对方气泡:灰色 + 左上角三角 */
+.other-bubble {
+  background-color: #f1f1f1;
+  color: #333;
+}
+.other-bubble::before {
+  content: "";
+  position: absolute;
+  top: 12px;
+  left: -12px;
+  border-top: 8px solid transparent;
+  border-right: 12px solid #f1f1f1;
+  border-bottom: 8px solid transparent;
+}
+
+/* 我方气泡:浅蓝色 + 右上角三角 */
+.my-bubble {
+  background-color: #d1e8ff;
+  color: #333;
+}
+.my-bubble::before {
+  content: "";
+  position: absolute;
+  top: 12px;
+  right: -12px;
+  border-top: 8px solid transparent;
+  border-left: 12px solid #d1e8ff;
+  border-bottom: 8px solid transparent;
+}
+
+/* 气泡文本 */
+.bubble-text {
+  margin: 0;
+  font-size: 14px;
+}
+
+/* 时间戳 */
+.timestamp {
+  font-size: 12px;
+  color: #999;
+}
+.other-timestamp {
+  margin-left: 4px;
+}
+.my-timestamp {
+  margin-right: 4px;
+  align-self: flex-end;
+}
+
+/* 输入区域 */
+.input-area {
+  display: flex;
+  gap: 8px;
+  margin-top: 20px;
+}
+.message-input {
+  flex: 1;
+  height: 80px;
+  padding: 12px;
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  resize: none;
+  font-size: 14px;
+}
+.send-button {
+  padding: 12px 24px;
+  background-color: #0078ff;
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  font-size: 14px;
+}
+.send-button:hover {
+  background-color: #005ecc;
+}

+ 155 - 0
src/pages/Base/Action/DateChangeDialog.tsx

@@ -0,0 +1,155 @@
+import React, { useState, useEffect, useRef } from "react";
+
+// 定义组件 props 类型
+interface DateChangeDialogProps {
+  show: boolean; // 控制对话框显示/隐藏
+  onClose: () => void; // 关闭对话框的回调
+  onConfirm: (reason: string, changeValue: string) => void; // 确认变更的回调
+  reasonOptions: string[]; // 事由选项列表
+}
+
+const DateChangeDialog: React.FC<DateChangeDialogProps> = ({
+  show,
+  onClose,
+  onConfirm,
+  reasonOptions,
+}) => {
+  // 组件内部维护输入状态
+  const [selectedReason, setSelectedReason] = useState("");
+  const [changeValue, setChangeValue] = useState("");
+  const reasonSelectRef = useRef<HTMLSelectElement>(null);
+
+  // 组件挂载时添加样式
+  useEffect(() => {
+    const style = document.createElement('style');
+    style.id = 'date-change-dialog-styles';
+    style.textContent = `
+      .custom-dialog {
+        position: fixed;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        padding: 20px;
+        background: white;
+        border: 1px solid #ccc;
+        border-radius: 5px;
+        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+        z-index: 1000;
+        width: 300px;
+      }
+      .dialog-overlay {
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: rgba(0,0,0,0.5);
+        z-index: 999;
+      }
+      .dialog-group {
+        margin-bottom: 15px;
+      }
+      .dialog-group select,
+      .dialog-group input {
+        width: 100%;
+        padding: 8px;
+        box-sizing: border-box;
+      }
+      .dialog-row {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+      }
+      .dialog-label {
+        width: 60px;
+        margin-bottom: 0;
+      }
+      .dialog-select,
+      .dialog-input {
+        flex: 1;
+        width: auto;
+      }
+    `;
+    document.head.appendChild(style);
+
+    return () => {
+      // 组件卸载时移除样式
+      const styleElement = document.getElementById('date-change-dialog-styles');
+      if (styleElement) {
+        document.head.removeChild(styleElement);
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    // 当对话框从显示变为隐藏时,重置状态
+    if (!show) {
+      setSelectedReason("");
+      setChangeValue("");
+    } else if (reasonSelectRef.current) {
+      // 当对话框显示时聚焦到下拉框
+      reasonSelectRef.current.focus();
+    }
+  }, [show]);
+
+  // 处理回车键逻辑
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === "Enter") {
+      if (selectedReason && changeValue) {
+        handleConfirm();
+      } else {
+        onClose();
+      }
+    }
+  };
+
+  // 处理确认按钮逻辑
+  const handleConfirm = () => {
+    if (selectedReason && changeValue) {
+      onConfirm(selectedReason, changeValue); // 将输入值传递给父组件
+      onClose(); // 关闭对话框
+    }
+  };
+
+  // 未显示时不渲染
+  if (!show) return null;
+
+  return (
+    <>
+      {/* 遮罩层,点击关闭对话框 */}
+      <div className="dialog-overlay" onClick={onClose} />
+      {/* 对话框主体 */}
+      <div className="custom-dialog">
+        <div className="dialog-group dialog-row">
+          <label className="dialog-label">事由</label>
+          <select
+            ref={reasonSelectRef}
+            value={selectedReason}
+            onChange={(e) => setSelectedReason(e.target.value)}
+            onKeyDown={handleKeyDown}
+            className="dialog-select"
+          >
+            <option value="">選択してください</option>
+            {reasonOptions.map((option) => (
+              <option key={option} value={option}>
+                {option}
+              </option>
+            ))}
+          </select>
+        </div>
+        <div className="dialog-group dialog-row">
+          <label className="dialog-label">変更後</label>
+          <input
+            type="text"
+            value={changeValue}
+            onChange={(e) => setChangeValue(e.target.value)}
+            onKeyDown={handleKeyDown}
+            className="dialog-input"
+          />
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default DateChangeDialog;

+ 810 - 0
src/pages/Base/Action/index.tsx

@@ -0,0 +1,810 @@
+"use client";
+
+import React, { useMemo, useState, useEffect } from "react";
+import { AgGridReact } from "ag-grid-react";
+import {
+    ClientSideRowModelModule,
+    ColDef,
+    ColGroupDef,
+    ModuleRegistry,
+    ValidationModule,
+    ICellRendererParams,
+    CellClassParams,
+    ValueFormatterParams,
+    CellClickedEvent,
+    ISelectCellEditorParams,
+    SelectEditorModule,
+    CellStyleModule
+} from "ag-grid-community";
+import "ag-grid-community/styles/ag-theme-alpine.css";
+import DateChangeDialog from "./DateChangeDialog";
+import Chat from "./Chat";
+
+// 注册必要的模块
+ModuleRegistry.registerModules([
+    ClientSideRowModelModule,
+    SelectEditorModule,
+    CellStyleModule,
+    ...(process.env.NODE_ENV!== "production"? [ValidationModule] : [])
+]);
+
+// 支店选项
+const branchOptions = [
+    "東京支店",
+    "神奈川支店",
+    "品川営業所",
+    "札幌支店",
+    "福岡支店"
+];
+
+// 事由选项
+const reasonOptions = [
+    "臨時変更",
+    "計画変更",
+    "休日調整",
+    "その他"
+];
+
+const orderInfo = {
+    deliveryPartner: {
+        id: "0311111111",
+        name: "AAA株式会社"
+    },
+    status: {
+        companyCode: "R000001",
+        status: "全承諾",
+        version: 1,
+        instructionDate: "2025/6/25",
+        approvalDate: "2025/6/25"
+    },
+    monthlyInstruction: {
+        versionOptions: ["ver-1", "ver-2", "ver-3", "ver-4", "ver-5"],
+        selectedVersion: "ver-1"
+    }
+};
+
+const historyData = [
+    {
+        type: '履歴',
+        date: '2025/06/16 10:00',
+        action: '指示書生成'
+    },
+    {
+        type: '履歴',
+        date: '2025/06/16 15:24',
+        action: '指示書編集'
+    },
+    {
+        type: '履歴',
+        date: '2025/06/16 15:24',
+        action: '指示書送付'
+    }
+];
+
+const weekTexts = ["火", "水", "木", "金", "土", "日", "月"];
+
+const GridExample = () => {
+    const containerStyle = useMemo(() => ({ width: "100%" }), []);
+    const gridStyle = useMemo(() => ({ width: "100%" }), []);
+    const [rowData, setRowData] = useState(initializeRowData());
+    const [showDialog, setShowDialog] = useState(false);
+    const [modifiedCells, setModifiedCells] = useState<Set<string>>(new Set());
+    const [selectedCell, setSelectedCell] = useState<{rowIndex: number | null, colId: string, currentValue: string} | null>(null);
+    const [cellReasons, setCellReasons] = useState<Record<string, string>>({});
+    const [showChat, setShowChat] = useState(false); // 控制聊天侧边栏显示的状态
+
+    // 初始化样式
+    useEffect(() => {
+        const style = document.createElement('style');
+        style.textContent = `
+      .ag-theme-alpine .header-center .ag-header-cell-label {
+        justify-content: center !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;
+      }
+
+      // .order-info-container {
+      //   margin-bottom: 20px;
+      //   padding: 15px;
+      //   border: 1px solid #dcdcdc;
+      //   border-radius: 4px;
+      // }
+      .order-info-section {
+        margin-bottom: 35px;
+      }
+      .order-info-section h3 {
+        margin: 0 0 5px 0;
+        font-size: 14px;
+        font-weight: bold;
+      }
+      .order-info-row {
+        margin: 10px 0 10px 20px;
+        font-size: 14px;
+      }
+      .status-section {
+        width: 100%;
+        border-collapse: collapse;
+        margin-top: 8px;
+        margin-left: 10px;
+      }
+      .status-section th,
+      .status-section td {
+        padding: 5px 10px;
+        text-align: left;
+        font-size: 14px;
+      }
+
+      .status-section th {
+        font-weight: normal;
+      }
+
+      .version-select-container {
+        display: flex;
+        align-items: center;
+        margin-left: 20px;
+      }
+      .version-select-container label {
+        margin-right: 10px;
+        white-space: nowrap;
+        font-size: 14px;
+      }
+      .version-select-container select {
+        padding: 5px 35px;
+        font-size: 14px;
+        margin-left: 20px;
+        padding-left: 10px;
+      }
+
+      .message-btn {
+        float: right;
+        padding: 7px 50px;
+        background-color: #000;
+        color: #fff;
+        border: none;
+        cursor: pointer;
+        position: relative;
+        top: -17px;
+      }
+      .edit-btn {
+        margin-left: auto;
+        padding: 7px 50px;
+        background-color: #000;
+        color: #fff;
+        border: none;
+        cursor: pointer;
+      }
+      .add-row-btn {
+        background: transparent;
+        border: none;
+        color: #0070c0;
+        cursor: pointer;
+        margin-left: 5px;
+      }
+      .action-btn {
+        background-color: #000;
+        color: #fff;
+        border: none;
+        padding: 7px 20px;
+        cursor: pointer;
+        font-size: 14px;
+      }
+      .history-table-custom {
+        width: 100%;
+        border-collapse: collapse;
+      }
+      .type-td {
+        padding: 5px 20px;
+      }
+      .date-td {
+        padding: 5px 20px;
+      }
+      .action-td {
+        padding: 5px 20px;
+      }
+      .history-title-custom {
+        font-size: 14px;
+        font-weight: bold;
+        margin-bottom: 8px;
+        display: block;
+      }
+      .ag-header-cell-resize {
+       &:after {
+        display: none;
+       }
+      }
+     .ag-theme-alpine .ag-cell {
+       border-right: 1px solid #dcdcdc;
+       text-align: center;
+     }
+     .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;
+     }
+     .checkbox{
+       display: none;
+     }
+     .tokyoBranchCol{
+       display: none;
+     }
+     .chat-sidebar {
+       position: fixed;
+       top: 0;
+       right: 0;
+       width: 1060px;
+       height: 100vh;
+       background-color: #fff;
+       box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
+       z-index: 1000;
+       display: flex;
+       flex-direction: column;
+       padding: 20px;
+       box-sizing: border-box;
+       overflow: hidden;
+     }
+     .chat-messages-container {
+       flex: 1;
+       overflow-y: auto;
+       margin-bottom: 16px;
+     }
+     .overlay {
+       position: fixed;
+       top: 0;
+       left: 0;
+       right: 0;
+       bottom: 0;
+       background-color: rgba(0, 0, 0, 0.5);
+       z-index: 999;
+     }
+    `;
+        document.head.appendChild(style);
+
+        return () => {
+            document.head.removeChild(style);
+        };
+    }, []);
+
+    // 生成1-31的日期列定义
+    const generateDateColumns = () => {
+        const columns: ColGroupDef[] = [];
+        for (let i = 1; i <= 31; i++) {
+            const weekIndex = (i - 1) % weekTexts.length;
+            const colId = `date_${i}`;
+            columns.push({
+                headerName: i.toString(),
+                headerClass: "header-center date-group-header",
+                children: [
+                    {
+                        field: colId,
+                        headerName: weekTexts[weekIndex],
+                        headerClass: "header-center",
+                        width: 55,
+                        cellRenderer: (params: ICellRendererParams) => {
+                            const cellValue = params.value || "";
+                            const rowIndex = params.node.rowIndex;
+                            const cellKey = `${rowIndex}-${colId}`;
+                            const reason = cellReasons[cellKey] || "";
+
+                            return (
+                                <div
+                                    title={reason? `变更理由:${reason}` : ""}
+                                    style={{
+                                        minHeight: '100%',
+                                        width: '100%',
+                                        display: 'flex',
+                                        alignItems: 'center',
+                                        justifyContent: 'center'
+                                    }}
+                                >
+                                    {cellValue}
+                                </div>
+                            );
+                        },
+                        onCellClicked: (params: CellClickedEvent) => {
+                            setSelectedCell({
+                                rowIndex: params.node.rowIndex,
+                                colId: colId,
+                                currentValue: params.value as string
+                            });
+                            setShowDialog(true);
+                        },
+                        cellStyle: (params: CellClassParams) => {
+                            const rowIndex = params.node.rowIndex;
+                            const cellKey = `${rowIndex}-${colId}`;
+                            const isModified = modifiedCells.has(cellKey);
+                            if (isModified) {
+                                return { backgroundColor: '#3a8aca' };
+                            }
+                            return null;
+                        }
+                    }
+                ]
+            });
+        }
+        return columns;
+    };
+
+
+    // 初始化行数据
+    function initializeRowData() {
+        return [
+            {
+                "AM": "",
+                "発": "032990",
+                "着": "025990",
+                "系統": "02",
+                "線便名": "大賀10t①",
+                "入庫場所": "羽田CGB",
+                "入庫時間": "21:00",
+                "使用車両(トン)": "13",
+                "checkboxCol": "",
+                "tokyoBranchCol": "東京支店",
+                "approveStatus": "approved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 3 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "",
+                "発": "032990",
+                "着": "027990",
+                "系統": "23",
+                "線便名": "大賀10t②",
+                "入庫場所": "桜丘",
+                "入庫時間": "19:00",
+                "使用車両(トン)": "13",
+                "checkboxCol": "",
+                "tokyoBranchCol": "神奈川支店",
+                "approveStatus": "unapproved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 4 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "*",
+                "発": "032990",
+                "着": "027990",
+                "系統": "G8",
+                "線便名": "大賀10t③",
+                "入庫場所": "目黒",
+                "入庫時間": "18:00",
+                "使用車両(トン)": "13",
+                "checkboxCol": "",
+                "tokyoBranchCol": "品川営業所",
+                "approveStatus": "approved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 5 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "",
+                "発": "032990",
+                "着": "027990",
+                "系統": "G9",
+                "線便名": "西大阪交表",
+                "入庫場所": "羽田CGB",
+                "入庫時間": "21:00",
+                "使用車両(トン)": "13",
+                "checkboxCol": "",
+                "tokyoBranchCol": "札幌支店",
+                "approveStatus": "unapproved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 2 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "",
+                "発": "061990",
+                "着": "032990",
+                "系統": "06",
+                "線便名": "羽田CG交裏",
+                "入庫場所": "西大阪B",
+                "入庫時間": "19:00",
+                "使用車両(トン)": "10",
+                "checkboxCol": "",
+                "tokyoBranchCol": "福岡支店",
+                "approveStatus": "approved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 6 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "",
+                "発": "032990",
+                "着": "032990",
+                "系統": "M8",
+                "線便名": "大賀10t⑥",
+                "入庫場所": "羽田CGB",
+                "入庫時間": "21:00",
+                "使用車両(トン)": "13",
+                "checkboxCol": "",
+                "tokyoBranchCol": "東京支店",
+                "approveStatus": "approved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 3 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "",
+                "発": "032990",
+                "着": "053990",
+                "系統": "06",
+                "線便名": "中部日祝",
+                "入庫場所": "成城",
+                "入庫時間": "19:00",
+                "使用車両(トン)": "13",
+                "checkboxCol": "",
+                "tokyoBranchCol": "神奈川支店",
+                "approveStatus": "unapproved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 4 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "*",
+                "発": "032990",
+                "着": "027990",
+                "系統": "G8",
+                "線便名": "大賀10t③",
+                "入庫場所": "目黒",
+                "入庫時間": "18:00",
+                "使用車両(トン)": "13",
+                "checkboxCol": "",
+                "tokyoBranchCol": "品川営業所",
+                "approveStatus": "approved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 5 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "",
+                "発": "032990",
+                "着": "132990",
+                "系統": "02",
+                "線便名": "大賀7t",
+                "入庫場所": "八幡山",
+                "入庫時間": "19:00",
+                "使用車両(トン)": "2",
+                "checkboxCol": "",
+                "tokyoBranchCol": "札幌支店",
+                "approveStatus": "unapproved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 2 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+            {
+                "AM": "",
+                "発": "061990",
+                "着": "032990",
+                "系統": "06",
+                "線便名": "羽田CG交裏",
+                "入庫場所": "西大阪B",
+                "入庫時間": "19:00",
+                "使用車両(トン)": "10",
+                "checkboxCol": "",
+                "tokyoBranchCol": "福岡支店",
+                "approveStatus": "approved",
+                ...Array.from({ length: 31 }, (_, i) => ({
+                    [`date_${i + 1}`]: (i + 1) % 6 === 0? "〇" : ""
+                })).reduce((acc, curr) => ({...acc,...curr }), {})
+            },
+        ];
+    }
+
+    // 构建列定义
+    const columnDefs: (ColDef | ColGroupDef)[] = [
+        {
+            field: "checkboxCol",
+            headerName: "",
+            width: 50,
+            cellRenderer: (params: ICellRendererParams) => {
+                return <input type="checkbox" />;
+            },
+            headerClass: "header-center checkbox",
+        },
+        {
+            field: "tokyoBranchCol",
+            headerName: "",
+            width: 110,
+            headerClass: "header-center tokyoBranchCol",
+            editable: true,
+            cellEditor: "agSelectCellEditor",
+            cellEditorParams: {
+                values: branchOptions
+            } as ISelectCellEditorParams,
+            valueFormatter: (params: ValueFormatterParams) => params.value || "",
+            cellRenderer: (params: ICellRendererParams) => {
+                return (
+                    <div style={{ textAlign: 'left'}}>
+                        {params.value || ''}
+                    </div>
+                );
+            }
+        },
+        {
+            field: "approveStatus",
+            headerName: "",
+            width: 130,
+            cellRenderer: (params: ICellRendererParams) => {
+                const status = params.data?.approveStatus;
+                const content = status === "approved"? (
+                    <span>
+        承認済
+        <a
+            href="#"
+            style={{
+                marginLeft: "5px",
+                color: "#3a8aca"
+            }}
+        >
+          差戻し
+        </a>
+      </span>
+                ) : status === "unapproved"? (
+                    <span>未承認</span>
+                ) : (
+                    <span></span>
+                );
+                return (
+                    <div style={{ textAlign: 'left'}}>
+                        {content}
+                    </div>
+                );
+            },
+            headerClass: "header-center"
+        },
+        {
+            field: "AM",
+            headerClass: "header-center",
+            width: 60,
+        },
+        {
+            headerName: "線便",
+            headerClass: "header-center line-group-header",
+            children: [
+                {
+                    field: "発",
+                    headerClass: "header-center",
+                    width: 85,
+                },
+                {
+                    field: "着",
+                    headerClass: "header-center",
+                    width: 85,
+                },
+                {
+                    field: "系統",
+                    headerClass: "header-center",
+                    width: 65,
+                },
+                {
+                    field: "線便名",
+                    headerClass: "header-center",
+                    width: 120,
+                },
+            ],
+        },
+        { field: "入庫場所", headerClass: "header-center", width: 110 },
+        { field: "入庫時間", headerClass: "header-center", width: 90 },
+        {
+            field: "使用車両(トン)",
+            headerClass: "header-center",
+            width: 125,
+            cellRenderer: (params: ICellRendererParams) => {
+                return (
+                    <div style={{ textAlign: 'right'}}>
+                        {params.value || ''}
+                    </div>
+                );
+            }
+        },
+        ...generateDateColumns()
+    ];
+
+
+    // 处理对话框确认
+    const handleDialogConfirm = (reason: string, changeValue: string) => {
+        if (!selectedCell || selectedCell.rowIndex === null) return;
+
+        const newRowData = [...rowData];
+        const currentValue = selectedCell.currentValue;
+        const newValue = currentValue === "〇"? "" : "〇";
+        const targetRowIndex = selectedCell.rowIndex;
+        const targetColId = selectedCell.colId;
+
+        newRowData[targetRowIndex] = {
+            ...newRowData[targetRowIndex],
+            [targetColId]: newValue
+        };
+        setRowData(newRowData);
+
+        const cellKey = `${targetRowIndex}-${targetColId}`;
+        setModifiedCells(prev => {
+            const newSet = new Set(prev);
+            newSet.add(cellKey);
+            return newSet;
+        });
+
+        setCellReasons(prev => ({
+            ...prev,
+            [cellKey]: reason
+        }));
+
+        setShowDialog(false);
+    };
+
+    // 处理对话框取消
+    const handleDialogCancel = () => {
+        setShowDialog(false);
+    };
+
+    // 处理消息按钮点击 - 切换聊天侧边栏显示状态
+    const handleMessageClick = () => {
+        setShowChat(true);
+    };
+
+    // 处理编辑按钮点击
+    const handleEditClick = () => {
+        alert('編集按钮被点击');
+    };
+
+    const handleAddRow = () => {
+        const newRow: any = {
+            "AM": "",
+            "発": "",
+            "着": "",
+            "系統": "",
+            "線便名": "",
+            "入庫場所": "",
+            "入庫時間": "",
+            "使用車両(トン)": "",
+            "checkboxCol": "",
+            "tokyoBranchCol": "",
+            "approveStatus": ""
+        };
+        for (let i = 1; i <= 31; i++) {
+            newRow[`date_${i}`] = "";
+        }
+
+        setRowData([...rowData, newRow]);
+    };
+
+    // 关闭聊天侧边栏
+    const closeChatSidebar = () => {
+        setShowChat(false);
+    };
+
+    return (
+        <div style={containerStyle}>
+            <div className="order-info-container">
+                <div className="order-info-section">
+                    <div style={{ fontSize: '14px' }}>発注詳細</div>
+                    <button className="message-btn" onClick={handleMessageClick}>
+                        メッセージ
+                    </button>
+                </div>
+
+                <div className="order-info-section">
+                    <h3>輸送パートナー </h3>
+                    <div className="order-info-row">
+                        輸送パートナーID:{orderInfo.deliveryPartner.id}
+                    </div>
+                    <div className="order-info-row">
+                        輸送パートナー名:{orderInfo.deliveryPartner.name}
+                    </div>
+                </div>
+
+                <div className="order-info-section">
+                    <h3>状態</h3>
+                    <table className="status-section">
+                        <thead>
+                        <tr>
+                            <th>会社コード</th>
+                            <th>ステータス</th>
+                            <th>version</th>
+                            <th>指示書送付</th>
+                            <th>承諾</th>
+                        </tr>
+                        </thead>
+                        <tbody>
+                        <tr>
+                            <td>{orderInfo.status.companyCode}</td>
+                            <td>{orderInfo.status.status}</td>
+                            <td>{orderInfo.status.version}</td>
+                            <td>{orderInfo.status.instructionDate}</td>
+                            <td>{orderInfo.status.approvalDate}</td>
+                        </tr>
+                        </tbody>
+                    </table>
+                </div>
+
+                <div className="order-info-section">
+                    <h3>月間運行指示書</h3>
+                    <div className="version-select-container">
+                        <label>version選択</label>
+                        <select value={orderInfo.monthlyInstruction.selectedVersion}>
+                            {orderInfo.monthlyInstruction.versionOptions.map(version => (
+                                <option key={version} value={version}>{version}</option>
+                            ))}
+                        </select>
+                        <button className="edit-btn" onClick={handleEditClick}>
+                            編集
+                        </button>
+                    </div>
+                </div>
+            </div>
+
+            <div style={gridStyle} className="ag-theme-alpine">
+                <AgGridReact
+                    rowData={rowData}
+                    columnDefs={columnDefs}
+                    domLayout="autoHeight"
+                />
+            </div>
+
+            <button className="add-row-btn" onClick={handleAddRow}>
+                +追加
+            </button>
+
+            <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '30px' }}>
+                <div>
+                    <button className="action-btn">変更を保存</button>
+                    <button className="action-btn" style={{ marginLeft: '10px' }}>輸送パートナーに送付する</button>
+                </div>
+                <button className="action-btn">代理承諾</button>
+            </div>
+
+            <div style={{ marginTop: '80px' }}>
+                <span className="history-title-custom">状態・アクション履歴</span>
+                <table className="history-table-custom">
+                    <tbody>
+                    {historyData.map((item, index) => (
+                        <tr key={index}>
+                            <td className="type-td">{item.type}</td>
+                            <td className="date-td">{item.date}</td>
+                            <td className="action-td">{item.action}</td>
+                        </tr>
+                    ))}
+                    </tbody>
+                </table>
+            </div>
+
+            {/* 使用封装的对话框组件 */}
+            <DateChangeDialog
+                show={showDialog}
+                onClose={handleDialogCancel}
+                onConfirm={handleDialogConfirm}
+                reasonOptions={reasonOptions}
+            />
+
+            {/* 聊天侧边栏 */}
+            {showChat && (
+                <>
+                    {/* 半透明背景遮罩 */}
+                    <div className="overlay" onClick={closeChatSidebar}></div>
+
+                    {/* 聊天侧边栏容器 */}
+                    <div className="chat-sidebar">
+                        <Chat onClose={closeChatSidebar} />
+                    </div>
+                </>
+            )}
+        </div>
+    );
+};
+
+export default GridExample;
+