kirin-ri / memo

0 stars 0 forks source link

mock fix #30

Open kirin-ri opened 2 months ago

kirin-ri commented 2 months ago

支出の選択欄の内容を調整し、 選択後に表示する文言は楽観:0.8倍、中立:相当、悲観:1.2倍にしたい。プルダウンの内容は既存のまま、もしくは調整してほしい。

          <div className="filter-group">
            <div className="filter-btn">支出</div>
            <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
              <option>楽観:前年度支出の0.8倍</option>
              <option>中立:前年度支出と相当</option>
              <option>悲観:前年度支出の1.2倍</option>
            </select>
          </div>
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(BarController, LineController, CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [incomeLevel, setIncomeLevel] = useState('中立');
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = () => {
    const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
    const selectedData = dataSets[key];

    if (!selectedData) {
      console.error(`No data found for key: ${incomeLevel}-${expenseLevel}`);
      return;
    }

    const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData.expense[i]);
    const balance = selectedData.balance || selectedData.income.reduce((acc: number[], income, i) => {
      let newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + income + selectedData.expense[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: selectedData.income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: selectedData.expense,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    if (selectedData.action) {
      setCashFlow(selectedData.action.cashFlow);
      setCountermeasure(selectedData.action.countermeasure);
    }

    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
  };

  useEffect(() => {
    updateChartAndAction();
  }, [incomeLevel, expenseLevel]);

  const handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setIncomeLevel(e.target.value);
  };

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setExpenseLevel(e.target.value);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10; // 第一行Y坐标

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観:0.8倍">楽観:前年度支出の0.8倍</option>
                  <option  value="中立:相当">中立:前年度支出と相当</option>
                  <option value="悲観:1.2倍">悲観:前年度支出の1.2倍</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}> {/* 全体のインデントをゼロに設定 */}
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance =10.0
const dataSets: { [key: string]: DataSet } = {
  "楽観-楽観": {
    income: [34.0, 35.5, 34.5, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0],
    expense: [-33.0, -34.0, -35.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0],
    incomeExpenseDiff: [1.0, 1.5, -0.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0],
    balance: [11.0, 12.5, 12.0, 14.0, 16.5, 19.5, 23.0, 27.0, 31.5, 36.5, 42.0, 48.0],
    action: {
      cashFlow: "昨年の残高推移は比較的安定していました。6月の当月収支がマイナスになっていて、収入が前年同期よりも若干低下しているが、支出の管理がより効率的であったため、全体としての残高推移は安定しています。",
      countermeasure: "-"
    }
  },
  "楽観-中立": {
    income: [34.0, 35.5, 34.5, 35.5, 36.0, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5, 39.0],
    expense: [-33.0, -34.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0],
    incomeExpenseDiff: [1.0, 1.5, -0.5, 0.5, 1.0, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
    balance: [11.0, 12.5, 12.0, 12.5, 13.5, 14.5, 16.0, 18.0, 20.5, 23.5, 27.0, 31.0],
    action: {
      cashFlow: "昨年は残高推移が年間を通して安定していました。今年の前半では、6月の当月収支がマイナスになっています。収入は前年並みで推移している一方で、支出がやや増加しています。",
      countermeasure: "-"
    }
  },
  "楽観-悲観": {
    income: [34.0, 35.5, 34.5, 35.0, 35.5, 36.0, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5],
    expense: [-33.0, -34.0, -35.0, -36.0, -36.5, -37.0, -37.5, -38.0, -38.5, -39.0, -39.5, -40.0],
    incomeExpenseDiff: [1.0, 1.5, -0.5, -1.0, -1.0, -1.0, -1.5, -1.5, -1.5, -1.5, -1.5, -1.5],
    balance: [11.0, 12.5, 12.0, 11.0, 10.0, 9.0, 7.5, 6.0, 4.5, 3.0, 1.5, 0.0],
    action: {
      cashFlow: "昨年は収入が安定していたものの、設備投資や労務費の増加により、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。特に支出が収入を上回るリスクが高まっています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
  "中立-楽観": {
    income: [34.0, 35.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5],
    expense: [-33.0, -34.0, -35.0, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5, -29.0],
    incomeExpenseDiff: [1.0, 1.5, -0.5, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5],
    balance: [11.0, 12.5, 12.0, 13.5, 15.5, 18.0, 21.0, 24.5, 28.5, 33.0, 38.0, 43.5],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立-中立": {
    income: [34.0, 35.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5],
    expense: [-33.0, -34.0, -35.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0],
    incomeExpenseDiff: [1.0, 1.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5],
    balance: [11.0, 12.5, 12.0, 12.5, 13.0, 13.5, 14.0, 14.5, 15.0, 15.5, 16.0, 16.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "中立-悲観": {
    income: [34.0, 35.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5],
    expense: [-33.0, -34.0, -35.0, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    incomeExpenseDiff: [1.0, 1.5, -0.5, -1.5, -2.5, -3.5, -4.5, -5.5, -6.5, -7.5, -8.5, -9.5],
    balance: [11.0, 12.5, 12.0, 10.5, 8.0, 4.5, 0.0, -5.5, -12.0, -19.5, -28.0, -37.5],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
  "悲観-楽観": {
    income: [34.0, 35.5, 34.5, 33.5, 33.0, 32.5, 32.0, 31.5, 31.0, 30.5, 30.0, 29.5],
    expense: [-33.0, -34.0, -35.0, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    incomeExpenseDiff: [1.0, 1.5, -0.5, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5],
    balance: [11.0, 12.5, 12.0, 12.5, 13.5, 15.0, 17.0, 19.5, 22.5, 26.0, 30.0, 34.5],
    action: {
      cashFlow: "昨年は収入の減少と支出の抑制により、残高推移が後半にかけて改善されました。今年の前半では収入の減少が続き、支出の抑制も限界に達しているため、6月の当月収支がマイナスになっています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n計画を見直し、過剰在庫を削減することで資金を効率的に運用\n短期および中長期の追加融資\n投資家からの資金調達など、手段の多様化"
    }
  },
  "悲観-中立": {
    income: [34.0, 35.5, 34.5, 33.0, 32.5, 32.0, 31.5, 31.0, 30.5, 30.0, 29.5, 29.0],
    expense: [-33.0, -34.0, -35.0, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    incomeExpenseDiff: [1.0, 1.5, -0.5, -1.5, -2.0, -2.5, -3.0, -3.5, -4.0, -4.5, -5.0, -5.5],
    balance: [11.0, 12.5, 12.0, 10.5, 8.5, 6.0, 3.0, -0.5, -4.5, -9.0, -14.0, -19.5],
    action: {
      cashFlow: "昨年の残高推移は、支出の抑制により後半にかけて若干の改善が見られましたが、今年の前半では収入の減少が続き、支出の増加が懸念されています。6月の当月収支がマイナスになっていて、残高推移の悪化に注意が必要です。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n現金回収を早めるため、請求の迅速化\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
  "悲観-悲観": {
    income: [34.0, 35.5, 34.5, 33.0, 32.5, 32.0, 31.5, 31.0, 30.5, 30.0, 29.5, 29.0],
    expense: [-33.0, -34.0, -35.0, -34.5, -35.0, -35.5, -36.0, -36.5, -37.0, -37.5, -38.0, -38.5],
    incomeExpenseDiff: [1.0, 1.5, -0.5, -1.5, -2.5, -3.5, -4.5, -5.5, -6.5, -7.5, -8.5, -9.5],
    balance: [11.0, 12.5, 12.0, 10.5, 8.0, 4.5, 0.0, -5.5, -12.0, -19.5, -28.0, -37.5],
    action: {
      cashFlow: "昨年は支出の増加により残高推移が悪化し、今年の前半でも同様の傾向が見られます。収入の減少と支出の増加が重なり、6月の当月収支がマイナスになっていて、残高推移が非常に厳しい状況です。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n現金回収を早めるため、請求の迅速化\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  }
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];
const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
収入の予測はいらなくなったので、関連の資材を全部消したい。
また、支出の選択欄の更新をお願いしたい。
完全のコードをください。
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <p className="forecast-instruction">
            支出予測値を入力してください。<br />
            空欄の場合、予測値は推定されます。
          </p>
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5</span>
              <span>5月</span>
              <span>-14</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19</span>
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
            </div>
            <div className="row">
              <span>8月</span>
              <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
            </div>
            <div className="row">
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
            </div>
            <div className="row">
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
            </div>
            <div className="row">
              <span>2月</span>
              <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const defaultIncome = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const defaultExpense = [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5]; // デフォルトの支出データを設定
    updateChartAndAction(defaultIncome, defaultExpense, false);
  }, [expenseLevel]);

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setExpenseLevel(e.target.value);

    // ダミーデータを使用してグラフと推奨アクションを更新
    const selectedData = dataSets[e.target.value];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    // カスタム予測を閉じる
    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  const selectedLevel = e.target.value;
  setExpenseLevel(selectedLevel);

  const selectedData = dataSets[selectedLevel];
  if (selectedData) {
    updateChartAndAction(selectedData.income, selectedData.expense, false);
  }

  setIsCustomForecastOpen(false);
};
kirin-ri commented 2 months ago
useEffect(() => {
  const selectedData = dataSets[expenseLevel];
  if (selectedData) {
    updateChartAndAction(selectedData.income, selectedData.expense, false);
  }
}, [expenseLevel]);
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <p className="forecast-instruction">
            支出予測値を入力してください。<br />
            空欄の場合、予測値は推定されます。
          </p>
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5</span>
              <span>5月</span>
              <span>-14</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19</span>
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
            </div>
            <div className="row">
              <span>8月</span>
              <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
            </div>
            <div className="row">
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
            </div>
            <div className="row">
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
            </div>
            <div className="row">
              <span>2月</span>
              <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const defaultIncome = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const defaultExpense = [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5]; // デフォルトの支出データを設定
    updateChartAndAction(defaultIncome, defaultExpense, false);
  }, []);

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }
  }, [expenseLevel]);

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const selectedLevel = e.target.value;
    setExpenseLevel(selectedLevel);

    const selectedData = dataSets[selectedLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <p className="forecast-instruction">
            支出予測値を入力してください。<br />
            空欄の場合、予測値は推定されます。
          </p>
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5</span>
              <span>5月</span>
              <span>-14</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19</span>
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
            </div>
            <div className="row">
              <span>8月</span>
              <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
            </div>
            <div className="row">
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
            </div>
            <div className="row">
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
            </div>
            <div className="row">
              <span>2月</span>
              <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const defaultIncome = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const defaultExpense = [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5]; // デフォルトの支出データを設定
    updateChartAndAction(defaultIncome, defaultExpense, false);
  }, []);

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }
  }, [expenseLevel]);

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const selectedLevel = e.target.value;
    setExpenseLevel(selectedLevel);

    const selectedData = dataSets[selectedLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

// Chart.jsのコンポーネントを登録
ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

// 型定義
type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

// 折りたたみパネルのコンポーネント
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

// アラートボックスのコンポーネント
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

// カスタム支出予測コンポーネント
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <p className="forecast-instruction">
            支出予測値を入力してください。<br />
            空欄の場合、予測値は推定されます。
          </p>
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5</span>
              <span>5月</span>
              <span>-14</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19</span>
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
            </div>
            <div className="row">
              <span>8月</span>
              <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
            </div>
            <div className="row">
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
            </div>
            <div className="row">
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
            </div>
            <div className="row">
              <span>2月</span>
              <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

// メインのページコンポーネント
const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const defaultIncome = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const defaultExpense = [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5];
    updateChartAndAction(defaultIncome, defaultExpense, false);
  }, []);

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }
  }, [expenseLevel]);

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const selectedLevel = e.target.value;
    setExpenseLevel(selectedLevel);

    const selectedData = dataSets[selectedLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

// キャッシュフロー詳細データ
const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  const newExpenseLevel = e.target.value;
  setExpenseLevel(newExpenseLevel);

  // グラフと推奨アクションを更新
  const selectedData = dataSets[newExpenseLevel];
  if (selectedData) {
    updateChartAndAction(selectedData.income, selectedData.expense, false);
  }

  // カスタム予測を閉じる
  setIsCustomForecastOpen(false);
};
kirin-ri commented 2 months ago
useEffect(() => {
  const selectedData = dataSets[expenseLevel];  // 選択されたデータセットを取得
  if (selectedData) {
    updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
  }
}, [expenseLevel]); // `expenseLevel`を依存関係として追加
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <p className="forecast-instruction">
            支出予測値を入力してください。<br />
            空欄の場合、予測値は推定されます。
          </p>
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5</span>
              <span>5月</span>
              <span>-14</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19</span>
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
            </div>
            <div className="row">
              <span>8月</span>
              <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
            </div>
            <div className="row">
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
            </div>
            <div className="row">
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
            </div>
            <div className="row">
              <span>2月</span>
              <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];  // 選択されたデータセットを取得
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
    }
  }, [expenseLevel]); // `expenseLevel`を依存関係として追加

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const newExpenseLevel = e.target.value;
    setExpenseLevel(newExpenseLevel);

    // グラフと推奨アクションを更新
    const selectedData = dataSets[newExpenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    // カスタム予測を閉じる
    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
import { useState, useEffect, useRef } from "react";
import { Chart } from 'react-chartjs-2';
import {
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend,
  Chart as ChartJS,
} from 'chart.js';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

const EmptyPage = () => {
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const [showReferenceWindow, setShowReferenceWindow] = useState(false);
  const [windowPosition, setWindowPosition] = useState({ x: 100, y: 100 });
  const chartRef = useRef<ChartJS | null>(null);
  const windowRef = useRef<HTMLDivElement | null>(null);

  // Function to handle window movement
  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    const startX = e.clientX - windowPosition.x;
    const startY = e.clientY - windowPosition.y;

    const handleMouseMove = (moveEvent: MouseEvent) => {
      setWindowPosition({
        x: moveEvent.clientX - startX,
        y: moveEvent.clientY - startY,
      });
    };

    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };

    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  // Render Movable Window
  const renderReferenceWindow = () => (
    <div
      className="reference-window"
      style={{ top: windowPosition.y, left: windowPosition.x }}
      ref={windowRef}
      onMouseDown={handleMouseDown}
    >
      <div className="window-header">
        参考資料
        <button onClick={() => setShowReferenceWindow(false)}>✖</button>
      </div>
      <div className="window-content">
        {/* Add any content you want here */}
        <p>ここに参考資料の内容を追加できます。</p>
      </div>
    </div>
  );

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>資金繰り表</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        {/* Your existing content */}
      </div>
      <button
        className="reference-btn"
        onClick={() => setShowReferenceWindow(true)}
        style={{ position: 'fixed', bottom: '20px', right: '20px' }}
      >
        参考資料を開く
      </button>
      {showReferenceWindow && renderReferenceWindow()}
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 2 months ago
.reference-btn {
  background-color: #007bff;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 5px;
  cursor: pointer;
}

.reference-window {
  position: fixed;
  width: 300px;
  height: 200px;
  background-color: white;
  border: 1px solid #ddd;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  z-index: 1000;
  cursor: move;
}

.window-header {
  background-color: #f0f0f0;
  padding: 10px;
  cursor: grab;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.window-header button {
  background: none;
  border: none;
  font-size: 1.2em;
  cursor: pointer;
}

.window-content {
  padding: 15px;
}
kirin-ri commented 2 months ago
import { useState, useEffect, useRef } from "react";
import { Chart } from 'react-chartjs-2';
import {
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend,
  Chart as ChartJS,
} from 'chart.js';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const [showReferenceWindow, setShowReferenceWindow] = useState(false);
  const [windowPosition, setWindowPosition] = useState({ x: 100, y: 100 });
  const chartRef = useRef<ChartJS | null>(null);
  const windowRef = useRef<HTMLDivElement | null>(null);

  // Function to handle window movement
  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    const startX = e.clientX - windowPosition.x;
    const startY = e.clientY - windowPosition.y;

    const handleMouseMove = (moveEvent: MouseEvent) => {
      setWindowPosition({
        x: moveEvent.clientX - startX,
        y: moveEvent.clientY - startY,
      });
    };

    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };

    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  // Render Movable Window
  const renderReferenceWindow = () => (
    <div
      className="reference-window"
      style={{ top: windowPosition.y, left: windowPosition.x }}
      ref={windowRef}
      onMouseDown={handleMouseDown}
    >
      <div className="window-header">
        参考資料
        <button onClick={() => setShowReferenceWindow(false)}>✖</button>
      </div>
      <div className="window-content">
        {/* Add any content you want here */}
        <p>ここに参考資料の内容を追加できます。</p>
      </div>
    </div>
  );

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];  // 選択されたデータセットを取得
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
    }
  }, [expenseLevel]); // `expenseLevel`を依存関係として追加

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const newExpenseLevel = e.target.value;
    setExpenseLevel(newExpenseLevel);

    // グラフと推奨アクションを更新
    const selectedData = dataSets[newExpenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    // カスタム予測を閉じる
    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
      <button
        className="reference-btn"
        onClick={() => setShowReferenceWindow(true)}
        style={{ position: 'fixed', bottom: '20px', right: '20px' }}
      >
        参考資料を開く
      </button>
      {showReferenceWindow && renderReferenceWindow()}
    </div>
  );
};
kirin-ri commented 2 months ago
 const handleButtonClick = () => {
    if (buttonRef.current) {
      const rect = buttonRef.current.getBoundingClientRect();
      setWindowPosition({
        x: rect.left,
        y: rect.top - 200, // 窗口的高度假设为200px
      });
      setShowReferenceWindow(true);
    }
  };
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <p className="forecast-instruction">
            支出予測値を入力してください。<br />
            空欄の場合、予測値は推定されます。
          </p>
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5</span>
              <span>5月</span>
              <span>-14</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19</span>
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
            </div>
            <div className="row">
              <span>8月</span>
              <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
            </div>
            <div className="row">
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
            </div>
            <div className="row">
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
            </div>
            <div className="row">
              <span>2月</span>
              <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);
  const [showReferenceWindow, setShowReferenceWindow] = useState(false);
  const [windowPosition, setWindowPosition] = useState({ x: 0, y:0 });
  const windowRef = useRef<HTMLDivElement | null>(null);

    // Function to handle window movement
    const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
      const startX = e.clientX - windowPosition.x;
      const startY = e.clientY - windowPosition.y;

      const handleMouseMove = (moveEvent: MouseEvent) => {
        setWindowPosition({
          x: moveEvent.clientX - startX,
          y: moveEvent.clientY - startY,
        });
      };

      const handleMouseUp = () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };

      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    };

    // Render Movable Window
    const renderReferenceWindow = () => (
      <div
        className="reference-window"
        style={{ top: windowPosition.y, left: windowPosition.x }}
        ref={windowRef}
        onMouseDown={handleMouseDown}
      >
        <div className="window-header">
          参考資料
          <button onClick={() => setShowReferenceWindow(false)}>✖</button>
        </div>
        <div className="window-content">
          {/* Add any content you want here */}
          <p>ここに参考資料の内容を追加できます。</p>
        </div>
      </div>
    );

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];  // 選択されたデータセットを取得
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
    }
  }, [expenseLevel]); // `expenseLevel`を依存関係として追加

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const newExpenseLevel = e.target.value;
    setExpenseLevel(newExpenseLevel);

    // グラフと推奨アクションを更新
    const selectedData = dataSets[newExpenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    // カスタム予測を閉じる
    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
      <button
        className="reference-btn"
        onClick={() => setShowReferenceWindow(true)}
        style={{ position: 'fixed', bottom: '20px', right: '20px' }}
      >
        ?
      </button>
      {showReferenceWindow && renderReferenceWindow()}
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
  const handleButtonClick = () => {
    if (buttonRef.current) {
      const rect = buttonRef.current.getBoundingClientRect();
      setWindowPosition({
        x: rect.left,
        y: rect.top, // 窗口的初始y位置与按钮一致
      });
      setShowReferenceWindow(true); // 显示小窗并隐藏按钮
    }
  };
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:245:11
TS2304: Cannot find name 'buttonRef'.
    243 |
    244 |     const handleButtonClick = () => {
  > 245 |       if (buttonRef.current) {
        |           ^^^^^^^^^
    246 |         const rect = buttonRef.current.getBoundingClientRect();
    247 |         setWindowPosition({
    248 |           x: rect.left,

ERROR in src/components/pages/financingPage.tsx:246:22
TS2304: Cannot find name 'buttonRef'.
    244 |     const handleButtonClick = () => {
    245 |       if (buttonRef.current) {
  > 246 |         const rect = buttonRef.current.getBoundingClientRect();
        |                      ^^^^^^^^^
    247 |         setWindowPosition({
    248 |           x: rect.left,
    249 |           y: rect.top, // 窗口的初始y位置与按钮一致
kirin-ri commented 2 months ago
      {/* 仅在showReferenceWindow为false时显示按钮 */}
      {!showReferenceWindow && (
        <button
          ref={buttonRef}
          className="reference-btn"
          onClick={handleButtonClick}
          style={{ position: 'fixed', bottom: '20px', right: '20px' }}
        >
          ?
        </button>
      )}

      {/* 显示小窗 */}
      {showReferenceWindow && renderReferenceWindow()}
kirin-ri commented 2 months ago

const [showSidebar, setShowSidebar] = useState(false);

kirin-ri commented 2 months ago
      {/* 右上角的小按钮 */}
      <button
        className="tiny-button"
        onClick={handleSidebarToggle}
        style={{ position: 'fixed', top: '10px', right: '10px', width: '30px', height: '30px', fontSize: '14px' }}
      >
        ☰
      </button>

      {/* 侧边栏 */}
      {showSidebar && (
        <div className="sidebar">
          <div className="sidebar-header">
            <button onClick={handleSidebarToggle}>✖</button>
          </div>
          <div className="sidebar-content">
            <h2>参考内容</h2>
            <p>这里是参考内容的示例,可以根据需要进行更改。</p>
          </div>
        </div>
      )}
    </div>
kirin-ri commented 2 months ago
  .sidebar {
    position: fixed;
    right: 0;
    top: 0;
    width: 300px;
    height: 100%;
    background-color: white;
    border-left: 1px solid #ccc;
    box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
    z-index: 1000;
    padding: 20px;
  }

  .sidebar-header {
    display: flex;
    justify-content: flex-end;
  }

  .tiny-button {
    cursor: pointer;
    border: none;
    background: #007bff;
    color: white;
    border-radius: 50%;
  }
kirin-ri commented 2 months ago
  const handleSidebarToggle = () => {
    setShowSidebar(!showSidebar); // 切换侧边栏的显示状态
  };
kirin-ri commented 2 months ago
  <button
    className="tiny-button"
    onClick={handleSidebarToggle}
    style={{ position: 'absolute', top: '10px', right: '10px', width: '30px', height: '30px', fontSize: '14px' }}
  >
    ☰
  </button>

  {/* 侧边栏,放置在 main-content 内 */}
  {showSidebar && (
    <div className="sidebar" style={{
      position: 'absolute', // 相对于 main-content 进行绝对定位
      right: '0', // 靠右显示
      top: '0',   // 顶部对齐
      height: '100%', // 高度占满 main-content
      width: '250px', // 可以根据需要调整侧边栏宽度
      backgroundColor: 'white',
      borderLeft: '1px solid #ccc',
      boxShadow: '-2px 0 5px rgba(0, 0, 0, 0.1)',
      zIndex: 1000,
      padding: '20px'
    }}>
      <div className="sidebar-header">
        <button onClick={handleSidebarToggle}>✖</button>
      </div>
      <div className="sidebar-content">
        <h2>参考内容</h2>
        <p>这里是参考内容的示例,可以根据需要进行更改。</p>
      </div>
    </div>
  )}
kirin-ri commented 2 months ago
const calculateAverage = (values) => {
  const validValues = values.filter(value => value !== '' && value !== '-');
  const sum = validValues.reduce((acc, val) => acc + parseFloat(val), 0);
  return validValues.length ? sum / validValues.length : 0;
};
kirin-ri commented 2 months ago
const handleUpdate = () => {
  const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
  const baseExpense = [-9.5, -14.0, -19.0];

  // 计算输入部分的平均值
  const avgExpense = calculateAverage(Object.values(customExpenses));

  const updatedExpenses = [
    ...baseExpense.map(value => value + avgExpense), // 将基本的-9.5, -14, -19加上平均值
    ...Object.keys(customExpenses).map(month => {
      const value = customExpenses[month as keyof CustomExpenses];
      return value !== '' ? parseFloat(value) : estimateExpense();
    })
  ];

  // 使用更新后的数据更新图表和推荐行动
  updateChartAndAction(income, updatedExpenses, true);

  // 滚动到图表显示区域
  if (chartRef.current && chartRef.current.canvas) {
    const parentNode = chartRef.current.canvas.parentNode as Element;
    parentNode.scrollIntoView({ behavior: 'smooth' });
  }
};
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム入力</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>  -9.5 百万円</span>
              <span>5月</span>
              <span>  -14 百万円</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>  -19 百万円</span>
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
            </div>
            <div className="row">
              <span>8月</span>
              <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
            </div>
            <div className="row">
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
            </div>
            <div className="row">
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
            </div>
            <div className="row">
              <span>2月</span>
              <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];  // 選択されたデータセットを取得
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
    }
  }, [expenseLevel]); // `expenseLevel`を依存関係として追加

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const newExpenseLevel = e.target.value;
    setExpenseLevel(newExpenseLevel);

    // グラフと推奨アクションを更新
    const selectedData = dataSets[newExpenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    // カスタム予測を閉じる
    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
          <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
.content-wrapper {
  padding: 0; /* 确保没有内边距 */
}

.page-cover {
  margin: 0; /* 确保没有外边距 */
}

.page-cover-title-frame {
  margin: 0; /* 确保没有外边距 */
  padding: 0; /* 确保没有内边距 */
}

.alert-container {
  padding: 0;
  margin: 0;
}

.alert-box {
  background-color: white;
  color: black;
  border: 1px solid #ccc;
  border-radius: 5px;
  padding: 10px;
  margin: 0; /* 确保没有外边距 */
  display: flex;
  justify-content: space-between; /* 确保按钮在右侧 */
  align-items: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  position: relative;
}

.alert-content {
  display: flex;
  align-items: center;
  justify-content: center; /* 确保内容居中 */
  flex-grow: 1;
}

.alert-icon {
  color: red;
  font-size: 2rem; /* 调整感叹号的大小 */
  margin-right: 10px; /* 感叹号与文字之间的间距 */
}

.alert-message {
  text-align: center; /* 文本居中对齐 */
  font-size: 1.2rem;
}

.close-btn {
  background-color: transparent;
  border: 1px solid red;
  border-radius: 15px; /* 椭圆形 */
  color: red;
  cursor: pointer;
  font-size: 1rem;
  padding: 5px 10px;
  margin-left: 10px;
}

.main-content {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  margin-top: 20px;
  padding-left: 20px; /* 增加左侧边距,避免图表贴边 */
  box-sizing: border-box; /* 确保内外边距包含在宽度内 */
}

.left-container {
  flex: 1;
  max-width: 48%; /* 图表宽度 */
  height: auto; /* 图表高度 */
  padding: 20px;
  margin-left: 2%;
  border-radius: 5px;
}

.graph-container {
  background-color: #f9f9f9;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  padding: 30px;
  border: 1px solid #000;
}

.right-container {
  flex: 1;
  max-width: 48%;
  padding: 20px;
  border-radius: 5px;
  margin-right: 2%;
}

.actionbox-title {
  text-align: center;
  background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
  border-radius: 5px;
  font-size: 1rem;
  color: white;
  padding: 5px;
}

.actionbox-message{
  background-color:  #f1f1f1;
  color: #000;
  font-size: 1rem;
  height:278px;
  overflow-y: auto;
  padding: 20px;
}

.actionbox-message::-webkit-scrollbar {
  width: 12px;
}

.actionbox-message::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 10px;
}

.actionbox-message::-webkit-scrollbar-thumb {
  background-color: #888;
  border-radius: 10px;
  border: 3px solid #f1f1f1;
}

.actionbox-message::-webkit-scrollbar-thumb:hover {
  background: #555;
}
.action-section h3{
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 10px;
}

.collapsible-panels {
  margin-top: 30px;
  padding: 20px;
  background-color: #f9f9f9;
}

.collapsible-panel {
  margin-bottom: 10px;
  width: 100%; /* 确保折叠面板宽度填满容器 */
}

.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  background-color: #f1f1f1;
  cursor: pointer;
  border-radius: 5px;
}

.panel-title {
  font-weight: bold;
  font-size: 1.2rem;
}

.panel-money {
  font-weight: bold;
  margin-left: auto;
  font-size: 1.2rem;
}

.panel-content {
  padding: 10px;
  background-color: #ffffff;
  border: 1px solid #f1f1f1;
  border-radius: 0 0 5px 5px;
}

.details-container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  width: fit-content;
  gap: 10px;
}

.detail-item {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

.detail-item span {
  margin-right: 5px;
  text-align: left;
  white-space: nowrap;
}

/* 新增样式 */
.additional-section {
  display: flex;
  margin-top: 10px;
}

.data-filter, .data-comparison {
  height: auto;
  width: 50%;
  padding: 20px;
}

.data-filter h2, .data-comparison h2 {
  margin-bottom: 20px;
  text-align: center;
  font-size: 1rem; /* 调整字体大小 */
  color: white; /* 白色字体 */
  background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
  padding: 10px; /* 增加内边距以确保背景颜色显示为长方形 */
  border-radius: 5px; /* 圆角 */
}

.filter-group, .comparison-group {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.comparison-btn {
  width: 30%;
  padding: 10px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1rem;
}

.filter-btn {
  width: 40%;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 5px;
  font-size: 0.9rem;
  text-align: center;
  margin-right: 10px;
  white-space: nowrap;
}

.filter-select {
  width: 60%;
  padding: 10px;
  border: 1px solid #ccc;
  background-color: white;
  border-radius: 20px;
  font-size: 1rem;
  text-align: center;
}

.comparison-btn {
  width: 100%;
  margin-bottom: 10px;
  border-radius: 20px;
  background-color: #f1f1f1;
  color: black;
}

.comparison-btn.active {
  background-color: #3498db;
}
.custom-expense-forecast {
  width: 100%;
}

.custom-expense-btn {
  width: 40%;
  margin-left: 20px;
  padding: 5px;
  background-color: #3498db;
  border-radius: 10px;
  color: white;
  border: none;
  cursor: pointer;
  font-size: 16px;
}

.custom-expense-btn:hover {
  background-color: #007bff;
}

.form-container {
  width: 90%;
  margin-left: 20px;
  background-color: white;
  padding: 15px;
  border-radius: 5px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.update-btn {
  width: 100%;
  padding: 2px 1px;
  background-color: #28a745;
  border-radius: 10px;
  color: white;
  border: none;
  cursor: pointer;
  margin-top: 10px;
  font-size: 16px;
}

.update-btn:hover {
  background-color: #218838;
}

.form-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.row span {
  flex: 1; /* 各要素が均等に分かれる */
  text-align: center; /* テキストを中央に配置 */
}

.row input {
  max-width: 10%; /* 入力欄の幅を枠の1/4に制限 */
  text-align: center;
}

.forecast-instruction {
  margin-bottom: 10px;
  color: #333;
  font-size: 12px;
  line-height: 1.5;
}
kirin-ri commented 2 months ago
.form-content {
  display: flex;
  flex-wrap: wrap; /* 要素を複数行に配置 */
  gap: 10px; /* 要素間のスペースを確保 */
  justify-content: space-between; /* 左右に要素を配置 */
}

.row {
  display: flex;
  align-items: center;
  width: 48%; /* 左右に2列表示するための幅調整 */
  gap: 10px;
  margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}

.row span {
  flex: 1; /* 各要素が均等に分かれる */
  text-align: center; /* テキストを中央に配置 */
}

.row input {
  flex: 1; /* 入力欄が残りのスペースを占有 */
  text-align: center;
}
kirin-ri commented 2 months ago
<div className="form-container">
  <div className="form-content">
    <div className="row">
      <span>4月</span>
      <span>-9.5 百万円</span>
    </div>
    <div className="row">
      <span>5月</span>
      <span>-14 百万円</span>
    </div>
    <div className="row">
      <span>6月</span>
      <span>-19 百万円</span>
    </div>
    <div className="row">
      <span>7月</span>
      <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>8月</span>
      <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>9月</span>
      <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>10月</span>
      <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>11月</span>
      <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>12月</span>
      <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>1月</span>
      <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>2月</span>
      <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>3月</span>
      <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
    </div>
  </div>
  <button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
kirin-ri commented 2 months ago
<div className="form-container">
  <div className="form-content">
    <div className="row">
      <span>4月</span>
      <span>-9.5 百万円</span>
    </div>
    <div className="row">
      <span>5月</span>
      <span>-14 百万円</span>
    </div>
    <div className="row">
      <span>6月</span>
      <span>-19 百万円</span>
    </div>
    <div className="row">
      <span>7月</span>
      <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>8月</span>
      <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>9月</span>
      <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>10月</span>
      <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>11月</span>
      <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>12月</span>
      <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>1月</span>
      <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>2月</span>
      <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>3月</span>
      <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
    </div>
  </div>
  <button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
kirin-ri commented 2 months ago
.form-content {
  display: flex;
  flex-wrap: wrap; /* 要素を複数行に配置 */
  gap: 10px; /* 要素間のスペースを確保 */
  justify-content: space-between; /* 左右に要素を配置 */
}

.row {
  display: flex;
  align-items: center;
  width: 48%; /* 左右に2列表示するための幅調整 */
  gap: 10px;
  margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}

.row span {
  flex: 1; /* 各要素が均等に分かれる */
  text-align: center; /* テキストを中央に配置 */
}

.row input {
  width: 60px; /* 入力欄の幅を指定して短くする */
  text-align: center;
}
kirin-ri commented 2 months ago
.form-content {
  display: flex;
  flex-wrap: wrap; /* 要素を複数行に配置 */
  gap: 10px; /* 要素間のスペースを確保 */
  justify-content: space-between; /* 左右に要素を配置 */
}

.row {
  display: flex;
  align-items: center;
  width: 48%; /* 左右に2列表示するための幅調整 */
  gap: 10px;
  margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}

.row span {
  width: 40px; /* 「月」の幅を固定して揃える */
  text-align: center; /* テキストを中央に配置 */
}

.row input {
  width: 60px; /* 入力欄の幅を固定 */
  text-align: center;
}

.amount-text {
  width: 80px; /* 「百万円」の幅を固定して揃える */
  text-align: left; /* テキストを左揃え */
}
kirin-ri commented 2 months ago
<div className="form-container">
  <div className="form-content">
    <div className="row">
      <span>4月</span>
      <span className="amount-text">-9.5 百万円</span>
    </div>
    <div className="row">
      <span>5月</span>
      <span className="amount-text">-14 百万円</span>
    </div>
    <div className="row">
      <span>6月</span>
      <span className="amount-text">-19 百万円</span>
    </div>
    <div className="row">
      <span>7月</span>
      <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>8月</span>
      <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>9月</span>
      <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>10月</span>
      <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>11月</span>
      <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>12月</span>
      <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>1月</span>
      <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>2月</span>
      <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>3月</span>
      <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
  </div>
  <button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
kirin-ri commented 2 months ago
.form-content {
  display: flex;
  flex-wrap: wrap; /* 要素を複数行に配置 */
  gap: 10px; /* 要素間のスペースを確保 */
  justify-content: space-between; /* 左右に要素を配置 */
}

.row {
  display: flex;
  align-items: center;
  width: 48%; /* 左右に2列表示するための幅調整 */
  gap: 10px;
  margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}

.row span {
  width: 40px; /* 「月」の幅を固定して揃える */
  text-align: center; /* テキストを中央に配置 */
}

.row input {
  width: 60px; /* 入力欄の幅を固定 */
  text-align: center;
}

.amount-text {
  width: 80px; /* 「百万円」の幅を固定して揃える */
  text-align: left; /* テキストを左揃え */
}
kirin-ri commented 2 months ago
<div className="form-container">
  <div className="form-content">
    {/* 左側(4月から9月) */}
    <div className="row">
      <span>4月</span>
      <input type="text" value={customExpenses['4月']} onChange={(e) => handleExpenseChange('4月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>5月</span>
      <input type="text" value={customExpenses['5月']} onChange={(e) => handleExpenseChange('5月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>6月</span>
      <input type="text" value={customExpenses['6月']} onChange={(e) => handleExpenseChange('6月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>7月</span>
      <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>8月</span>
      <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>9月</span>
      <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>

    {/* 右側(10月から3月) */}
    <div className="row">
      <span>10月</span>
      <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>11月</span>
      <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>12月</span>
      <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>1月</span>
      <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>2月</span>
      <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>3月</span>
      <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
  </div>
  <button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
kirin-ri commented 2 months ago
Issues checking in progress...
ERROR in src/components/pages/financingPage.tsx:165:33
TS7053: Element implicitly has an 'any' type because expression of type '"4月"' can't be used to index type 'CustomExpenses'.
  Property '4月' does not exist on type 'CustomExpenses'.
    163 |     <div className="row">
    164 |       <span>4月</span>
  > 165 |       <input type="text" value={customExpenses['4月']} onChange={(e) => handleExpenseChange('4月', e.target.value)} />
        |                                 ^^^^^^^^^^^^^^^^^^^^
    166 |       <span className="amount-text">百万円</span>
    167 |     </div>
    168 |     <div className="row">

ERROR in src/components/pages/financingPage.tsx:165:92
TS2345: Argument of type '"4月"' is not assignable to parameter of type 'keyof CustomExpenses'.
    163 |     <div className="row">
    164 |       <span>4月</span>
  > 165 |       <input type="text" value={customExpenses['4月']} onChange={(e) => handleExpenseChange('4月', e.target.value)} />
        |                                                                                            ^^^^
    166 |       <span className="amount-text">百万円</span>
    167 |     </div>
    168 |     <div className="row">

ERROR in src/components/pages/financingPage.tsx:170:33
TS7053: Element implicitly has an 'any' type because expression of type '"5月"' can't be used to index type 'CustomExpenses'.
  Property '5月' does not exist on type 'CustomExpenses'.
    168 |     <div className="row">
    169 |       <span>5月</span>
  > 170 |       <input type="text" value={customExpenses['5月']} onChange={(e) => handleExpenseChange('5月', e.target.value)} />
        |                                 ^^^^^^^^^^^^^^^^^^^^
    171 |       <span className="amount-text">百万円</span>
    172 |     </div>
    173 |     <div className="row">

ERROR in src/components/pages/financingPage.tsx:170:92
TS2345: Argument of type '"5月"' is not assignable to parameter of type 'keyof CustomExpenses'.
    168 |     <div className="row">
    169 |       <span>5月</span>
  > 170 |       <input type="text" value={customExpenses['5月']} onChange={(e) => handleExpenseChange('5月', e.target.value)} />
        |                                                                                            ^^^^
    171 |       <span className="amount-text">百万円</span>
    172 |     </div>
    173 |     <div className="row">

ERROR in src/components/pages/financingPage.tsx:175:33
TS7053: Element implicitly has an 'any' type because expression of type '"6月"' can't be used to index type 'CustomExpenses'.
  Property '6月' does not exist on type 'CustomExpenses'.
    173 |     <div className="row">
    174 |       <span>6月</span>
  > 175 |       <input type="text" value={customExpenses['6月']} onChange={(e) => handleExpenseChange('6月', e.target.value)} />
        |                                 ^^^^^^^^^^^^^^^^^^^^
    176 |       <span className="amount-text">百万円</span>
    177 |     </div>
    178 |     <div className="row">

ERROR in src/components/pages/financingPage.tsx:175:92
TS2345: Argument of type '"6月"' is not assignable to parameter of type 'keyof CustomExpenses'.
    173 |     <div className="row">
    174 |       <span>6月</span>
  > 175 |       <input type="text" value={customExpenses['6月']} onChange={(e) => handleExpenseChange('6月', e.target.value)} />
        |                                                                                            ^^^^
    176 |       <span className="amount-text">百万円</span>
    177 |     </div>
    178 |     <div className="row">
kirin-ri commented 2 months ago
.form-content {
  display: flex;
  flex-wrap: wrap; /* 要素を複数行に配置 */
  gap: 10px; /* 要素間のスペースを確保 */
  justify-content: space-between; /* 左右に要素を配置 */
}

.row {
  display: flex;
  align-items: center; /* 垂直方向に中央揃え */
  width: 48%; /* 左右に2列表示するための幅調整 */
  gap: 10px;
  margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}

.row span {
  width: 40px; /* 「月」の幅を固定して揃える */
  text-align: center; /* テキストを中央に配置 */
}

.row input {
  width: 60px; /* 入力欄の幅を固定 */
  text-align: center;
}

.amount-text {
  width: 60px; /* 「百万円」の幅を入力欄と揃えて固定 */
  text-align: center; /* テキストを中央に配置 */
}
kirin-ri commented 2 months ago
<div className="form-container">
  <div className="form-content">
    {/* 左側(4月から9月) */}
    <div className="row">
      <span>4月</span>
      <input type="text" value={customExpenses['4月']} onChange={(e) => handleExpenseChange('4月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>5月</span>
      <input type="text" value={customExpenses['5月']} onChange={(e) => handleExpenseChange('5月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>6月</span>
      <input type="text" value={customExpenses['6月']} onChange={(e) => handleExpenseChange('6月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>7月</span>
      <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>8月</span>
      <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>9月</span>
      <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>

    {/* 右側(10月から3月) */}
    <div className="row">
      <span>10月</span>
      <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>11月</span>
      <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>12月</span>
      <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>1月</span>
      <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>2月</span>
      <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
    <div className="row">
      <span>3月</span>
      <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
      <span className="amount-text">百万円</span>
    </div>
  </div>
  <button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
kirin-ri commented 2 months ago
.content-wrapper {
  padding: 0; /* 确保没有内边距 */
}

.page-cover {
  margin: 0; /* 确保没有外边距 */
}

.page-cover-title-frame {
  margin: 0; /* 确保没有外边距 */
  padding: 0; /* 确保没有内边距 */
}

.alert-container {
  padding: 0;
  margin: 0;
}

.alert-box {
  background-color: white;
  color: black;
  border: 1px solid #ccc;
  border-radius: 5px;
  padding: 10px;
  margin: 0; /* 确保没有外边距 */
  display: flex;
  justify-content: space-between; /* 确保按钮在右侧 */
  align-items: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  position: relative;
}

.alert-content {
  display: flex;
  align-items: center;
  justify-content: center; /* 确保内容居中 */
  flex-grow: 1;
}

.alert-icon {
  color: red;
  font-size: 2rem; /* 调整感叹号的大小 */
  margin-right: 10px; /* 感叹号与文字之间的间距 */
}

.alert-message {
  text-align: center; /* 文本居中对齐 */
  font-size: 1.2rem;
}

.close-btn {
  background-color: transparent;
  border: 1px solid red;
  border-radius: 15px; /* 椭圆形 */
  color: red;
  cursor: pointer;
  font-size: 1rem;
  padding: 5px 10px;
  margin-left: 10px;
}

.main-content {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  margin-top: 20px;
  padding-left: 20px; /* 增加左侧边距,避免图表贴边 */
  box-sizing: border-box; /* 确保内外边距包含在宽度内 */
}

.left-container {
  flex: 1;
  max-width: 48%; /* 图表宽度 */
  height: auto; /* 图表高度 */
  padding: 20px;
  margin-left: 2%;
  border-radius: 5px;
}

.graph-container {
  background-color: #f9f9f9;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  padding: 30px;
  border: 1px solid #000;
}

.right-container {
  flex: 1;
  max-width: 48%;
  padding: 20px;
  border-radius: 5px;
  margin-right: 2%;
}

.actionbox-title {
  text-align: center;
  background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
  border-radius: 5px;
  font-size: 1rem;
  color: white;
  padding: 5px;
}

.actionbox-message{
  background-color:  #f1f1f1;
  color: #000;
  font-size: 1rem;
  height:278px;
  overflow-y: auto;
  padding: 20px;
}

.actionbox-message::-webkit-scrollbar {
  width: 12px;
}

.actionbox-message::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 10px;
}

.actionbox-message::-webkit-scrollbar-thumb {
  background-color: #888;
  border-radius: 10px;
  border: 3px solid #f1f1f1;
}

.actionbox-message::-webkit-scrollbar-thumb:hover {
  background: #555;
}
.action-section h3{
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 10px;
}

.collapsible-panels {
  margin-top: 30px;
  padding: 20px;
  background-color: #f9f9f9;
}

.collapsible-panel {
  margin-bottom: 10px;
  width: 100%; /* 确保折叠面板宽度填满容器 */
}

.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  background-color: #f1f1f1;
  cursor: pointer;
  border-radius: 5px;
}

.panel-title {
  font-weight: bold;
  font-size: 1.2rem;
}

.panel-money {
  font-weight: bold;
  margin-left: auto;
  font-size: 1.2rem;
}

.panel-content {
  padding: 10px;
  background-color: #ffffff;
  border: 1px solid #f1f1f1;
  border-radius: 0 0 5px 5px;
}

.details-container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  width: fit-content;
  gap: 10px;
}

.detail-item {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

.detail-item span {
  margin-right: 5px;
  text-align: left;
  white-space: nowrap;
}

/* 新增样式 */
.additional-section {
  display: flex;
  margin-top: 10px;
}

.data-filter, .data-comparison {
  height: auto;
  width: 50%;
  padding: 20px;
}

.data-filter h2, .data-comparison h2 {
  margin-bottom: 20px;
  text-align: center;
  font-size: 1rem; /* 调整字体大小 */
  color: white; /* 白色字体 */
  background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
  padding: 10px; /* 增加内边距以确保背景颜色显示为长方形 */
  border-radius: 5px; /* 圆角 */
}

.filter-group, .comparison-group {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.comparison-btn {
  width: 30%;
  padding: 10px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1rem;
}

.filter-btn {
  width: 40%;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 5px;
  font-size: 0.9rem;
  text-align: center;
  margin-right: 10px;
  white-space: nowrap;
}

.filter-select {
  width: 60%;
  padding: 10px;
  border: 1px solid #ccc;
  background-color: white;
  border-radius: 20px;
  font-size: 1rem;
  text-align: center;
}

.comparison-btn {
  width: 100%;
  margin-bottom: 10px;
  border-radius: 20px;
  background-color: #f1f1f1;
  color: black;
}

.comparison-btn.active {
  background-color: #3498db;
}
.custom-expense-forecast {
  width: 100%;
}

.custom-expense-btn {
  margin-top: 15px;
  width: 100%;
  margin-left: 0px;
  padding: 5px;
  background-color: #3498db;
  border-radius: 10px;
  color: white;
  border: none;
  cursor: pointer;
  font-size: 16px;
}

.custom-expense-btn:hover {
  background-color: #007bff;
}

.form-container {
  width: 200%;
  flex-wrap: wrap;
  background-color: white;
  padding: 15px;
  border-radius: 5px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.update-btn {
  width: 100%;
  padding: 2px 1px;
  background-color: #28a745;
  border-radius: 10px;
  color: white;
  border: none;
  cursor: pointer;
  margin-top: 10px;
  font-size: 16px;
}

.update-btn:hover {
  background-color: #218838;
}

.form-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.row span {
  width: 10%;
  flex: 1; /* 各要素が均等に分かれる */
  text-align: center; /* テキストを中央に配置 */
}

.row input {
  max-width: 10%; /* 入力欄の幅を枠の1/4に制限 */
  text-align: center;
}
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム入力</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5 百万円</span>
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
            </div>
            <div className="row">
              <span>5月</span>
              <span>  -14 百万円</span>
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
            </div>
            <div className="row">
              <span>6月</span>
              <span>  -19 百万円</span>
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
            </div>
            <div className="row">
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
            </div>
            <div className="row">
            <span>8月</span>
            <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
            <span>2月</span>
            <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
            </div>
            <div className="row">
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];  // 選択されたデータセットを取得
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
    }
  }, [expenseLevel]); // `expenseLevel`を依存関係として追加

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const newExpenseLevel = e.target.value;
    setExpenseLevel(newExpenseLevel);

    // グラフと推奨アクションを更新
    const selectedData = dataSets[newExpenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    // カスタム予測を閉じる
    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
<div className="form-container">
  <div className="form-content">
    <div className="row">
      <span>4月</span>
      <span>-9.5 百万円</span>
      <span>10月</span>
      <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>5月</span>
      <span>  -14 百万円</span>
      <span>11月</span>
      <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>6月</span>
      <span>  -19 百万円</span>
      <span>12月</span>
      <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>7月</span>
      <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
      <span>1月</span>
      <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>8月</span>
      <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
      <span>2月</span>
      <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
    </div>
    <div className="row">
      <span>9月</span>
      <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
      <span>3月</span>
      <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
    </div>
  </div>
  <button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
kirin-ri commented 2 months ago
.content-wrapper {
  padding: 0; /* 确保没有内边距 */
}

.page-cover {
  margin: 0; /* 确保没有外边距 */
}

.page-cover-title-frame {
  margin: 0; /* 确保没有外边距 */
  padding: 0; /* 确保没有内边距 */
}

.alert-container {
  padding: 0;
  margin: 0;
}

.alert-box {
  background-color: white;
  color: black;
  border: 1px solid #ccc;
  border-radius: 5px;
  padding: 10px;
  margin: 0; /* 确保没有外边距 */
  display: flex;
  justify-content: space-between; /* 确保按钮在右侧 */
  align-items: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  position: relative;
}

.alert-content {
  display: flex;
  align-items: center;
  justify-content: center; /* 确保内容居中 */
  flex-grow: 1;
}

.alert-icon {
  color: red;
  font-size: 2rem; /* 调整感叹号的大小 */
  margin-right: 10px; /* 感叹号与文字之间的间距 */
}

.alert-message {
  text-align: center; /* 文本居中对齐 */
  font-size: 1.2rem;
}

.close-btn {
  background-color: transparent;
  border: 1px solid red;
  border-radius: 15px; /* 椭圆形 */
  color: red;
  cursor: pointer;
  font-size: 1rem;
  padding: 5px 10px;
  margin-left: 10px;
}

.main-content {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  margin-top: 20px;
  padding-left: 20px; /* 增加左侧边距,避免图表贴边 */
  box-sizing: border-box; /* 确保内外边距包含在宽度内 */
}

.left-container {
  flex: 1;
  max-width: 48%; /* 图表宽度 */
  height: auto; /* 图表高度 */
  padding: 20px;
  margin-left: 2%;
  border-radius: 5px;
}

.graph-container {
  background-color: #f9f9f9;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  padding: 30px;
  border: 1px solid #000;
}

.right-container {
  flex: 1;
  max-width: 48%;
  padding: 20px;
  border-radius: 5px;
  margin-right: 2%;
}

.actionbox-title {
  text-align: center;
  background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
  border-radius: 5px;
  font-size: 1rem;
  color: white;
  padding: 5px;
}

.actionbox-message{
  background-color:  #f1f1f1;
  color: #000;
  font-size: 1rem;
  height:278px;
  overflow-y: auto;
  padding: 20px;
}

.actionbox-message::-webkit-scrollbar {
  width: 12px;
}

.actionbox-message::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 10px;
}

.actionbox-message::-webkit-scrollbar-thumb {
  background-color: #888;
  border-radius: 10px;
  border: 3px solid #f1f1f1;
}

.actionbox-message::-webkit-scrollbar-thumb:hover {
  background: #555;
}
.action-section h3{
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 10px;
}

.collapsible-panels {
  margin-top: 30px;
  padding: 20px;
  background-color: #f9f9f9;
}

.collapsible-panel {
  margin-bottom: 10px;
  width: 100%; /* 确保折叠面板宽度填满容器 */
}

.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  background-color: #f1f1f1;
  cursor: pointer;
  border-radius: 5px;
}

.panel-title {
  font-weight: bold;
  font-size: 1.2rem;
}

.panel-money {
  font-weight: bold;
  margin-left: auto;
  font-size: 1.2rem;
}

.panel-content {
  padding: 10px;
  background-color: #ffffff;
  border: 1px solid #f1f1f1;
  border-radius: 0 0 5px 5px;
}

.details-container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  width: fit-content;
  gap: 10px;
}

.detail-item {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

.detail-item span {
  margin-right: 5px;
  text-align: left;
  white-space: nowrap;
}

/* 新增样式 */
.additional-section {
  display: flex;
  margin-top: 10px;
}

.data-filter, .data-comparison {
  height: auto;
  width: 50%;
  padding: 20px;
}

.data-filter h2, .data-comparison h2 {
  margin-bottom: 20px;
  text-align: center;
  font-size: 1rem; /* 调整字体大小 */
  color: white; /* 白色字体 */
  background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
  padding: 10px; /* 增加内边距以确保背景颜色显示为长方形 */
  border-radius: 5px; /* 圆角 */
}

.filter-group, .comparison-group {
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.comparison-btn {
  width: 30%;
  padding: 10px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1rem;
}

.filter-btn {
  width: 40%;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 5px;
  font-size: 0.9rem;
  text-align: center;
  margin-right: 10px;
  white-space: nowrap;
}

.filter-select {
  width: 60%;
  padding: 10px;
  border: 1px solid #ccc;
  background-color: white;
  border-radius: 20px;
  font-size: 1rem;
  text-align: center;
}

.comparison-btn {
  width: 100%;
  margin-bottom: 10px;
  border-radius: 20px;
  background-color: #f1f1f1;
  color: black;
}

.comparison-btn.active {
  background-color: #3498db;
}
.custom-expense-forecast {
  width: 100%;
}

.custom-expense-btn {
  width: 100%;
  margin-top: 5px;
  padding: 5px;
  background-color: #3498db;
  border-radius: 10px;
  color: white;
  border: none;
  cursor: pointer;
  font-size: 16px;
}

.custom-expense-btn:hover {
  background-color: #007bff;
}

.form-container {
  background-color: white;
  padding: 15px;
  border-radius: 5px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.update-btn {
  width: 100%;
  padding: 2px 1px;
  background-color: #28a745;
  border-radius: 10px;
  color: white;
  border: none;
  cursor: pointer;
  margin-top: 10px;
  font-size: 16px;
}

.update-btn:hover {
  background-color: #218838;
}

.form-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.row span {
  flex: 1; /* 各要素が均等に分かれる */
  text-align: center; /* テキストを中央に配置 */
}

.row input {
  max-width: 22%; /* 入力欄の幅を枠の1/4に制限 */
  text-align: center;
}

.forecast-instruction {
  margin-bottom: 10px;
  color: #333;
  font-size: 12px;
  line-height: 1.5;
}
kirin-ri commented 2 months ago
import {
  BarController,
  BarElement,
  CategoryScale,
  Chart as ChartJS,
  ChartTypeRegistry,
  Legend,
  LinearScale,
  LineController,
  LineElement,
  PointElement,
  Title,
  Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(
  BarController,
  LineController,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

type DataSet = {
  income: number[];
  expense: number[];
  incomeExpenseDiff?: number[];
  balance?: number[];
  action?: {
    cashFlow: string;
    countermeasure: string;
  };
};

type CustomExpenses = {
  '7月': string;
  '8月': string;
  '9月': string;
  '10月': string;
  '11月': string;
  '12月': string;
  '1月': string;
  '2月': string;
  '3月': string;
};

const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
  const [isOpen, setIsOpen] = useState(false);

  const togglePanel = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div className="collapsible-panel">
      <div className="panel-header" onClick={togglePanel}>
        <div className="panel-title">{title}</div>
        <div className="panel-money">{money}</div>
      </div>
      {isOpen && (
        <div className="panel-content">
          <div className="details-container">
            {details.map((detail, index) => (
              <div key={index} className="detail-item" style={{ color: 'black' }}>
                <span>{detail.month}</span>
                <span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
                  {detail.amount}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
  return (
    <div className="alert-box">
      <div className="alert-content">
        <i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
        <span className="alert-message">{message}</span>
      </div>
      <button className="close-btn" onClick={onClose}>非表示</button>
    </div>
  );
};

const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);  // カスタム予測の開閉状態を管理
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100); // フォームが開いた後にスクロールする
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    // カスタムデータを使用してグラフと推奨アクションを更新
    updateChartAndAction(income, updatedExpenses, true);

    // グラフ表示エリアにスクロール
    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5</span>
              <span>5月</span>
              <span>-14</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19</span>
              <span>7月</span>
              <input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
            </div>
            <div className="row">
              <span>8月</span>
              <input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
              <span>9月</span>
              <input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
            </div>
            <div className="row">
              <span>10月</span>
              <input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
              <span>11月</span>
              <input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
            </div>
            <div className="row">
              <span>12月</span>
              <input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
              <span>1月</span>
              <input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
            </div>
            <div className="row">
              <span>2月</span>
              <input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
              <span>3月</span>
              <input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [cashFlow, setCashFlow] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40,
        bottom: 0,
        left: 0,
        right: 0,
      },
    },
    plugins: {
      legend: {
        display: false,
        labels: {
          color: 'white',
          boxWidth: 0,
          boxHeight: 0,
        },
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '[百万円]',
        },
      },
    },
  };

  const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
    const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
    const balance = income.reduce((acc: number[], incomeValue, i) => {
      const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
      acc.push(newBalance);
      return acc;
    }, [] as number[]);

    if (chartRef.current) {
      chartRef.current.data = {
        labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
        datasets: [
          {
            type: 'bar',
            label: '収入',
            data: income,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
            },
          },
          {
            type: 'bar',
            label: '支出',
            data: expenses,
            backgroundColor: function (context) {
              const index = context.dataIndex;
              return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
            },
          },
          {
            type: 'line',
            label: '当月収支',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '月末残高',
            data: balance,
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 3 ? [] : [5, 5];
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '前年度残高',
            data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
            borderColor: 'gray',
            backgroundColor: 'gray',
            fill: false,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 4,
            pointHoverRadius: 6,
          },
        ],
      };

      chartRef.current.update();
    }

    // 推奨アクションの更新
    if (isCustom) {
      setCashFlow('-');
      setCountermeasure('-');
    } else {
      const selectedData = dataSets[expenseLevel];
      if (selectedData && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
      }
    }
  };

  useEffect(() => {
    const selectedData = dataSets[expenseLevel];  // 選択されたデータセットを取得
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
    }
  }, [expenseLevel]); // `expenseLevel`を依存関係として追加

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const newExpenseLevel = e.target.value;
    setExpenseLevel(newExpenseLevel);

    // グラフと推奨アクションを更新
    const selectedData = dataSets[newExpenseLevel];
    if (selectedData) {
      updateChartAndAction(selectedData.income, selectedData.expense, false);
    }

    // カスタム予測を閉じる
    setIsCustomForecastOpen(false);
  };

  useEffect(() => {
    const customLegendPlugin = {
      id: 'customLegend',
      afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        const legend = chart?.legend;
        if (!legend || !legend.legendItems) return;

        const ctx = chart.ctx;
        const itemWidth = chart.width / 4;
        let currentX = (chart.width - itemWidth * 3) / 2;
        let currentY = 10;

        legend.legendItems.forEach((legendItem, i) => {
          if (i === 3) {
            currentX = (chart.width - itemWidth * 2) / 2;
            currentY += 20;
          }

          if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
            ctx.save();
            ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + 40, currentY);
            ctx.stroke();
            ctx.restore();
          } else {
            ctx.save();
            ctx.fillStyle = legendItem.fillStyle as string;
            ctx.fillRect(currentX, currentY - 5, 40, 10);
            ctx.restore();
          }

          ctx.textBaseline = 'middle';
          ctx.fillStyle = 'black';
          ctx.fillText(legendItem.text, currentX + 50, currentY);

          currentX += itemWidth;
        });
      },
    };

    ChartJS.register(customLegendPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin);
    };
  }, []);

  const defaultData = {
    labels: [],
    datasets: []
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      {showAlert && (
        <div className="alert-container">
          <AlertBox
            message="期中に当月収支がマイナスになる期間があります"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <div className="filter-btn">
                  <div>支出</div>
                  <div className='filter-btn-before'>(前年度比)</div>
                </div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option value="楽観">楽観:0.8倍</option>
                  <option value="中立">中立:1.0倍</option>
                  <option value="悲観">悲観:1.2倍</option>
                </select>
              </div>
              <CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>残高推移</h3>
              <p>{cashFlow}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <ul style={{ paddingLeft: '0' }}>
                {countermeasure.split('\n').map((item, index) => (
                  item.trim() === '-' ? (
                    <li key={index} style={{ listStyleType: 'none' }}>{item}</li>
                  ) : (
                    <li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
                      {item}
                    </li>
                  )
                ))}
              </ul>
            </div>
          </div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
            <CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
            <CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
          </div>
        </div>
      </div>
    </div>
  );
};

const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
  "楽観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
    action: {
      cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
      countermeasure: "-"
    }
  },
  "中立": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
    action: {
      cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
    }
  },
  "悲観": {
    income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
    expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
    action: {
      cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
      countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
    }
  },
};

const operatingCashFlow = [
  { month: '4月', amount: '34,035,567円' },
  { month: '5月', amount: '30,407,343円' },
  { month: '6月', amount: '34,528,390円' },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const investingCashFlow = [
  { month: '4月', amount: '▲21,502,456円', alert: true },
  { month: '5月', amount: '▲34,023,289円', alert: true },
  { month: '6月', amount: '▲22,315,267円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

const financingCashFlow = [
  { month: '4月', amount: '▲11,504,456円', alert: true },
  { month: '5月', amount: '5,005,275円' },
  { month: '6月', amount: '▲12,746,198円', alert: true },
  { month: '7月', amount: '-' },
  { month: '8月', amount: '-' },
  { month: '9月', amount: '-' },
  { month: '10月', amount: '-' },
  { month: '11月', amount: '-' },
  { month: '12月', amount: '-' },
  { month: '1月', amount: '-' },
  { month: '2月', amount: '-' },
  { month: '3月', amount: '-' },
];

export default EmptyPage;
kirin-ri commented 2 months ago
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100);
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    updateChartAndAction(income, updatedExpenses, true);

    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5 百万円</span> {/* 添加单位 */}
              <span>5月</span>
              <span>-14 百万円</span> {/* 添加单位 */}
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19 百万円</span> {/* 添加单位 */}
              <span>7月</span>
              <input
                type="text"
                value={customExpenses['7月']}
                onChange={(e) => handleExpenseChange('7月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
            </div>
            <div className="row">
              <span>8月</span>
              <input
                type="text"
                value={customExpenses['8月']}
                onChange={(e) => handleExpenseChange('8月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
              <span>9月</span>
              <input
                type="text"
                value={customExpenses['9月']}
                onChange={(e) => handleExpenseChange('9月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
            </div>
            <div className="row">
              <span>10月</span>
              <input
                type="text"
                value={customExpenses['10月']}
                onChange={(e) => handleExpenseChange('10月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
              <span>11月</span>
              <input
                type="text"
                value={customExpenses['11月']}
                onChange={(e) => handleExpenseChange('11月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
            </div>
            <div className="row">
              <span>12月</span>
              <input
                type="text"
                value={customExpenses['12月']}
                onChange={(e) => handleExpenseChange('12月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
              <span>1月</span>
              <input
                type="text"
                value={customExpenses['1月']}
                onChange={(e) => handleExpenseChange('1月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
            </div>
            <div className="row">
              <span>2月</span>
              <input
                type="text"
                value={customExpenses['2月']}
                onChange={(e) => handleExpenseChange('2月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
              <span>3月</span>
              <input
                type="text"
                value={customExpenses['3月']}
                onChange={(e) => handleExpenseChange('3月', e.target.value)}
              />
              <span>百万円</span> {/* 添加单位 */}
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:97:34
TS7031: Binding element 'updateChartAndAction' implicitly has an 'any' type.
     95 | };
     96 |
  >  97 | const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
        |                                  ^^^^^^^^^^^^^^^^^^^^
     98 |   const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
     99 |     '7月': '',
    100 |     '8月': '',

ERROR in src/components/pages/financingPage.tsx:97:56
TS7031: Binding element 'chartRef' implicitly has an 'any' type.
     95 | };
     96 |
  >  97 | const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
        |                                                        ^^^^^^^^
     98 |   const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
     99 |     '7月': '',
    100 |     '8月': '',

ERROR in src/components/pages/financingPage.tsx:97:66
TS7031: Binding element 'isCustomForecastOpen' implicitly has an 'any' type.
     95 | };
     96 |
  >  97 | const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
        |                                                                  ^^^^^^^^^^^^^^^^^^^^
     98 |   const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
     99 |     '7月': '',
    100 |     '8月': '',

ERROR in src/components/pages/financingPage.tsx:97:88
TS7031: Binding element 'setIsCustomForecastOpen' implicitly has an 'any' type.
     95 | };
     96 |
  >  97 | const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
        |                                                                                        ^^^^^^^^^^^^^^^^^^^^^^^
     98 |   const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
     99 |     '7月': '',
    100 |     '8月': '',
kirin-ri commented 2 months ago
 (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <span>-9.5</span>
              <span>百万円</span>
              <span>5月</span>
              <span>-14</span>
              <span>百万円</span>
            </div>
            <div className="row">
              <span>6月</span>
              <span>-19</span>
              <span>百万円</span>
              <span>7月</span>
              <input
                type="text"
                value={customExpenses['7月']}
                onChange={(e) => handleExpenseChange('7月', e.target.value)}
              />
              <span>百万円</span>
            </div>
            <div className="row">
              <span>8月</span>
              <input
                type="text"
                value={customExpenses['8月']}
                onChange={(e) => handleExpenseChange('8月', e.target.value)}
              />
              <span>百万円</span>
              <span>9月</span>
              <input
                type="text"
                value={customExpenses['9月']}
                onChange={(e) => handleExpenseChange('9月', e.target.value)}
              />
              <span>百万円</span>
            </div>
            <div className="row">
              <span>10月</span>
              <input
                type="text"
                value={customExpenses['10月']}
                onChange={(e) => handleExpenseChange('10月', e.target.value)}
              />
              <span>百万円</span>
              <span>11月</span>
              <input
                type="text"
                value={customExpenses['11月']}
                onChange={(e) => handleExpenseChange('11月', e.target.value)}
              />
              <span>百万円</span>
            </div>
            <div className="row">
              <span>12月</span>
              <input
                type="text"
                value={customExpenses['12月']}
                onChange={(e) => handleExpenseChange('12月', e.target.value)}
              />
              <span>百万円</span>
              <span>1月</span>
              <input
                type="text"
                value={customExpenses['1月']}
                onChange={(e) => handleExpenseChange('1月', e.target.value)}
              />
              <span>百万円</span>
            </div>
            <div className="row">
              <span>2月</span>
              <input
                type="text"
                value={customExpenses['2月']}
                onChange={(e) => handleExpenseChange('2月', e.target.value)}
              />
              <span>百万円</span>
              <span>3月</span>
              <input
                type="text"
                value={customExpenses['3月']}
                onChange={(e) => handleExpenseChange('3月', e.target.value)}
              />
              <span>百万円</span>
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  )
kirin-ri commented 2 months ago
.form-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.row {
  display: grid;
  grid-template-columns: repeat(6, 1fr); /* 使用网格布局进行对齐,设置6列 */
  align-items: center; /* 垂直居中对齐 */
  gap: 10px;
}

.row span, .row input {
  text-align: center; /* 文本居中 */
  margin-right: 5px; /* 添加右边距 */
}
kirin-ri commented 2 months ago
const CustomExpenseForecast = ({
  updateChartAndAction,
  chartRef,
  isCustomForecastOpen,
  setIsCustomForecastOpen,
}: {
  updateChartAndAction: UpdateChartAndAction;
  chartRef: ChartRef;
  isCustomForecastOpen: boolean;
  setIsCustomForecastOpen: SetIsCustomForecastOpen;
}) => {
  const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
    '7月': '',
    '8月': '',
    '9月': '',
    '10月': '',
    '11月': '',
    '12月': '',
    '1月': '',
    '2月': '',
    '3月': ''
  });

  const toggleForm = () => {
    const newState = !isCustomForecastOpen;
    setIsCustomForecastOpen(newState);
    if (newState) {
      setTimeout(() => {
        const formContainer = document.querySelector('.form-container');
        if (formContainer) {
          formContainer.scrollIntoView({ behavior: 'smooth' });
        }
      }, 100);
    }
  };

  const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
    setCustomExpenses(prevState => ({
      ...prevState,
      [month]: value
    }));
  };

  const handleUpdate = () => {
    const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
    const baseExpense = [-9.5, -14.0, -19.0];

    const updatedExpenses = [
      ...baseExpense,
      ...Object.keys(customExpenses).map(month => {
        const value = customExpenses[month as keyof CustomExpenses];
        return value !== '' ? parseFloat(value) : estimateExpense();
      })
    ];

    updateChartAndAction(income, updatedExpenses, true);

    if (chartRef.current && chartRef.current.canvas) {
      const parentNode = chartRef.current.canvas.parentNode as Element;
      parentNode.scrollIntoView({ behavior: 'smooth' });
    }
  };

  const estimateExpense = () => {
    return -10; // 任意の推定値
  };

  return (
    <div className="custom-expense-forecast">
      <button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
      {isCustomForecastOpen && (
        <div className="form-container">
          <div className="form-content">
            <div className="row">
              <span>4月</span>
              <div className="number-unit">
                <span>-9.5</span><span className="unit">百万円</span>
              </div>
              <span>5月</span>
              <div className="number-unit">
                <span>-14</span><span className="unit">百万円</span>
              </div>
            </div>
            <div className="row">
              <span>6月</span>
              <div className="number-unit">
                <span>-19</span><span className="unit">百万円</span>
              </div>
              <span>7月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['7月']}
                  onChange={(e) => handleExpenseChange('7月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
            </div>
            <div className="row">
              <span>8月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['8月']}
                  onChange={(e) => handleExpenseChange('8月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
              <span>9月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['9月']}
                  onChange={(e) => handleExpenseChange('9月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
            </div>
            <div className="row">
              <span>10月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['10月']}
                  onChange={(e) => handleExpenseChange('10月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
              <span>11月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['11月']}
                  onChange={(e) => handleExpenseChange('11月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
            </div>
            <div className="row">
              <span>12月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['12月']}
                  onChange={(e) => handleExpenseChange('12月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
              <span>1月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['1月']}
                  onChange={(e) => handleExpenseChange('1月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
            </div>
            <div className="row">
              <span>2月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['2月']}
                  onChange={(e) => handleExpenseChange('2月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
              <span>3月</span>
              <div className="number-unit">
                <input
                  type="text"
                  value={customExpenses['3月']}
                  onChange={(e) => handleExpenseChange('3月', e.target.value)}
                />
                <span className="unit">百万円</span>
              </div>
            </div>
          </div>
          <button className="update-btn" onClick={handleUpdate}>予測</button>
        </div>
      )}
    </div>
  );
};
kirin-ri commented 2 months ago
.form-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.row {
  display: grid;
  grid-template-columns: repeat(4, 1fr); /* 设置为4列布局 */
  align-items: center; /* 垂直居中对齐 */
  gap: 10px;
}

.row span {
  text-align: right; /* 月份名称右对齐 */
  white-space: nowrap; /* 保持文本不换行 */
}

.number-unit {
  display: flex;
  align-items: center; /* 数字和单位垂直居中 */
}

.number-unit span {
  margin-right: 5px; /* 数字和单位之间的间距 */
}

.number-unit input {
  max-width: 60px; /* 控制输入框宽度 */
  text-align: right; /* 输入框内文字右对齐 */
  margin-right: 5px; /* 输入框和单位之间的间距 */
}

.unit {
  white-space: nowrap; /* 确保单位不会换行 */
  text-align: left; /* 单位左对齐 */
}