kirin-ri / memo

0 stars 0 forks source link

mock2 #29

Open kirin-ri opened 2 months ago

kirin-ri commented 2 months ago

収入 プラス(収入を増やす要因) 製品販売の増加: 販売数量や価格の上昇により収入が増えます。 新規受注の獲得: 新たな注文が入ることで将来の収入が増加します。 市場需要の拡大: 市場の成長やニーズの増加により売上が増えます。 為替レートの有利な変動(輸出の場合): 為替レートが有利に変動した場合、輸出による収入が増加します。 補助金・助成金の獲得: 政府や機関からの補助金や助成金が収入に加わります。 マイナス(収入を減らす要因) 製品販売の減少: 販売数量や価格の低下により収入が減少します。 受注減少: 新規受注が減少し、将来の収入が見込めなくなります。 市場需要の縮小: 市場全体の需要が減少し、売上が減ります。 為替レートの不利な変動(輸出の場合): 為替レートが不利に動いた場合、輸出による収入が減少します。 支払い遅延: 顧客からの支払いが遅れたり、回収不能になることで収入が減少します。 支出 プラス(支出を増やす要因) 原材料費の上昇: 原材料の価格が上昇することで支出が増えます。 労務費の増加: 従業員の給与や福利厚生費の増加が支出を増加させます。 設備投資の増加: 新規設備の導入や既存設備のメンテナンス費用が増えることで支出が増加します。 外注費の増加: 外部業者に依頼する費用が増えることで支出が増加します。 過剰在庫の発生: 過剰在庫の維持コストが支出を増加させます。 利息支払いの増加: 借入金の利息が増加することで定期的な支出が増えます。 税金・保険料の増加: 税率の引き上げや保険料の増加が支出を増やします。 マイナス(支出を減らす要因) 原材料費の減少: 原材料の価格が下がることで支出が減少します。 労務費の削減: 従業員数の削減や給与カットなどにより支出が減少します。 設備投資の抑制: 設備投資を延期または削減することで支出が減少します。 外注費の削減: 外部業者への依頼を減らすことで支出が減少します。 在庫管理の最適化: 適切な在庫管理により、過剰在庫の削減が支出を抑えます。 利息支払いの減少: 借入金の返済や利率の低下により、利息支払いが減少します。 税金・保険料の減少: 税制優遇措置や保険料の見直しにより支出が減少します。

kirin-ri commented 2 months ago
{
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": "収入が楽観的に予測される理由として、製品販売の増加、新規受注の獲得、そして市場需要の拡大が挙げられます。特に、新規顧客の獲得や既存製品の価格上昇が収益を押し上げる見込みです。加えて、為替レートが有利に変動した場合、輸出収入の増加も期待されます。一方、支出が楽観的に予測される理由は、原材料費の抑制や労務費の最適化が成功し、外注費や在庫管理費用の削減が可能となるからです。これにより、企業の利益率は大幅に向上するでしょう。対策として、収益増加を維持するために、新市場への積極的な参入と既存市場での競争力強化を図ることが重要です。また、効率的な資金運用により、余剰資金を将来の成長に向けた投資に振り分けるべきです。"
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": "収入が楽観的に予測される要因として、製品販売の増加や新規受注の増加が挙げられます。市場の成長と新製品の導入が、売上拡大を支えるでしょう。また、為替レートの有利な変動も収入増加に寄与すると予想されます。支出が中立的に予測される背景には、原材料費や労務費が安定し、大きな変動が見込まれないことがあります。支出の増減が少ないため、利益率の改善が期待されますが、大きなコスト削減の余地も限られています。対策として、現状の効率性を維持しつつ、利益を最大化するための新しい収益機会の創出が求められます。また、長期的な視点での投資計画を策定し、リスクを管理しながら成長戦略を推進することが重要です。"
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": "収入が楽観的に予測される理由は、製品販売の増加と市場需要の拡大です。新規受注の増加や為替レートの有利な変動が、輸出収入の増加を促進するでしょう。しかし、支出が悲観的に予測される背景には、原材料費や労務費の上昇、設備投資の増加があります。特に、供給チェーンの問題やインフレの影響がコストに影響を与える可能性があります。この状況に対処するためには、効率的なサプライチェーン管理や、代替材料の活用によるコスト削減が求められます。また、長期的なコスト管理戦略を強化し、外部環境の変化に対応できる柔軟な経営体制を整えることが重要です。"
  },
  "中立-楽観": {
    "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.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": "収入が中立的に予測されるのは、既存市場での売上が安定して推移すると見込まれるためです。市場の大きな変動がない中、安定した収益が期待されています。一方で、支出が楽観的に予測される理由は、原材料費の減少や労務費の削減に成功し、運営コストが着実に削減されるためです。この状況を最大限に活用するためには、得られた資金を新規投資や新市場の開拓に振り向けることが重要です。特に、成長市場への参入や新製品の開発に注力することで、将来的な収益拡大を図ることが可能です。また、競争力を維持するための技術革新や人材育成にも重点を置く必要があります。"
  },
  "中立-中立": {
    "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],
    "action": "収入が中立的に予測される背景には、製品販売の増減が少なく、市場全体の需要が安定していることが挙げられます。また、為替レートの変動が小さく、収益に大きな影響を与えないことも安定要因です。支出も中立的に予測されており、原材料費や労務費、設備投資が一定の水準に保たれると考えられます。このような状況下では、収益とコストのバランスを維持しつつ、新たな成長機会を模索することが重要です。特に、将来的な市場の変動に備えて、戦略的な計画を策定し、効率的なリソース配分を行うことが求められます。また、リスク管理を強化し、安定した財務状況を維持するための経営戦略を継続的に見直すことも重要です。"
  },
  "中立-悲観": {
    "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],
    "action": "収入が中立的に予測される理由は、既存市場での需要が安定していることが主な要因です。しかし、拡大の余地が限られているため、大幅な収益増加は期待できません。一方、支出が悲観的に予測されるのは、原材料費や労務費の上昇、設備投資の増加が見込まれるためです。特に、新たな規制対応やインフレの影響がコストに影響を与える可能性があります。この状況では、コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要です。また、予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要です。"
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": "収入が悲観的に予測される背景には、主要市場の需要減少や競争の激化があります。特に、経済環境の悪化や消費者行動の変化が、売上に悪影響を及ぼすと見込まれます。一方で、支出が楽観的に予測される理由は、労務費の削減や設備投資の抑制によって、コストが抑えられる見込みがあるからです。この状況では、収益減少に対処するために、新たなビジネスモデルの導入や既存事業の見直しが求められます。さらに、効率的なコスト管理とリソースの最適化を行い、利益率の向上を目指すことが必要です。"
  },
  "悲観-中立": {
    "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],
    "action": "収入が悲観的に予測される背景には、需要の減少や市場環境の悪化が挙げられます。特に、経済の低迷や競争の激化が収益に影響を与えると見込まれます。一方、支出が中立的に予測されるのは、現状のコスト管理が維持されるためです。大幅な削減は見込まれないものの、支出の増加も限定的と考えられます。このような環境下では、収益性を維持するために、支出削減の施策を検討することが急務です。特に、予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要です。また、新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべきです。"
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": "収入が悲観的に予測される背景には、経済の悪化や市場競争の激化があり、需要の低迷が予測されます。製品販売の減少や受注減少が売上に影響を与える可能性があります。同時に、支出が悲観的に予測されるのは、原材料費や人件費の上昇、設備投資の増加が予想されるためです。この状況に対処するためには、迅速な財務再構築とコスト削減策の実施が急務です。特に、不要な投資の見直しや運営コストの徹底的な削減が求められます。また、外部環境の変化に対応するための戦略的な投資が必要であり、特に新たな収益源の開拓と既存事業の強化が不可欠です。経営陣は、財務状況の安定化を図り、長期的な成長を目指す戦略を構築することが重要です。"
  }
}
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, 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);

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>
  );
};

// 定义 AlertBox 组件
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 [actionMessage, setActionMessage] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

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

    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: 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[]),
                  borderColor: 'black',
                  backgroundColor: 'black',
                  fill: false,
                  tension: 0.1,
                  borderWidth: 2,
                  pointStyle: 'rectRot',
                  pointRadius: 6,
                  pointHoverRadius: 8,
                  segment: {
                      borderDash: (ctx) => {
                          return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
                      },
                  },
                  pointBackgroundColor: function(context) {
                      const index = context.dataIndex;
                      const value = context.dataset.data[index] ?? 0;
                      return value < 0 ? 'red' : 'black';
                  }
              }
          ],
        };

        chartRef.current.update(); // 强制触发图表更新
    }

    setActionMessage(selectedData.action);
  };

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

  useEffect(() => {
    const chartElement = chartRef.current?.canvas?.parentNode;
    if (chartElement) {
      const legendElement = chartElement.querySelector('.chart-legend') as HTMLElement;
      if (legendElement) {
        legendElement.style.setProperty('pointer-events', 'none');
      }
    }
  }, [chartRef.current]);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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 handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setIncomeLevel(e.target.value);
  };

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100; // 每个图例项的宽度(包括图标和文本)
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2; // 图例起始X坐标(居中)
      let currentX = startX;

      const y = 10; // 在图例下方添加额外的距离,以确保与图表内容不重叠

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高') {
          ctx.save();
          ctx.strokeStyle = legendItem.strokeStyle as string;
          ctx.lineWidth = 2; // 设置线条的宽度
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y); // 假设线条宽度为 40
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10); // 假设图标宽度为 40, 高度为 10
          ctx.restore();
        }

        ctx.textBaseline = 'middle';
        ctx.fillStyle = 'black';
        ctx.fillText(legendItem.text, currentX + 50, y); // 图标和文本之间的间距为 50

        currentX += itemWidth; // 移动到下一个图例项
      });
    },
  };

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className='actionbox-message'>
            {actionMessage}
          </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>
  );
};

// dami
const initialBalance = 10.0
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": "収入が楽観的に予測される理由として、製品販売の増加、新規受注の獲得、そして市場需要の拡大が挙げられます。特に、新規顧客の獲得や既存製品の価格上昇が収益を押し上げる見込みです。加えて、為替レートが有利に変動した場合、輸出収入の増加も期待されます。一方、支出が楽観的に予測される理由は、原材料費の抑制や労務費の最適化が成功し、外注費や在庫管理費用の削減が可能となるからです。これにより、企業の利益率は大幅に向上するでしょう。対策として、収益増加を維持するために、新市場への積極的な参入と既存市場での競争力強化を図ることが重要です。また、効率的な資金運用により、余剰資金を将来の成長に向けた投資に振り分けるべきです。"
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": "収入が楽観的に予測される要因として、製品販売の増加や新規受注の増加が挙げられます。市場の成長と新製品の導入が、売上拡大を支えるでしょう。また、為替レートの有利な変動も収入増加に寄与すると予想されます。支出が中立的に予測される背景には、原材料費や労務費が安定し、大きな変動が見込まれないことがあります。支出の増減が少ないため、利益率の改善が期待されますが、大きなコスト削減の余地も限られています。対策として、現状の効率性を維持しつつ、利益を最大化するための新しい収益機会の創出が求められます。また、長期的な視点での投資計画を策定し、リスクを管理しながら成長戦略を推進することが重要です。"
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": "収入が楽観的に予測される理由は、製品販売の増加と市場需要の拡大です。新規受注の増加や為替レートの有利な変動が、輸出収入の増加を促進するでしょう。しかし、支出が悲観的に予測される背景には、原材料費や労務費の上昇、設備投資の増加があります。特に、供給チェーンの問題やインフレの影響がコストに影響を与える可能性があります。この状況に対処するためには、効率的なサプライチェーン管理や、代替材料の活用によるコスト削減が求められます。また、長期的なコスト管理戦略を強化し、外部環境の変化に対応できる柔軟な経営体制を整えることが重要です。"
  },
  "中立-楽観": {
    "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.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": "収入が中立的に予測されるのは、既存市場での売上が安定して推移すると見込まれるためです。市場の大きな変動がない中、安定した収益が期待されています。一方で、支出が楽観的に予測される理由は、原材料費の減少や労務費の削減に成功し、運営コストが着実に削減されるためです。この状況を最大限に活用するためには、得られた資金を新規投資や新市場の開拓に振り向けることが重要です。特に、成長市場への参入や新製品の開発に注力することで、将来的な収益拡大を図ることが可能です。また、競争力を維持するための技術革新や人材育成にも重点を置く必要があります。"
  },
  "中立-中立": {
    "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],
    "action": "収入が中立的に予測される背景には、製品販売の増減が少なく、市場全体の需要が安定していることが挙げられます。また、為替レートの変動が小さく、収益に大きな影響を与えないことも安定要因です。支出も中立的に予測されており、原材料費や労務費、設備投資が一定の水準に保たれると考えられます。このような状況下では、収益とコストのバランスを維持しつつ、新たな成長機会を模索することが重要です。特に、将来的な市場の変動に備えて、戦略的な計画を策定し、効率的なリソース配分を行うことが求められます。また、リスク管理を強化し、安定した財務状況を維持するための経営戦略を継続的に見直すことも重要です。"
  },
  "中立-悲観": {
    "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],
    "action": "収入が中立的に予測される理由は、既存市場での需要が安定していることが主な要因です。しかし、拡大の余地が限られているため、大幅な収益増加は期待できません。一方、支出が悲観的に予測されるのは、原材料費や労務費の上昇、設備投資の増加が見込まれるためです。特に、新たな規制対応やインフレの影響がコストに影響を与える可能性があります。この状況では、コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要です。また、予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要です。"
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": "収入が悲観的に予測される背景には、主要市場の需要減少や競争の激化があります。特に、経済環境の悪化や消費者行動の変化が、売上に悪影響を及ぼすと見込まれます。一方で、支出が楽観的に予測される理由は、労務費の削減や設備投資の抑制によって、コストが抑えられる見込みがあるからです。この状況では、収益減少に対処するために、新たなビジネスモデルの導入や既存事業の見直しが求められます。さらに、効率的なコスト管理とリソースの最適化を行い、利益率の向上を目指すことが必要です。"
  },
  "悲観-中立": {
    "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],
    "action": "収入が悲観的に予測される背景には、需要の減少や市場環境の悪化が挙げられます。特に、経済の低迷や競争の激化が収益に影響を与えると見込まれます。一方、支出が中立的に予測されるのは、現状のコスト管理が維持されるためです。大幅な削減は見込まれないものの、支出の増加も限定的と考えられます。このような環境下では、収益性を維持するために、支出削減の施策を検討することが急務です。特に、予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要です。また、新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべきです。"
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": "収入が悲観的に予測される背景には、経済の悪化や市場競争の激化があり、需要の低迷が予測されます。製品販売の減少や受注減少が売上に影響を与える可能性があります。同時に、支出が悲観的に予測されるのは、原材料費や人件費の上昇、設備投資の増加が予想されるためです。この状況に対処するためには、迅速な財務再構築とコスト削減策の実施が急務です。特に、不要な投資の見直しや運営コストの徹底的な削減が求められます。また、外部環境の変化に対応するための戦略的な投資が必要であり、特に新たな収益源の開拓と既存事業の強化が不可欠です。経営陣は、財務状況の安定化を図り、長期的な成長を目指す戦略を構築することが重要です。"
  }
};

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 EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [incomeLevel, setIncomeLevel] = useState('中立');
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [incomeReason, setIncomeReason] = useState('');
  const [expenseReason, setExpenseReason] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

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

    if (chartRef.current) {
        // Updating chart data
        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: 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[]),
                  borderColor: 'black',
                  backgroundColor: 'black',
                  fill: false,
                  tension: 0.1,
                  borderWidth: 2,
                  pointStyle: 'rectRot',
                  pointRadius: 6,
                  pointHoverRadius: 8,
                  segment: {
                      borderDash: (ctx) => {
                          return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
                      },
                  },
                  pointBackgroundColor: function(context) {
                      const index = context.dataIndex;
                      const value = context.dataset.data[index] ?? 0;
                      return value < 0 ? 'red' : 'black';
                  }
              }
          ],
        };

        chartRef.current.update(); // Force chart update
    }

    // Split the action message into three parts and set them
    const actionParts = selectedData.action.split('。');
    setIncomeReason(actionParts[0] + '。');
    setExpenseReason(actionParts[1] + '。');
    setCountermeasure(actionParts.slice(2).join('。'));
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </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>{incomeReason}</p>
            </div>
            <div className="action-section">
              <h3>支出が予測される理由</h3>
              <p>{expenseReason}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:170:100
TS2552: Cannot find name 'options'. Did you mean 'Option'?
    168 |         <div className="left-container">
    169 |           <div className="graph-container">
  > 170 |             <Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
        |                                                                                                    ^^^^^^^
    171 |           </div>
    172 |           <div className="additional-section">
    173 |             <div className="data-filter">
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, 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);

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 [incomeReason, setIncomeReason] = useState('');
  const [expenseReason, setExpenseReason] = useState('');
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

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

    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: 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[]),
                  borderColor: 'black',
                  backgroundColor: 'black',
                  fill: false,
                  tension: 0.1,
                  borderWidth: 2,
                  pointStyle: 'rectRot',
                  pointRadius: 6,
                  pointHoverRadius: 8,
                  segment: {
                      borderDash: (ctx) => {
                          return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
                      },
                  },
                  pointBackgroundColor: function(context) {
                      const index = context.dataIndex;
                      const value = context.dataset.data[index] ?? 0;
                      return value < 0 ? 'red' : 'black';
                  }
              }
          ],
        };

        chartRef.current.update(); // Force chart update
    }

    const actionParts = selectedData.action.split('。');
    setIncomeReason(actionParts[0] + '。');
    setExpenseReason(actionParts[1] + '。');
    setCountermeasure(actionParts.slice(2).join('。'));
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </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>{incomeReason}</p>
            </div>
            <div className="action-section">
              <h3>支出が予測される理由</h3>
              <p>{expenseReason}</p>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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 = {
  // Your data sets here
};

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 dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": "収入が楽観的に予測される理由として、製品販売の増加、新規受注の獲得、そして市場需要の拡大が挙げられます。特に、新規顧客の獲得や既存製品の価格上昇が収益を押し上げる見込みです。加えて、為替レートが有利に変動した場合、輸出収入の増加も期待されます。",
      "expenseReason": "支出が楽観的に予測される理由は、原材料費の抑制や労務費の最適化が成功し、外注費や在庫管理費用の削減が可能となるからです。これにより、企業の利益率は大幅に向上するでしょう。",
      "countermeasure": "収益増加を維持するために、新市場への積極的な参入と既存市場での競争力強化を図ることが重要です。また、効率的な資金運用により、余剰資金を将来の成長に向けた投資に振り分けるべきです。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "incomeReason": "収入が楽観的に予測される要因として、製品販売の増加や新規受注の増加が挙げられます。市場の成長と新製品の導入が、売上拡大を支えるでしょう。また、為替レートの有利な変動も収入増加に寄与すると予想されます。",
      "expenseReason": "支出が中立的に予測される背景には、原材料費や労務費が安定し、大きな変動が見込まれないことがあります。支出の増減が少ないため、利益率の改善が期待されますが、大きなコスト削減の余地も限られています。",
      "countermeasure": "現状の効率性を維持しつつ、利益を最大化するための新しい収益機会の創出が求められます。また、長期的な視点での投資計画を策定し、リスクを管理しながら成長戦略を推進することが重要です。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": "収入が楽観的に予測される理由は、製品販売の増加と市場需要の拡大です。新規受注の増加や為替レートの有利な変動が、輸出収入の増加を促進するでしょう。",
      "expenseReason": "支出が悲観的に予測される背景には、原材料費や労務費の上昇、設備投資の増加があります。特に、供給チェーンの問題やインフレの影響がコストに影響を与える可能性があります。",
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": "収入が中立的に予測されるのは、既存市場での売上が安定して推移すると見込まれるためです。市場の大きな変動がない中、安定した収益が期待されています。",
      "expenseReason": "支出が楽観的に予測される理由は、原材料費の減少や労務費の削減に成功し、運営コストが着実に削減されるためです。",
      "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],
    "action": {
      "incomeReason": "収入が中立的に予測される背景には、製品販売の増減が少なく、市場全体の需要が安定していることが挙げられます。また、為替レートの変動が小さく、収益に大きな影響を与えないことも安定要因です。",
      "expenseReason": "支出も中立的に予測されており、原材料費や労務費、設備投資が一定の水準に保たれると考えられます。",
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": "収入が中立的に予測される理由は、既存市場での需要が安定していることが主な要因です。しかし、拡大の余地が限られているため、大幅な収益増加は期待できません。",
      "expenseReason": "支出が悲観的に予測されるのは、原材料費や労務費の上昇、設備投資の増加が見込まれるためです。特に、新たな規制対応やインフレの影響がコストに影響を与える可能性があります。",
      "countermeasure": "コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要です。また、予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要です。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": "収入が悲観的に予測される背景には、主要市場の需要減少や競争の激化があります。特に、経済環境の悪化や消費者行動の変化が、売上に悪影響を及ぼすと見込まれます。",
      "expenseReason": "支出が楽観的に予測される理由は、労務費の削減や設備投資の抑制によって、コストが抑えられる見込みがあるからです。",
      "countermeasure": "収益減少に対処するために、新たなビジネスモデルの導入や既存事業の見直しが求められます。さらに、効率的なコスト管理とリソースの最適化を行い、利益率の向上を目指すことが必要です。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "incomeReason": "収入が悲観的に予測される背景には、需要の減少や市場環境の悪化が挙げられます。特に、経済の低迷や競争の激化が収益に影響を与えると見込まれます。",
      "expenseReason": "支出が中立的に予測されるのは、現状のコスト管理が維持されるためです。大幅な削減は見込まれないものの、支出の増加も限定的と考えられます。",
      "countermeasure": "収益性を維持するために、支出削減の施策を検討することが急務です。特に、予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要です。また、新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": "収入が悲観的に予測される背景には、経済の悪化や市場競争の激化があり、需要の低迷が予測されます。製品販売の減少や受注減少が売上に影響を与える可能性があります。",
      "expenseReason": "支出が悲観的に予測されるのは、原材料費や人件費の上昇、設備投資の増加が予想されるためです。",
      "countermeasure": "迅速な財務再構築とコスト削減策の実施が急務です。特に、不要な投資の見直しや運営コストの徹底的な削減が求められます。また、外部環境の変化に対応するための戦略的な投資が必要であり、特に新たな収益源の開拓と既存事業の強化が不可欠です。"
    }
  }
};
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:151:45
TS2339: Property 'split' does not exist on type '{ incomeReason: string; expenseReason: string; countermeasure: string; } | { incomeReason: string; expenseReason: string; countermeasure: string; } | { incomeReason: string; expenseReason: string; countermeasure: string; } | ... 5 more ... | { ...; }'.
  Property 'split' does not exist on type '{ incomeReason: string; expenseReason: string; countermeasure: string; }'.
    149 |     }
    150 |
  > 151 |     const actionParts = selectedData.action.split('。');
        |                                             ^^^^^
    152 |     setIncomeReason(actionParts[0] + '。');
    153 |     setExpenseReason(actionParts[1] + '。');
    154 |     setCountermeasure(actionParts.slice(2).join('。'));
kirin-ri commented 2 months ago
const updateChartAndAction = () => {
    const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
    const selectedData = dataSets[key];

    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: 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[]),
                  borderColor: 'black',
                  backgroundColor: 'black',
                  fill: false,
                  tension: 0.1,
                  borderWidth: 2,
                  pointStyle: 'rectRot',
                  pointRadius: 6,
                  pointHoverRadius: 8,
                  segment: {
                      borderDash: (ctx) => {
                          return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
                      },
                  },
                  pointBackgroundColor: function(context) {
                      const index = context.dataIndex;
                      const value = context.dataset.data[index] ?? 0;
                      return value < 0 ? 'red' : 'black';
                  }
              }
          ],
        };

        chartRef.current.update(); // 强制触发图表更新
    }

    // 直接从对象中提取各部分的内容
    setIncomeReason(selectedData.action.incomeReason);
    setExpenseReason(selectedData.action.expenseReason);
    setCountermeasure(selectedData.action.countermeasure);
};
kirin-ri commented 2 months ago
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加。",
        "新規受注の獲得。",
        "市場需要の拡大。",
        "新規顧客の獲得や既存製品の価格上昇。",
        "為替レートが有利に変動した場合、輸出収入の増加も期待。"
      ],
      "expenseReason": [
        "原材料費の抑制。",
        "労務費の最適化が成功。",
        "外注費や在庫管理費用の削減。"
      ],
      "countermeasure": "収益増加を維持するために、新市場への積極的な参入と既存市場での競争力強化を図ることが重要。また、効率的な資金運用により、余剰資金を将来の成長に向けた投資に振り分けるべき。"
    }
  },
  // 其他数据集同样更新为箇条書き格式
};
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

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

    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: 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[]),
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          }
        ],
      };

      chartRef.current.update(); // 强制触发图表更新
    }

    setIncomeReason(selectedData.action.incomeReason);
    setExpenseReason(selectedData.action.expenseReason);
    setCountermeasure(selectedData.action.countermeasure);
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </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>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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 = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加。",
        "新規受注の獲得。",
        "市場需要の拡大。",
        "新規顧客の獲得や既存製品の価格上昇。",
        "為替レートが有利に変動した場合、輸出収入の増加も期待。"
      ],
      "expenseReason": [
        "原材料費の抑制。",
        "労務費の最適化が成功。",
        "外注費や在庫管理費用の削減。"
      ],
      "countermeasure": "収益増加を維持するために、新市場への積極的な参入と既存市場での競争力強化を図ることが重要。また、効率的な資金運用により、余剰資金を将来の成長に向けた投資に振り分けるべき。"
    }
  },
  // 其他数据集同样更新为箇条書き格式
};

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
Uncaught TypeError: Cannot read properties of undefined (reading 'income')
    at updateChartAndAction (financingPage.tsx:103:1)
    at financingPage.tsx:157:1
    at commitHookEffectListMount (react-dom.development.js:23150:1)
    at commitPassiveMountOnFiber (react-dom.development.js:24926:1)
    at commitPassiveMountEffects_complete (react-dom.development.js:24891:1)
    at commitPassiveMountEffects_begin (react-dom.development.js:24878:1)
    at commitPassiveMountEffects (react-dom.development.js:24866:1)
    at flushPassiveEffectsImpl (react-dom.development.js:27039:1)
    at flushPassiveEffects (react-dom.development.js:26984:1)
    at react-dom.development.js:26769:1
react-dom.development.js:22839 Uncaught TypeError: Cannot read properties of undefined (reading 'income')
    at updateChartAndAction (financingPage.tsx:103:1)
    at financingPage.tsx:157:1
    at commitHookEffectListMount (react-dom.development.js:23150:1)
    at invokePassiveEffectMountInDEV (react-dom.development.js:25154:1)
    at invokeEffectsInDev (react-dom.development.js:27351:1)
    at commitDoubleInvokeEffectsInDEV (react-dom.development.js:27330:1)
    at flushPassiveEffectsImpl (react-dom.development.js:27056:1)
    at flushPassiveEffects (react-dom.development.js:26984:1)
    at react-dom.development.js:26769:1
    at workLoop (scheduler.development.js:266:1)
2react-dom.development.js:18687 The above error occurred in the <EmptyPage> component:

    at EmptyPage (http://localhost:3000/js/bundle.js:794:84)
    at Route (http://localhost:3000/js/bundle.js:41811:29)
    at div
    at Router (http://localhost:3000/js/bundle.js:41483:30)
    at HashRouter (http://localhost:3000/js/bundle.js:41034:35)
    at div
    at MainComponent
    at App (http://localhost:3000/js/bundle.js:51:81)
    at CookiesProvider (http://localhost:3000/js/bundle.js:14440:24)
    at Router (http://localhost:3000/js/bundle.js:41483:30)
    at BrowserRouter (http://localhost:3000/js/bundle.js:40997:35)

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
logCapturedError @ react-dom.development.js:18687
Show 1 more frame
Show less
react-dom.development.js:12056 Uncaught TypeError: Cannot read properties of undefined (reading 'income')
    at updateChartAndAction (financingPage.tsx:103:1)
    at financingPage.tsx:157:1
    at commitHookEffectListMount (react-dom.development.js:23150:1)
    at commitPassiveMountOnFiber (react-dom.development.js:24926:1)
    at commitPassiveMountEffects_complete (react-dom.development.js:24891:1)
    at commitPassiveMountEffects_begin (react-dom.development.js:24878:1)
    at commitPassiveMountEffects (react-dom.development.js:24866:1)
    at flushPassiveEffectsImpl (react-dom.development.js:27039:1)
    at flushPassiveEffects (react-dom.development.js:26984:1)
    at react-dom.development.js:26769:1
kirin-ri commented 2 months ago
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加。",
        "新規受注の獲得。",
        "市場需要の拡大。",
        "新規顧客の獲得や既存製品の価格上昇。",
        "為替レートが有利に変動した場合、輸出収入の増加も期待。"
      ],
      "expenseReason": [
        "原材料費の抑制。",
        "労務費の最適化が成功。",
        "外注費や在庫管理費用の削減。"
      ],
      "countermeasure": "収益増加を維持するために、新市場への積極的な参入と既存市場での競争力強化を図ることが重要。また、効率的な資金運用により、余剰資金を将来の成長に向けた投資に振り分けるべき。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "incomeReason": [
        "製品販売の増加。",
        "新規受注の増加。",
        "市場の成長。",
        "新製品の導入。",
        "為替レートの有利な変動が収入増加に寄与。"
      ],
      "expenseReason": [
        "原材料費や労務費が安定。",
        "大きな変動が見込まれない。",
        "支出の増減が少ない。"
      ],
      "countermeasure": "現状の効率性を維持しつつ、利益を最大化するための新しい収益機会の創出が求められる。長期的な視点での投資計画を策定し、リスクを管理しながら成長戦略を推進することが重要。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "製品販売の増加。",
        "市場需要の拡大。",
        "新規受注の増加。",
        "為替レートの有利な変動による輸出収入の増加。"
      ],
      "expenseReason": [
        "原材料費や労務費の上昇。",
        "設備投資の増加。",
        "供給チェーンの問題やインフレの影響。"
      ],
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "既存市場での売上が安定して推移。",
        "市場の大きな変動がない。",
        "安定した収益が期待。"
      ],
      "expenseReason": [
        "原材料費の減少。",
        "労務費の削減に成功。",
        "運営コストの削減。"
      ],
      "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],
    "action": {
      "incomeReason": [
        "製品販売の増減が少ない。",
        "市場全体の需要が安定。",
        "為替レートの変動が小さい。"
      ],
      "expenseReason": [
        "原材料費や労務費が一定の水準に保たれる。",
        "設備投資の安定。"
      ],
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "既存市場での需要が安定している。",
        "拡大の余地が限られている。",
        "大幅な収益増加は期待できない。"
      ],
      "expenseReason": [
        "原材料費や労務費の上昇。",
        "設備投資の増加。",
        "新たな規制対応やインフレの影響。"
      ],
      "countermeasure": "コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要。予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "主要市場の需要減少。",
        "競争の激化。",
        "経済環境の悪化や消費者行動の変化。"
      ],
      "expenseReason": [
        "労務費の削減。",
        "設備投資の抑制。",
        "コストが抑えられる見込み。"
      ],
      "countermeasure": "収益減少に対処するために、新たなビジネスモデルの導入や既存事業の見直しが求められる。効率的なコスト管理とリソースの最適化を行い、利益率の向上を目指すことが必要。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "incomeReason": [
        "需要の減少。",
        "市場環境の悪化。",
        "経済の低迷や競争の激化。"
      ],
      "expenseReason": [
        "現状のコスト管理が維持される。",
        "支出の増加が限定的。"
      ],
      "countermeasure": "収益性を維持するために、支出削減の施策を検討することが急務。予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要。新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべき。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "経済の悪化。",
        "市場競争の激化。",
        "需要の低迷が予測。"
      ],
      "expenseReason": [
        "原材料費や人件費の上昇。",
        "設備投資の増加が予想される。"
      ],
      "countermeasure": "迅速な財務再構築とコスト削減策の実施が急務。不要な投資の見直しや運営コストの徹底的な削減が求められる。外部環境の変化に対応するための戦略的な投資が必要であり、新たな収益源の開拓と既存事業の強化が不可欠。"
    }
  }
};
kirin-ri commented 2 months ago
const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const [incomeLevel, setIncomeLevel] = useState('中立');
  const [expenseLevel, setExpenseLevel] = useState('中立');
  const [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

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

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

    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: 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[]),
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          }
        ],
      };

      chartRef.current.update(); // 强制触发图表更新
    }

    setIncomeReason(selectedData.action.incomeReason);
    setExpenseReason(selectedData.action.expenseReason);
    setCountermeasure(selectedData.action.countermeasure);
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
収入
プラス(収入を増やす要因)

・製品販売の増加: 販売数量や価格の上昇により収入が増えます。
・新規受注の獲得: 新たな注文が入ることで将来の収入が増加します。
・市場需要の拡大: 市場の成長やニーズの増加により売上が増えます。
・為替レートの有利な変動(輸出の場合): 為替レートが有利に変動した場合、輸出による収入が増加します。
・補助金・助成金の獲得: 政府や機関からの補助金や助成金が収入に加わります。
kirin-ri commented 2 months ago
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加: 販売数量や価格の上昇により収入が増えます。",
        "新規受注の獲得: 新たな注文が入ることで将来の収入が増加します。",
        "市場需要の拡大: 市場の成長やニーズの増加により売上が増えます。",
        "為替レートの有利な変動(輸出の場合): 為替レートが有利に変動した場合、輸出による収入が増加します。",
        "補助金・助成金の獲得: 政府や機関からの補助金や助成金が収入に加わります。"
      ],
      "expenseReason": [
        "原材料費の抑制: 原材料費が抑えられることで、支出が削減されます。",
        "労務費の最適化: 労務費の最適化が成功し、コストが削減されます。",
        "外注費や在庫管理費用の削減: 外注費や在庫管理費用が削減されることで、支出が減少します。"
      ],
      "countermeasure": "収益増加を維持するためには、積極的な新市場への参入と既存市場での競争力強化が必要です。また、効率的な資金運用により、余剰資金を将来の成長に向けた投資に振り分けることが重要です。さらに、為替リスクを軽減するための対策や、補助金・助成金の活用も検討すべきです。これにより、企業の収益基盤を強化し、持続的な成長を達成できます。"
    }
  },
  // 其他数据集也可以按照同样的格式进行更新
};
kirin-ri commented 2 months ago
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加に伴う売上向上。",
        "新規受注の拡大による収益確保。",
        "市場需要の高まりによる成長機会。",
        "為替レートの有利な変動での輸出拡大。",
        "補助金・助成金の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の抑制によるコスト削減。",
        "労務費の最適化による運営効率化。",
        "外注費や在庫管理費用の削減による支出減少。"
      ],
      "countermeasure": "収益増加を維持するためには、新市場への参入と既存市場での競争力強化が必要です。効率的な資金運用と余剰資金の適切な投資が重要です。また、為替リスクの管理や補助金の活用も検討すべきです。"
    }
  },
kirin-ri commented 2 months ago
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加に伴う売上向上。",
        "新規受注の拡大による収益確保。",
        "市場需要の高まりによる成長機会。",
        "為替レートの有利な変動での輸出拡大。",
        "補助金・助成金の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の抑制によるコスト削減。",
        "労務費の最適化による運営効率化。",
        "外注費や在庫管理費用の削減による支出減少。"
      ],
      "countermeasure": "収益増加を維持するためには、新市場への参入と既存市場での競争力強化が必要です。効率的な資金運用と余剰資金の適切な投資が重要です。また、為替リスクの管理や補助金の活用も検討すべきです。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の安定による支出抑制。",
        "労務費の最適化によるコスト削減。",
        "支出の変動が少ないための安定経営。"
      ],
      "countermeasure": "収益性を維持しながら、新たな市場機会を追求し、効率的な資源配分を行うことが重要です。長期的な成長戦略とリスク管理が求められます。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出拡大。",
        "設備投資の増加による負担増。"
      ],
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場需要の安定による成長機会。",
        "新規顧客の維持による収益確保。",
        "価格競争力による市場シェア維持。",
        "為替の安定による輸出収益の確保。"
      ],
      "expenseReason": [
        "原材料費の減少による支出抑制。",
        "労務費の削減によるコスト削減。",
        "運営費の効率化による利益率向上。"
      ],
      "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],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場全体の需要安定による収益確保。",
        "既存顧客の維持による売上維持。",
        "為替の安定による輸出収益の確保。",
        "競争力のある価格設定による売上維持。"
      ],
      "expenseReason": [
        "原材料費の安定によるコスト抑制。",
        "労務費の安定による支出維持。",
        "設備投資の安定による財務負担の軽減。"
      ],
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "既存市場での需要の安定。",
        "市場の大きな変動がない状況。",
        "現状維持を優先する戦略。",
        "既存顧客の安定した需要。",
        "為替リスクの管理による収益維持。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の増加による支出増。",
        "設備投資の増加による支出拡大。"
      ],
      "countermeasure": "コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要です。また、予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要です。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "主要市場の需要減少。",
        "競争の激化による収益減少。",
        "経済環境の悪化による売上減。",
        "顧客行動の変化による影響。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "労務費の削減によるコスト抑制。",
        "設備投資の抑制による支出減。",
        "運営費の効率化によるコスト削減。"
      ],
      "countermeasure": "収益減少に対処するためには、新たなビジネスモデルの導入や既存事業の見直しが必要です。また、効率的なコスト管理とリソースの最適化を通じて、利益率の向上を目指すことが重要です。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "incomeReason": [
        "需要の減少による売上低下。",
        "市場競争の激化による影響。",
        "経済の低迷による収益減。",
        "顧客行動の変化による売上減少。",
        "新規顧客獲得の難しさ。"
      ],
      "expenseReason": [
        "コスト管理の維持による支出抑制。",
        "労務費の安定による支出管理。",
        "設備投資の抑制による財務負担軽減。"
      ],
      "countermeasure": "収益性を維持するために、支出削減の施策を検討することが急務です。予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要です。また、新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "経済の悪化による売上減少。",
        "市場競争の激化による収益減。",
        "需要の低迷による収益悪化。",
        "主要顧客の減少による売上低下。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出増。",
        "設備投資の増加による財務負担増。"
      ],
      "countermeasure": "迅速な財務再構築とコスト削減策の実施が急務です。特に、不要な投資の見直しや運営コストの徹底的な削減が求められます。また、外部環境の変化に対応するための戦略的な投資が必要であり、新たな収益源の開拓と既存事業の強化が不可欠です。"
    }
  }
};
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

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

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

    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: 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[]),
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action.incomeReason);
    setExpenseReason(selectedData.action.expenseReason);
    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);
  };

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100; // 每个图例项的宽度(包括图标和文本)
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2; // 图例起始X坐标(居中)
      let currentX = startX;

      const y = 10; // 在图例下方添加额外的距离,以确保与图表内容不重叠

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高') {
          ctx.save();
          ctx.strokeStyle = legendItem.strokeStyle as string;
          ctx.lineWidth = 2; // 设置线条的宽度
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y); // 假设线条宽度为 40
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10); // 假设图标宽度为 40, 高度为 10
          ctx.restore();
        }

        ctx.textBaseline = 'middle';
        ctx.fillStyle = 'black';
        ctx.fillText(legendItem.text, currentX + 50, y); // 图标和文本之间的间距为 50

        currentX += itemWidth; // 移动到下一个图例项
      });
    },
  };

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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 = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加に伴う売上向上。",
        "新規受注の拡大による収益確保。",
        "市場需要の高まりによる成長機会。",
        "為替レートの有利な変動での輸出拡大。",
        "補助金・助成金の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の抑制によるコスト削減。",
        "労務費の最適化による運営効率化。",
        "外注費や在庫管理費用の削減による支出減少。"
      ],
      "countermeasure": " 収益増加を維持するためには、新市場への参入と既存市場での競争力強化が必要です。効率的な資金運用と余剰資金の適切な投資が重要です。また、為替リスクの管理や補助金の活用も検討すべきです。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の安定による支出抑制。",
        "労務費の最適化によるコスト削減。",
        "支出の変動が少ないための安定経営。"
      ],
      "countermeasure": " 収益性を維持しながら、新たな市場機会を追求し、効率的な資源配分を行うことが重要です。長期的な成長戦略とリスク管理が求められます。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出拡大。",
        "設備投資の増加による負担増。"
      ],
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場需要の安定による成長機会。",
        "新規顧客の維持による収益確保。",
        "価格競争力による市場シェア維持。",
        "為替の安定による輸出収益の確保。"
      ],
      "expenseReason": [
        "原材料費の減少による支出抑制。",
        "労務費の削減によるコスト削減。",
        "運営費の効率化による利益率向上。"
      ],
      "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],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場全体の需要安定による収益確保。",
        "既存顧客の維持による売上維持。",
        "為替の安定による輸出収益の確保。",
        "競争力のある価格設定による売上維持。"
      ],
      "expenseReason": [
        "原材料費の安定によるコスト抑制。",
        "労務費の安定による支出維持。",
        "設備投資の安定による財務負担の軽減。"
      ],
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "既存市場での需要の安定。",
        "市場の大きな変動がない状況。",
        "現状維持を優先する戦略。",
        "既存顧客の安定した需要。",
        "為替リスクの管理による収益維持。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の増加による支出増。",
        "設備投資の増加による支出拡大。"
      ],
      "countermeasure": " コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要です。また、予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要です。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "主要市場の需要減少。",
        "競争の激化による収益減少。",
        "経済環境の悪化による売上減。",
        "顧客行動の変化による影響。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "労務費の削減によるコスト抑制。",
        "設備投資の抑制による支出減。",
        "運営費の効率化によるコスト削減。"
      ],
      "countermeasure": " 収益減少に対処するためには、新たなビジネスモデルの導入や既存事業の見直しが必要です。また、効率的なコスト管理とリソースの最適化を通じて、利益率の向上を目指すことが重要です。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "incomeReason": [
        "需要の減少による売上低下。",
        "市場競争の激化による影響。",
        "経済の低迷による収益減。",
        "顧客行動の変化による売上減少。",
        "新規顧客獲得の難しさ。"
      ],
      "expenseReason": [
        "コスト管理の維持による支出抑制。",
        "労務費の安定による支出管理。",
        "設備投資の抑制による財務負担軽減。"
      ],
      "countermeasure": " 収益性を維持するために、支出削減の施策を検討することが急務です。予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要です。また、新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "経済の悪化による売上減少。",
        "市場競争の激化による収益減。",
        "需要の低迷による収益悪化。",
        "主要顧客の減少による売上低下。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出増。",
        "設備投資の増加による財務負担増。"
      ],
      "countermeasure": " 迅速な財務再構築とコスト削減策の実施が急務です。特に、不要な投資の見直しや運営コストの徹底的な削減が求められます。また、外部環境の変化に対応するための戦略的な投資が必要であり、新たな収益源の開拓と既存事業の強化が不可欠です。"
    }
  }
};

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, 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

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

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

    if (chartRef.current) {
      // 収支差を計算
      const incomeExpenseDiff = selectedData.income.map((income, i) => income + selectedData.expense[i]);

      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: 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[]),
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '収支差',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action.incomeReason);
    setExpenseReason(selectedData.action.expenseReason);
    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);
  };

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  const defaultData = {
    labels: [],
    datasets: []
  };
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

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

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

    if (chartRef.current) {
      // 収支差を計算
      const incomeExpenseDiff = selectedData.income.map((income, i) => income + selectedData.expense[i]);

      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: 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[]),
            borderColor: 'black',
            backgroundColor: 'black',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'rectRot',
            pointRadius: 6,
            pointHoverRadius: 8,
            segment: {
              borderDash: (ctx) => {
                return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '収支差',
            data: incomeExpenseDiff,
            borderColor: 'blue',
            backgroundColor: 'blue',
            fill: false,
            tension: 0.1,
            borderWidth: 2,
            pointStyle: 'circle',
            pointRadius: 4,
            pointHoverRadius: 6
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action.incomeReason);
    setExpenseReason(selectedData.action.expenseReason);
    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);
  };

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:206:47
TS2304: Cannot find name 'ChartTypeRegistry'.
    204 |   const customLegendPlugin = {
    205 |     id: 'customLegend',
  > 206 |     afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
        |                                               ^^^^^^^^^^^^^^^^^
    207 |       const legend = chart?.legend;
    208 |       if (!legend || !legend.legendItems) return;
    209 |

ERROR in src/components/pages/financingPage.tsx:217:47
TS2304: Cannot find name 'LegendItem'.
    215 |       const y = 10;
    216 |
  > 217 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        |                                               ^^^^^^^^^^
    218 |         if (legendItem.text === '残高' || legendItem.text === '収支差') {
    219 |           ctx.save();
    220 |           ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, ChartTypeRegistry, LegendItem } 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);
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, ChartTypeRegistry, LegendItem } 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

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

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

    if (chartRef.current) {
      // 収支差を計算
      const incomeExpenseDiff = selectedData.income.map((income, i) => income + selectedData.expense[i]);

      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: 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[]),
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action.incomeReason);
    setExpenseReason(selectedData.action.expenseReason);
    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);
  };

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, ChartTypeRegistry, LegendItem } 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const generateRandomData = () => {
    const income = Array.from({ length: 12 }, () => Math.random() * 10 + 30);
    const expense = Array.from({ length: 12 }, () => -(Math.random() * 10 + 20));

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

    // Adjust balance to end at 10 in March
    const balanceAdjustment = 10 - balance[11];
    balance.forEach((b, i) => {
      if (i >= 11) {
        balance[i] += balanceAdjustment;
      }
    });

    return {
      income,
      expense,
      balance,
      incomeExpenseDiff: income.map((income, i) => income + expense[i])
    };
  };

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
    const selectedData = activeComparison === '今年度比較' ? dataSets[key] : generateRandomData();

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

    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: selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '収支差',
            data: selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action?.incomeReason || []);
    setExpenseReason(selectedData.action?.expenseReason || []);
    setCountermeasure(selectedData.action?.countermeasure || '');
    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, ChartTypeRegistry, LegendItem } 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const generateRandomData = () => {
    const income = Array.from({ length: 12 }, () => Math.random() * 10 + 30);
    const expense = Array.from({ length: 12 }, () => -(Math.random() * 10 + 20));

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

    // Adjust balance to end at 10 in March
    const balanceAdjustment = 10 - balance[11];
    balance.forEach((b, i) => {
      if (i >= 11) {
        balance[i] += balanceAdjustment;
      }
    });

    return {
      income,
      expense,
      balance,
      incomeExpenseDiff: income.map((income, i) => income + expense[i])
    };
  };

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
    const selectedData = activeComparison === '今年度比較' ? dataSets[key] : generateRandomData();

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

    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: selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '収支差',
            data: selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action?.incomeReason || []);
    setExpenseReason(selectedData.action?.expenseReason || []);
    setCountermeasure(selectedData.action?.countermeasure || '');
    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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 = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加に伴う売上向上。",
        "新規受注の拡大による収益確保。",
        "市場需要の高まりによる成長機会。",
        "為替レートの有利な変動での輸出拡大。",
        "補助金・助成金の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の抑制によるコスト削減。",
        "労務費の最適化による運営効率化。",
        "外注費や在庫管理費用の削減による支出減少。"
      ],
      "countermeasure": " 収益増加を維持するためには、新市場への参入と既存市場での競争力強化が必要です。効率的な資金運用と余剰資金の適切な投資が重要です。また、為替リスクの管理や補助金の活用も検討すべきです。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の安定による支出抑制。",
        "労務費の最適化によるコスト削減。",
        "支出の変動が少ないための安定経営。"
      ],
      "countermeasure": " 収益性を維持しながら、新たな市場機会を追求し、効率的な資源配分を行うことが重要です。長期的な成長戦略とリスク管理が求められます。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出拡大。",
        "設備投資の増加による負担増。"
      ],
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場需要の安定による成長機会。",
        "新規顧客の維持による収益確保。",
        "価格競争力による市場シェア維持。",
        "為替の安定による輸出収益の確保。"
      ],
      "expenseReason": [
        "原材料費の減少による支出抑制。",
        "労務費の削減によるコスト削減。",
        "運営費の効率化による利益率向上。"
      ],
      "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],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場全体の需要安定による収益確保。",
        "既存顧客の維持による売上維持。",
        "為替の安定による輸出収益の確保。",
        "競争力のある価格設定による売上維持。"
      ],
      "expenseReason": [
        "原材料費の安定によるコスト抑制。",
        "労務費の安定による支出維持。",
        "設備投資の安定による財務負担の軽減。"
      ],
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "既存市場での需要の安定。",
        "市場の大きな変動がない状況。",
        "現状維持を優先する戦略。",
        "既存顧客の安定した需要。",
        "為替リスクの管理による収益維持。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の増加による支出増。",
        "設備投資の増加による支出拡大。"
      ],
      "countermeasure": " コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要です。また、予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要です。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "主要市場の需要減少。",
        "競争の激化による収益減少。",
        "経済環境の悪化による売上減。",
        "顧客行動の変化による影響。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "労務費の削減によるコスト抑制。",
        "設備投資の抑制による支出減。",
        "運営費の効率化によるコスト削減。"
      ],
      "countermeasure": " 収益減少に対処するためには、新たなビジネスモデルの導入や既存事業の見直しが必要です。また、効率的なコスト管理とリソースの最適化を通じて、利益率の向上を目指すことが重要です。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "incomeReason": [
        "需要の減少による売上低下。",
        "市場競争の激化による影響。",
        "経済の低迷による収益減。",
        "顧客行動の変化による売上減少。",
        "新規顧客獲得の難しさ。"
      ],
      "expenseReason": [
        "コスト管理の維持による支出抑制。",
        "労務費の安定による支出管理。",
        "設備投資の抑制による財務負担軽減。"
      ],
      "countermeasure": " 収益性を維持するために、支出削減の施策を検討することが急務です。予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要です。また、新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "経済の悪化による売上減少。",
        "市場競争の激化による収益減。",
        "需要の低迷による収益悪化。",
        "主要顧客の減少による売上低下。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出増。",
        "設備投資の増加による財務負担増。"
      ],
      "countermeasure": " 迅速な財務再構築とコスト削減策の実施が急務です。特に、不要な投資の見直しや運営コストの徹底的な削減が求められます。また、外部環境の変化に対応するための戦略的な投資が必要であり、新たな収益源の開拓と既存事業の強化が不可欠です。"
    }
  }
};

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, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, ChartTypeRegistry, LegendItem } 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const generateRandomData = () => {
    const income = Array.from({ length: 12 }, () => Math.random() * 10 + 30);
    const expense = Array.from({ length: 12 }, () => -(Math.random() * 10 + 20));

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

    // Adjust balance to end at 10 in March
    const balanceAdjustment = 10 - balance[11];
    balance.forEach((b, i) => {
      if (i >= 11) {
        balance[i] += balanceAdjustment;
      }
    });

    return {
      income,
      expense,
      balance,
      incomeExpenseDiff: income.map((income, i) => income + expense[i])
    };
  };

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
    const selectedData = activeComparison === '今年度比較' ? dataSets[key] : generateRandomData();

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

    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: selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '収支差',
            data: selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action?.incomeReason || []);
    setExpenseReason(selectedData.action?.expenseReason || []);
    setCountermeasure(selectedData.action?.countermeasure || '');
    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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 = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加に伴う売上向上。",
        "新規受注の拡大による収益確保。",
        "市場需要の高まりによる成長機会。",
        "為替レートの有利な変動での輸出拡大。",
        "補助金・助成金の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の抑制によるコスト削減。",
        "労務費の最適化による運営効率化。",
        "外注費や在庫管理費用の削減による支出減少。"
      ],
      "countermeasure": " 収益増加を維持するためには、新市場への参入と既存市場での競争力強化が必要です。効率的な資金運用と余剰資金の適切な投資が重要です。また、為替リスクの管理や補助金の活用も検討すべきです。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の安定による支出抑制。",
        "労務費の最適化によるコスト削減。",
        "支出の変動が少ないための安定経営。"
      ],
      "countermeasure": " 収益性を維持しながら、新たな市場機会を追求し、効率的な資源配分を行うことが重要です。長期的な成長戦略とリスク管理が求められます。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出拡大。",
        "設備投資の増加による負担増。"
      ],
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場需要の安定による成長機会。",
        "新規顧客の維持による収益確保。",
        "価格競争力による市場シェア維持。",
        "為替の安定による輸出収益の確保。"
      ],
      "expenseReason": [
        "原材料費の減少による支出抑制。",
        "労務費の削減によるコスト削減。",
        "運営費の効率化による利益率向上。"
      ],
      "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],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場全体の需要安定による収益確保。",
        "既存顧客の維持による売上維持。",
        "為替の安定による輸出収益の確保。",
        "競争力のある価格設定による売上維持。"
      ],
      "expenseReason": [
        "原材料費の安定によるコスト抑制。",
        "労務費の安定による支出維持。",
        "設備投資の安定による財務負担の軽減。"
      ],
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "既存市場での需要の安定。",
        "市場の大きな変動がない状況。",
        "現状維持を優先する戦略。",
        "既存顧客の安定した需要。",
        "為替リスクの管理による収益維持。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の増加による支出増。",
        "設備投資の増加による支出拡大。"
      ],
      "countermeasure": " コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要です。また、予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要です。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "主要市場の需要減少。",
        "競争の激化による収益減少。",
        "経済環境の悪化による売上減。",
        "顧客行動の変化による影響。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "労務費の削減によるコスト抑制。",
        "設備投資の抑制による支出減。",
        "運営費の効率化によるコスト削減。"
      ],
      "countermeasure": " 収益減少に対処するためには、新たなビジネスモデルの導入や既存事業の見直しが必要です。また、効率的なコスト管理とリソースの最適化を通じて、利益率の向上を目指すことが重要です。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "incomeReason": [
        "需要の減少による売上低下。",
        "市場競争の激化による影響。",
        "経済の低迷による収益減。",
        "顧客行動の変化による売上減少。",
        "新規顧客獲得の難しさ。"
      ],
      "expenseReason": [
        "コスト管理の維持による支出抑制。",
        "労務費の安定による支出管理。",
        "設備投資の抑制による財務負担軽減。"
      ],
      "countermeasure": " 収益性を維持するために、支出削減の施策を検討することが急務です。予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要です。また、新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "経済の悪化による売上減少。",
        "市場競争の激化による収益減。",
        "需要の低迷による収益悪化。",
        "主要顧客の減少による売上低下。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出増。",
        "設備投資の増加による財務負担増。"
      ],
      "countermeasure": " 迅速な財務再構築とコスト削減策の実施が急務です。特に、不要な投資の見直しや運営コストの徹底的な削減が求められます。また、外部環境の変化に対応するための戦略的な投資が必要であり、新たな収益源の開拓と既存事業の強化が不可欠です。"
    }
  }
};

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, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, ChartTypeRegistry, LegendItem } 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const dummyPreviousYearData = {
    income: [35.0, 36.5, 36.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0, 10.0],
    expense: [-34.0, -35.0, -36.0, -34.5, -34.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, 0.0],
    balance: [10.0, 12.5, 13.0, 16.0, 20.0, 24.5, 29.0, 33.5, 38.0, 42.5, 47.0, 10.0],
    incomeExpenseDiff: [1.0, 1.5, 0.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 10.0],
  };

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
    const selectedData = activeComparison === '今年度比較' ? dataSets[key] : dummyPreviousYearData;

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

    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: selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '収支差',
            data: selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action?.incomeReason || []);
    setExpenseReason(selectedData.action?.expenseReason || []);
    setCountermeasure(selectedData.action?.countermeasure || '');
    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:133:32
TS2339: Property 'balance' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; } | { income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
  Property 'balance' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
    131 |             type: 'line',
    132 |             label: '残高',
  > 133 |             data: selectedData.balance,
        |                                ^^^^^^^
    134 |             borderColor: 'black',
    135 |             backgroundColor: 'black',
    136 |             fill: false,

ERROR in src/components/pages/financingPage.tsx:156:32
TS2339: Property 'incomeExpenseDiff' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; } | { income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
  Property 'incomeExpenseDiff' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
    154 |             type: 'line',
    155 |             label: '収支差',
  > 156 |             data: selectedData.incomeExpenseDiff,
        |                                ^^^^^^^^^^^^^^^^^
    157 |             borderColor: 'blue',
    158 |             backgroundColor: 'blue',
    159 |             fill: false,

ERROR in src/components/pages/financingPage.tsx:182:34
TS2339: Property 'action' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; } | { income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
  Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
    180 |     }
    181 |
  > 182 |     setIncomeReason(selectedData.action?.incomeReason || []);
        |                                  ^^^^^^
    183 |     setExpenseReason(selectedData.action?.expenseReason || []);
    184 |     setCountermeasure(selectedData.action?.countermeasure || '');
    185 |     const actionBox = document.querySelector('.actionbox-message');

ERROR in src/components/pages/financingPage.tsx:183:35
TS2339: Property 'action' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; } | { income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
  Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
    181 |
    182 |     setIncomeReason(selectedData.action?.incomeReason || []);
  > 183 |     setExpenseReason(selectedData.action?.expenseReason || []);
        |                                   ^^^^^^
    184 |     setCountermeasure(selectedData.action?.countermeasure || '');
    185 |     const actionBox = document.querySelector('.actionbox-message');
    186 |     if (actionBox) {

ERROR in src/components/pages/financingPage.tsx:184:36
TS2339: Property 'action' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; } | { income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
  Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
    182 |     setIncomeReason(selectedData.action?.incomeReason || []);
    183 |     setExpenseReason(selectedData.action?.expenseReason || []);
  > 184 |     setCountermeasure(selectedData.action?.countermeasure || '');
        |                                    ^^^^^^
    185 |     const actionBox = document.querySelector('.actionbox-message');
    186 |     if (actionBox) {
    187 |       actionBox.scrollTop = 0;
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, ChartTypeRegistry, LegendItem } 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const dummyPreviousYearData = {
    income: [35.0, 36.5, 36.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0, 10.0],
    expense: [-34.0, -35.0, -36.0, -34.5, -34.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, 0.0],
    balance: [10.0, 12.5, 13.0, 16.0, 20.0, 24.5, 29.0, 33.5, 38.0, 42.5, 47.0, 10.0],
    incomeExpenseDiff: [1.0, 1.5, 0.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 10.0],
  };

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
    const selectedData = activeComparison === '今年度比較' ? dataSets[key] : dummyPreviousYearData;

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

    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: 'balance' in selectedData ? selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '収支差',
            data: 'incomeExpenseDiff' in selectedData ? selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    if ('action' in selectedData) {
      setIncomeReason(selectedData.action.incomeReason || []);
      setExpenseReason(selectedData.action.expenseReason || []);
      setCountermeasure(selectedData.action.countermeasure || '');
    } else {
      setIncomeReason([]);
      setExpenseReason([]);
      setCountermeasure('');
    }

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

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, ChartTypeRegistry, LegendItem } 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const dummyPreviousYearData = {
    income: [35.0, 36.5, 36.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0, 10.0],
    expense: [-34.0, -35.0, -36.0, -34.5, -34.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, 0.0],
    balance: [10.0, 12.5, 13.0, 16.0, 20.0, 24.5, 29.0, 33.5, 38.0, 42.5, 47.0, 10.0],
    incomeExpenseDiff: [1.0, 1.5, 0.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 10.0],
  };

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
    const selectedData = activeComparison === '今年度比較' ? dataSets[key] : dummyPreviousYearData;

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

    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: 'balance' in selectedData ? selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            type: 'line',
            label: '収支差',
            data: 'incomeExpenseDiff' in selectedData ? selectedData.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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    if ('action' in selectedData) {
      setIncomeReason(selectedData.action.incomeReason || []);
      setExpenseReason(selectedData.action.expenseReason || []);
      setCountermeasure(selectedData.action.countermeasure || '');
    } else {
      setIncomeReason([]);
      setExpenseReason([]);
      setCountermeasure('');
    }

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

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

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

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

    if (chartRef.current) {
      // 収支差を計算
      const incomeExpenseDiff = selectedData.income.map((income, i) => income + selectedData.expense[i]);

      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: 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[]),
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    setIncomeReason(selectedData.action.incomeReason);
    setExpenseReason(selectedData.action.expenseReason);
    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);
  };

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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 = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "incomeReason": [
        "製品販売の増加に伴う売上向上。",
        "新規受注の拡大による収益確保。",
        "市場需要の高まりによる成長機会。",
        "為替レートの有利な変動での輸出拡大。",
        "補助金・助成金の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の抑制によるコスト削減。",
        "労務費の最適化による運営効率化。",
        "外注費や在庫管理費用の削減による支出減少。"
      ],
      "countermeasure": " 収益増加を維持するためには、新市場への参入と既存市場での競争力強化が必要です。効率的な資金運用と余剰資金の適切な投資が重要です。また、為替リスクの管理や補助金の活用も検討すべきです。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の安定による支出抑制。",
        "労務費の最適化によるコスト削減。",
        "支出の変動が少ないための安定経営。"
      ],
      "countermeasure": " 収益性を維持しながら、新たな市場機会を追求し、効率的な資源配分を行うことが重要です。長期的な成長戦略とリスク管理が求められます。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "製品販売の増加による売上向上。",
        "新規受注の拡大による収益確保。",
        "市場の成長による売上安定化。",
        "為替レートの好転による輸出増加。",
        "新規顧客の獲得による収益強化。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出拡大。",
        "設備投資の増加による負担増。"
      ],
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場需要の安定による成長機会。",
        "新規顧客の維持による収益確保。",
        "価格競争力による市場シェア維持。",
        "為替の安定による輸出収益の確保。"
      ],
      "expenseReason": [
        "原材料費の減少による支出抑制。",
        "労務費の削減によるコスト削減。",
        "運営費の効率化による利益率向上。"
      ],
      "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],
    "action": {
      "incomeReason": [
        "既存市場での売上安定化。",
        "市場全体の需要安定による収益確保。",
        "既存顧客の維持による売上維持。",
        "為替の安定による輸出収益の確保。",
        "競争力のある価格設定による売上維持。"
      ],
      "expenseReason": [
        "原材料費の安定によるコスト抑制。",
        "労務費の安定による支出維持。",
        "設備投資の安定による財務負担の軽減。"
      ],
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "既存市場での需要の安定。",
        "市場の大きな変動がない状況。",
        "現状維持を優先する戦略。",
        "既存顧客の安定した需要。",
        "為替リスクの管理による収益維持。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の増加による支出増。",
        "設備投資の増加による支出拡大。"
      ],
      "countermeasure": " コスト削減が急務となり、サプライチェーンの効率化やエネルギーコストの見直しが必要です。また、予算の再評価と非効率な支出の削減を通じて、企業の財務基盤を強化することが重要です。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "incomeReason": [
        "主要市場の需要減少。",
        "競争の激化による収益減少。",
        "経済環境の悪化による売上減。",
        "顧客行動の変化による影響。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "労務費の削減によるコスト抑制。",
        "設備投資の抑制による支出減。",
        "運営費の効率化によるコスト削減。"
      ],
      "countermeasure": " 収益減少に対処するためには、新たなビジネスモデルの導入や既存事業の見直しが必要です。また、効率的なコスト管理とリソースの最適化を通じて、利益率の向上を目指すことが重要です。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "incomeReason": [
        "需要の減少による売上低下。",
        "市場競争の激化による影響。",
        "経済の低迷による収益減。",
        "顧客行動の変化による売上減少。",
        "新規顧客獲得の難しさ。"
      ],
      "expenseReason": [
        "コスト管理の維持による支出抑制。",
        "労務費の安定による支出管理。",
        "設備投資の抑制による財務負担軽減。"
      ],
      "countermeasure": " 収益性を維持するために、支出削減の施策を検討することが急務です。予算の再評価やコスト構造の見直しを行い、無駄な支出を排除することが重要です。また、新たな収益源の開拓や、事業ポートフォリオの最適化を図ることで、企業の競争力を維持し、長期的な成長を目指すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "incomeReason": [
        "経済の悪化による売上減少。",
        "市場競争の激化による収益減。",
        "需要の低迷による収益悪化。",
        "主要顧客の減少による売上低下。",
        "価格競争の激化による利益率低下。"
      ],
      "expenseReason": [
        "原材料費の上昇によるコスト増。",
        "労務費の上昇による支出増。",
        "設備投資の増加による財務負担増。"
      ],
      "countermeasure": " 迅速な財務再構築とコスト削減策の実施が急務です。特に、不要な投資の見直しや運営コストの徹底的な削減が求められます。また、外部環境の変化に対応するための戦略的な投資が必要であり、新たな収益源の開拓と既存事業の強化が不可欠です。"
    }
  }
};

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, LegendItem, 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);

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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    let selectedData;

    if (activeComparison === 'test') {
      selectedData = dummyPreviousYearData;
    } else {
      const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
      selectedData = dataSets[key];
    }

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

    if (chartRef.current) {
      const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData.expense[i]);

      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: 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[]),
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    if (activeComparison !== 'test') {
      setIncomeReason(selectedData.action.incomeReason);
      setExpenseReason(selectedData.action.expenseReason);
      setCountermeasure(selectedData.action.countermeasure);
    }
    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === 'test' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('test')}
              >
                test
              </button>
            </div>
          </div>
        </div>
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:93:9
TS7034: Variable 'selectedData' implicitly has type 'any' in some locations where its type cannot be determined.
    91 |
    92 |   const updateChartAndAction = () => {
  > 93 |     let selectedData;
       |         ^^^^^^^^^^^^
    94 |
    95 |     if (activeComparison === 'test') {
    96 |       selectedData = dummyPreviousYearData;

ERROR in src/components/pages/financingPage.tsx:108:46
TS2339: Property 'incomeExpenseDiff' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; } | { income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
  Property 'incomeExpenseDiff' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
    106 |
    107 |     if (chartRef.current) {
  > 108 |       const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData.expense[i]);
        |                                              ^^^^^^^^^^^^^^^^^
    109 |
    110 |       chartRef.current.data = {
    111 |         labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],

ERROR in src/components/pages/financingPage.tsx:108:115
TS7005: Variable 'selectedData' implicitly has an 'any' type.
    106 |
    107 |     if (chartRef.current) {
  > 108 |       const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData.expense[i]);
        |                                                                                                                   ^^^^^^^^^^^^
    109 |
    110 |       chartRef.current.data = {
    111 |         labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],

ERROR in src/components/pages/financingPage.tsx:134:32
TS2339: Property 'balance' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; } | { income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
  Property 'balance' does not exist on type '{ income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
    132 |             type: 'line',
    133 |             label: '残高',
  > 134 |             data: selectedData.balance || selectedData.income.reduce((acc: number[], income, i) => {
        |                                ^^^^^^^
    135 |               let newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + income + selectedData.expense[i];
    136 |               acc.push(newBalance);
    137 |               return acc;

ERROR in src/components/pages/financingPage.tsx:135:99
TS7005: Variable 'selectedData' implicitly has an 'any' type.
    133 |             label: '残高',
    134 |             data: selectedData.balance || selectedData.income.reduce((acc: number[], income, i) => {
  > 135 |               let newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + income + selectedData.expense[i];
        |                                                                                                   ^^^^^^^^^^^^
    136 |               acc.push(newBalance);
    137 |               return acc;
    138 |             }, [] as number[]),

ERROR in src/components/pages/financingPage.tsx:188:36
TS2339: Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; } | { income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
  Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
    186 |
    187 |     if (activeComparison !== 'test') {
  > 188 |       setIncomeReason(selectedData.action.incomeReason);
        |                                    ^^^^^^
    189 |       setExpenseReason(selectedData.action.expenseReason);
    190 |       setCountermeasure(selectedData.action.countermeasure);
    191 |     }

ERROR in src/components/pages/financingPage.tsx:189:37
TS2339: Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; } | { income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
  Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
    187 |     if (activeComparison !== 'test') {
    188 |       setIncomeReason(selectedData.action.incomeReason);
  > 189 |       setExpenseReason(selectedData.action.expenseReason);
        |                                     ^^^^^^
    190 |       setCountermeasure(selectedData.action.countermeasure);
    191 |     }
    192 |     const actionBox = document.querySelector('.actionbox-message');

ERROR in src/components/pages/financingPage.tsx:190:38
TS2339: Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; } | { income: number[]; expense: number[]; action: { incomeReason: string[]; expenseReason: string[]; countermeasure: string; }; }'.
  Property 'action' does not exist on type '{ income: number[]; expense: number[]; balance: number[]; incomeExpenseDiff: number[]; }'.
    188 |       setIncomeReason(selectedData.action.incomeReason);
    189 |       setExpenseReason(selectedData.action.expenseReason);
  > 190 |       setCountermeasure(selectedData.action.countermeasure);
        |                                      ^^^^^^
    191 |     }
    192 |     const actionBox = document.querySelector('.actionbox-message');
    193 |     if (actionBox) {
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, 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[];
  balance?: number[];
  incomeExpenseDiff?: number[];
  action?: {
    incomeReason: string[];
    expenseReason: 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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    let selectedData: DataSet | undefined;

    if (activeComparison === 'test') {
      selectedData = dummyPreviousYearData;
    } else {
      const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
      selectedData = dataSets[key];
    }

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

    if (chartRef.current) {
      const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData!.expense[i]);

      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: 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[]),
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    if (activeComparison !== 'test' && selectedData.action) {
      setIncomeReason(selectedData.action.incomeReason);
      setExpenseReason(selectedData.action.expenseReason);
      setCountermeasure(selectedData.action.countermeasure);
    }
    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('時系列比較')}
              >
                時系列比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === 'test' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('test')}
              >
                test
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
const dummyPreviousYearData = {
  income: [36.0, 35.0, 34.5, 30.0, 36.5, 32.0, 36.0, 35.5, 36.0, 31.0, 36.5, 30.0],
  expense: [-34.0, -32.0, -35.0, -36.0, -33.0, -36.5, -32.0, -36.0, -34.5, -36.5, -31.0, -35.0],
  balance: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
  incomeExpenseDiff: [2.0, 3.0, -0.5, -6.0, 3.5, -4.5, 4.0, -0.5, 1.5, -5.5, 5.5, -5.0],
};
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, 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[];
  balance?: number[];
  incomeExpenseDiff?: number[];
  action?: {
    incomeReason: string[];
    expenseReason: 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 [incomeReason, setIncomeReason] = useState<string[]>([]);
  const [expenseReason, setExpenseReason] = useState<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    let selectedData: DataSet | undefined;

    if (activeComparison === '前年度比較') {
      selectedData = dummyPreviousYearData;
    } else {
      const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
      selectedData = dataSets[key];
    }

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

    if (chartRef.current) {
      const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData!.expense[i]);

      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: 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[]),
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    if (activeComparison !== '前年度比較' && selectedData.action) {
      setIncomeReason(selectedData.action.incomeReason);
      setExpenseReason(selectedData.action.expenseReason);
      setCountermeasure(selectedData.action.countermeasure);
    }
    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
  };

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

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

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

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </button>
            </div>
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className="actionbox-message">
            <div className="action-section">
              <h3>収入が{incomeLevel}と予測される理由</h3>
              <ul>
                {incomeReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>支出が{expenseLevel}と予測される理由</h3>
              <ul>
                {expenseReason.map((reason, index) => (
                  <li key={index}>{reason}</li>
                ))}
              </ul>
            </div>
            <div className="action-section">
              <h3>対策</h3>
              <p>{countermeasure}</p>
            </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>
  );
};
kirin-ri commented 2 months ago
const handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  setIncomeLevel(e.target.value);
  setActiveComparison('今年度比較'); // 切换到“今年度比較”
};

const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  setExpenseLevel(e.target.value);
  setActiveComparison('今年度比較'); // 切换到“今年度比較”
};
kirin-ri commented 2 months ago
Files successfully emitted, waiting for typecheck results...
Issues checking in progress...
ERROR in src/components/pages/financingPage.tsx:214:9
TS2451: Cannot redeclare block-scoped variable 'handleIncomeChange'.
    212 |   }, [incomeLevel, expenseLevel, activeComparison]);
    213 |
  > 214 |   const handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        |         ^^^^^^^^^^^^^^^^^^
    215 |     setIncomeLevel(e.target.value);
    216 |   };
    217 |

ERROR in src/components/pages/financingPage.tsx:218:9
TS2451: Cannot redeclare block-scoped variable 'handleExpenseChange'.
    216 |   };
    217 |
  > 218 |   const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        |         ^^^^^^^^^^^^^^^^^^^
    219 |     setExpenseLevel(e.target.value);
    220 |   };
    221 |

ERROR in src/components/pages/financingPage.tsx:226:9
TS2451: Cannot redeclare block-scoped variable 'handleIncomeChange'.
    224 |   };
    225 |
  > 226 |   const handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        |         ^^^^^^^^^^^^^^^^^^
    227 |     setIncomeLevel(e.target.value);
    228 |     setActiveComparison('今年度比較'); // 切换到“今年度比較”
    229 |   };

ERROR in src/components/pages/financingPage.tsx:231:9
TS2451: Cannot redeclare block-scoped variable 'handleExpenseChange'.
    229 |   };
    230 |
  > 231 |   const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        |         ^^^^^^^^^^^^^^^^^^^
    232 |     setExpenseLevel(e.target.value);
    233 |     setActiveComparison('今年度比較'); // 切换到“今年度比較”
    234 |   };
kirin-ri commented 2 months ago
const handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  setIncomeLevel(e.target.value);
  setActiveComparison('今年度比較'); // 切换到“今年度比較”
};

const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  setExpenseLevel(e.target.value);
  setActiveComparison('今年度比較'); // 切换到“今年度比較”
};
kirin-ri commented 2 months ago
{
  "action": {
    "cashFlow": "昨年の現金の流れは比較的安定していましたが、4月と12月に特に注意が必要でした。今年の4月から6月のデータでは、収入が前年同期よりも若干低下しているものの、支出の管理がより効率的であったため、全体としての現金の流れは安定しています。7月以降も、収入の増加が期待されますが、支出の抑制が鍵となります。",
    "countermeasure": "銀行としては、企業の資金繰りを支援するために、柔軟な融資条件の提供を検討することが重要です。特に、短期的な運転資金の貸付を拡大し、企業が収益機会を最大限に活かせるようにサポートすることが求められます。また、為替リスクに対応した金融商品の提供や、資産運用のアドバイスを通じて、企業の財務安定を支援することが効果的です。"
  }
}
kirin-ri commented 2 months ago
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "cashFlow": "昨年の現金の流れは比較的安定していましたが、4月と12月に特に注意が必要でした。今年の4月から6月のデータでは、収入が前年同期よりも若干低下しているものの、支出の管理がより効率的であったため、全体としての現金の流れは安定しています。7月以降も、収入の増加が期待されますが、支出の抑制が鍵となります。",
      "countermeasure": "銀行としては、企業の資金繰りを支援するために、柔軟な融資条件の提供を検討することが重要です。特に、短期的な運転資金の貸付を拡大し、企業が収益機会を最大限に活かせるようにサポートすることが求められます。また、為替リスクに対応した金融商品の提供や、資産運用のアドバイスを通じて、企業の財務安定を支援することが効果的です。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "cashFlow": "昨年は現金の流れが年間を通して安定していましたが、特に後半での支出管理が課題となりました。今年の前半では、収入は前年並みで推移している一方で、支出がやや増加しています。このため、後半にかけては支出管理がさらに重要になります。",
      "countermeasure": "銀行としては、企業が支出管理を改善できるように、コスト削減に関連するコンサルティングサービスの提供を検討すべきです。また、キャッシュフローの安定化を図るための短期融資や、支出が増加する期間に備えた資金計画の策定をサポートすることが求められます。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は収入が安定していたものの、設備投資や労務費の増加により、現金の流れが後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、特に支出が収入を上回るリスクが高まっています。",
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "cashFlow": "昨年は全体的に安定した現金の流れが見られましたが、今年の前半では収入が予想を下回る一方で、支出管理が良好であったため、現金の流れは比較的安定しています。今後の成長のためには、収入増加のための戦略が重要です。",
      "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],
    "action": {
      "cashFlow": "昨年の現金の流れは非常に安定していました。今年の前半でも、収入と支出のバランスが取れており、全体として安定した現金の流れを維持しています。今後も同様のパターンが続くと予想されます。",
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は支出が増加し、現金の流れが後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、収入は安定していますが、支出の増加により現金の流れが厳しくなっています。",
      "countermeasure": "銀行としては、企業の支出削減を支援するためのコンサルティングサービスを提供することが重要です。また、短期的な資金ニーズに対応するための融資プログラムの提供や、支出増加に備えたリスクヘッジ手段の提案も有効です。企業が支出管理を強化することで、現金の流れを改善するための具体的なアドバイスを提供することが求められます。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "cashFlow": "昨年は収入の減少と支出の抑制により、現金の流れが後半にかけて改善されましたが、今年の前半では収入の減少が続き、支出の抑制も限界に達しています。",
      "countermeasure": "銀行としては、企業の収益性向上を支援するために、ビジネスモデルの転換をサポートする資金提供を検討すべきです。また、既存事業の効率化を促進するための設備投資支援や、コスト管理の改善に役立つツールの提供が重要です。さらに、企業が収益性の高い市場での競争力を維持できるよう、戦略的なアドバイスを行うことが求められます。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "cashFlow": "昨年の現金の流れは、支出の抑制により後半にかけて若干の改善が見られましたが、今年の前半では収入の減少が続き、支出の増加が懸念されています。現金の流れが厳しい状況です。",
      "countermeasure": "銀行としては、企業の現金流の改善を支援するために、短期融資や支出削減をサポートするコンサルティングサービスを提供することが重要です。また、財務リスクを最小限に抑えるための戦略的アドバイスや、資金運用の効率化を促進するための金融商品の提供も検討すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は支出の増加により現金の流れが悪化し、今年の前半でも同様の傾向が見られます。収入の減少とコストの増加が重なり、現金の流れが非常に厳しい状況です。",
      "countermeasure": "銀行としては、企業が速やかに財務の再構築を行えるように、リストラ資金の提供や、不要な投資の見直しを支援するプログラムを提供することが求められます。また、戦略的な投資をサポートするための資金提供や、企業が新たな収益源を開拓できるような金融サービスを提供することも重要です。"
    }
  }
};
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:110:7
TS2322: Type '{ income: number[]; expense: number[]; action: { cashFlow: string; countermeasure: string; }; } | { income: number[]; expense: number[]; action: { cashFlow: string; countermeasure: string; }; } | { income: number[]; expense: number[]; action: { ...; }; } | ... 5 more ... | { ...; }' is not assignable to type 'DataSet | undefined'.
  Type '{ income: number[]; expense: number[]; action: { cashFlow: string; countermeasure: string; }; }' is not assignable to type 'DataSet'.
    Types of property 'action' are incompatible.
      Type '{ cashFlow: string; countermeasure: string; }' is missing the following properties from type '{ incomeReason: string[]; expenseReason: string[]; countermeasure: string; }': incomeReason, expenseReason
    108 |     } else {
    109 |       const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
  > 110 |       selectedData = dataSets[key];
        |       ^^^^^^^^^^^^
    111 |     }
    112 |
    113 |     if (!selectedData) {

ERROR in src/components/pages/financingPage.tsx:199:39
TS2339: Property 'cashFlow' does not exist on type '{ incomeReason: string[]; expenseReason: string[]; countermeasure: string; }'.
    197 |
    198 |     if (activeComparison !== '前年度比較' && selectedData.action) {
  > 199 |       setCashFlow(selectedData.action.cashFlow);
        |                                       ^^^^^^^^
    200 |       setCountermeasure(selectedData.action.countermeasure);
    201 |     }
    202 |     const actionBox = document.querySelector('.actionbox-message');
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, 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[];
  balance?: number[];
  incomeExpenseDiff?: 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<string[]>([]);
  const [countermeasure, setCountermeasure] = useState('');
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    let selectedData: DataSet | undefined;

    if (activeComparison === '前年度比較') {
      selectedData = dummyPreviousYearData;
    } else {
      const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
      selectedData = dataSets[key];
    }

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

    if (chartRef.current) {
      const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData!.expense[i]);

      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: 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[]),
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
          {
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          }
        ],
      };

      chartRef.current.update();
    }

    if (activeComparison !== '前年度比較' && 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, activeComparison]);

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  const handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setIncomeLevel(e.target.value);
    setActiveComparison('今年度比較'); // 切换到“今年度比較”
  };

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setExpenseLevel(e.target.value);
    setActiveComparison('今年度比較'); // 切换到“今年度比較”
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </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>
              <p>{countermeasure}</p>
            </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 dummyPreviousYearData = {
  income: [36.0, 35.0, 34.5, 30.0, 36.5, 32.0, 36.0, 35.5, 36.0, 31.0, 36.5, 30.0],
  expense: [-34.0, -32.0, -35.0, -36.0, -33.0, -36.5, -32.0, -36.0, -34.5, -36.5, -31.0, -35.0],
  balance: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
  incomeExpenseDiff: [2.0, 3.0, -0.5, -6.0, 3.5, -4.5, 4.0, -0.5, 1.5, -5.5, 5.5, -5.0],
};
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "cashFlow": "昨年の現金の流れは比較的安定していましたが、4月と12月に特に注意が必要でした。今年の4月から6月のデータでは、収入が前年同期よりも若干低下しているものの、支出の管理がより効率的であったため、全体としての現金の流れは安定しています。7月以降も、収入の増加が期待されますが、支出の抑制が鍵となります。",
      "countermeasure": "銀行としては、企業の資金繰りを支援するために、柔軟な融資条件の提供を検討することが重要です。特に、短期的な運転資金の貸付を拡大し、企業が収益機会を最大限に活かせるようにサポートすることが求められます。また、為替リスクに対応した金融商品の提供や、資産運用のアドバイスを通じて、企業の財務安定を支援することが効果的です。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "cashFlow": "昨年は現金の流れが年間を通して安定していましたが、特に後半での支出管理が課題となりました。今年の前半では、収入は前年並みで推移している一方で、支出がやや増加しています。このため、後半にかけては支出管理がさらに重要になります。",
      "countermeasure": "銀行としては、企業が支出管理を改善できるように、コスト削減に関連するコンサルティングサービスの提供を検討すべきです。また、キャッシュフローの安定化を図るための短期融資や、支出が増加する期間に備えた資金計画の策定をサポートすることが求められます。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は収入が安定していたものの、設備投資や労務費の増加により、現金の流れが後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、特に支出が収入を上回るリスクが高まっています。",
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "cashFlow": "昨年は全体的に安定した現金の流れが見られましたが、今年の前半では収入が予想を下回る一方で、支出管理が良好であったため、現金の流れは比較的安定しています。今後の成長のためには、収入増加のための戦略が重要です。",
      "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],
    "action": {
      "cashFlow": "昨年の現金の流れは非常に安定していました。今年の前半でも、収入と支出のバランスが取れており、全体として安定した現金の流れを維持しています。今後も同様のパターンが続くと予想されます。",
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は支出が増加し、現金の流れが後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、収入は安定していますが、支出の増加により現金の流れが厳しくなっています。",
      "countermeasure": "銀行としては、企業の支出削減を支援するためのコンサルティングサービスを提供することが重要です。また、短期的な資金ニーズに対応するための融資プログラムの提供や、支出増加に備えたリスクヘッジ手段の提案も有効です。企業が支出管理を強化することで、現金の流れを改善するための具体的なアドバイスを提供することが求められます。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "cashFlow": "昨年は収入の減少と支出の抑制により、現金の流れが後半にかけて改善されましたが、今年の前半では収入の減少が続き、支出の抑制も限界に達しています。",
      "countermeasure": "銀行としては、企業の収益性向上を支援するために、ビジネスモデルの転換をサポートする資金提供を検討すべきです。また、既存事業の効率化を促進するための設備投資支援や、コスト管理の改善に役立つツールの提供が重要です。さらに、企業が収益性の高い市場での競争力を維持できるよう、戦略的なアドバイスを行うことが求められます。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "cashFlow": "昨年の現金の流れは、支出の抑制により後半にかけて若干の改善が見られましたが、今年の前半では収入の減少が続き、支出の増加が懸念されています。現金の流れが厳しい状況です。",
      "countermeasure": "銀行としては、企業の現金流の改善を支援するために、短期融資や支出削減をサポートするコンサルティングサービスを提供することが重要です。また、財務リスクを最小限に抑えるための戦略的アドバイスや、資金運用の効率化を促進するための金融商品の提供も検討すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は支出の増加により現金の流れが悪化し、今年の前半でも同様の傾向が見られます。収入の減少とコストの増加が重なり、現金の流れが非常に厳しい状況です。",
      "countermeasure": "銀行としては、企業が速やかに財務の再構築を行えるように、リストラ資金の提供や、不要な投資の見直しを支援するプログラムを提供することが求められます。また、戦略的な投資をサポートするための資金提供や、企業が新たな収益源を開拓できるような金融サービスを提供することも重要です。"
    }
  }
};

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
ERROR in src/components/pages/financingPage.tsx:198:19
TS2345: Argument of type 'string' is not assignable to parameter of type 'SetStateAction<string[]>'.
    196 |
    197 |     if (activeComparison !== '前年度比較' && selectedData.action) {
  > 198 |       setCashFlow(selectedData.action.cashFlow);
        |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    199 |       setCountermeasure(selectedData.action.countermeasure);
    200 |     }
    201 |     const actionBox = document.querySelector('.actionbox-message');
kirin-ri commented 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, 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[];
  balance?: number[];
  incomeExpenseDiff?: 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: 30,
        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: '[百万円]', // Y-axis label
        },
      },
    },
  };

  const updateChartAndAction = () => {
    let selectedData: DataSet | undefined;

    if (activeComparison === '前年度比較') {
      selectedData = dummyPreviousYearData;
    } else {
      const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
      selectedData = dataSets[key];
    }

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

    if (chartRef.current) {
      const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData!.expense[i]);

      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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'blue';
            }
          },
          {
            type: 'line',
            label: '期末残高',
            data: 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[]),
            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]; // 4月から6月は実線、7月から3月は破線
              },
            },
            pointBackgroundColor: function (context) {
              const index = context.dataIndex;
              const value = context.dataset.data[index] ?? 0;
              return value < 0 ? 'red' : 'black';
            }
          },
        ],
      };

      chartRef.current.update();
    }

    if (activeComparison !== '前年度比較' && 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, activeComparison]);

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  const handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setIncomeLevel(e.target.value);
    setActiveComparison('今年度比較'); // 切换到“今年度比較”
  };

  const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setExpenseLevel(e.target.value);
    setActiveComparison('今年度比較'); // 切换到“今年度比較”
  };

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

  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 = 100;
      const startX = (chart.width - legend.legendItems.length * itemWidth) / 2;
      let currentX = startX;

      const y = 10;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        if (legendItem.text === '期末残高' || legendItem.text === '収支差') {
          ctx.save();
          ctx.strokeStyle = legendItem.text === '期末残高' ? 'black' : 'blue';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(currentX, y);
          ctx.lineTo(currentX + 40, y);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(currentX, y - 5, 40, 10);
          ctx.restore();
        }

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

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

  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={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <div className="filter-btn">支出</div>
                <select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
            </div>
            <div className="data-comparison">
              <h2>データ比較</h2>
              <button
                className={`comparison-btn ${activeComparison === '今年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('今年度比較')}
              >
                今年度比較
              </button>
              <button
                className={`comparison-btn ${activeComparison === '前年度比較' ? 'active' : ''}`}
                onClick={() => handleComparisonClick('前年度比較')}
              >
                前年度比較
              </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>
              <p>{countermeasure}</p>
            </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 dummyPreviousYearData = {
  income: [36.0, 35.0, 34.5, 30.0, 36.5, 32.0, 36.0, 35.5, 36.0, 31.0, 36.5, 30.0],
  expense: [-34.0, -32.0, -35.0, -36.0, -33.0, -36.5, -32.0, -36.0, -34.5, -36.5, -31.0, -35.0],
  balance: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
  incomeExpenseDiff: [2.0, 3.0, -0.5, -6.0, 3.5, -4.5, 4.0, -0.5, 1.5, -5.5, 5.5, -5.0],
};
const dataSets = {
  "楽観-楽観": {
    "income": [34.0, 35.5, 34.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0],
    "expense": [-33.0, -34.0, -35.0, -33.5, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5],
    "action": {
      "cashFlow": "昨年の現金の流れは比較的安定していましたが、4月と12月に特に注意が必要でした。今年の4月から6月のデータでは、収入が前年同期よりも若干低下しているものの、支出の管理がより効率的であったため、全体としての現金の流れは安定しています。7月以降も、収入の増加が期待されますが、支出の抑制が鍵となります。",
      "countermeasure": "銀行としては、企業の資金繰りを支援するために、柔軟な融資条件の提供を検討することが重要です。特に、短期的な運転資金の貸付を拡大し、企業が収益機会を最大限に活かせるようにサポートすることが求められます。また、為替リスクに対応した金融商品の提供や、資産運用のアドバイスを通じて、企業の財務安定を支援することが効果的です。"
    }
  },
  "楽観-中立": {
    "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.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
    "action": {
      "cashFlow": "昨年は現金の流れが年間を通して安定していましたが、特に後半での支出管理が課題となりました。今年の前半では、収入は前年並みで推移している一方で、支出がやや増加しています。このため、後半にかけては支出管理がさらに重要になります。",
      "countermeasure": "銀行としては、企業が支出管理を改善できるように、コスト削減に関連するコンサルティングサービスの提供を検討すべきです。また、キャッシュフローの安定化を図るための短期融資や、支出が増加する期間に備えた資金計画の策定をサポートすることが求められます。"
    }
  },
  "楽観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は収入が安定していたものの、設備投資や労務費の増加により、現金の流れが後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、特に支出が収入を上回るリスクが高まっています。",
      "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "cashFlow": "昨年は全体的に安定した現金の流れが見られましたが、今年の前半では収入が予想を下回る一方で、支出管理が良好であったため、現金の流れは比較的安定しています。今後の成長のためには、収入増加のための戦略が重要です。",
      "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],
    "action": {
      "cashFlow": "昨年の現金の流れは非常に安定していました。今年の前半でも、収入と支出のバランスが取れており、全体として安定した現金の流れを維持しています。今後も同様のパターンが続くと予想されます。",
      "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は支出が増加し、現金の流れが後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、収入は安定していますが、支出の増加により現金の流れが厳しくなっています。",
      "countermeasure": "銀行としては、企業の支出削減を支援するためのコンサルティングサービスを提供することが重要です。また、短期的な資金ニーズに対応するための融資プログラムの提供や、支出増加に備えたリスクヘッジ手段の提案も有効です。企業が支出管理を強化することで、現金の流れを改善するための具体的なアドバイスを提供することが求められます。"
    }
  },
  "悲観-楽観": {
    "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, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
    "action": {
      "cashFlow": "昨年は収入の減少と支出の抑制により、現金の流れが後半にかけて改善されましたが、今年の前半では収入の減少が続き、支出の抑制も限界に達しています。",
      "countermeasure": "銀行としては、企業の収益性向上を支援するために、ビジネスモデルの転換をサポートする資金提供を検討すべきです。また、既存事業の効率化を促進するための設備投資支援や、コスト管理の改善に役立つツールの提供が重要です。さらに、企業が収益性の高い市場での競争力を維持できるよう、戦略的なアドバイスを行うことが求められます。"
    }
  },
  "悲観-中立": {
    "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],
    "action": {
      "cashFlow": "昨年の現金の流れは、支出の抑制により後半にかけて若干の改善が見られましたが、今年の前半では収入の減少が続き、支出の増加が懸念されています。現金の流れが厳しい状況です。",
      "countermeasure": "銀行としては、企業の現金流の改善を支援するために、短期融資や支出削減をサポートするコンサルティングサービスを提供することが重要です。また、財務リスクを最小限に抑えるための戦略的アドバイスや、資金運用の効率化を促進するための金融商品の提供も検討すべきです。"
    }
  },
  "悲観-悲観": {
    "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, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
    "action": {
      "cashFlow": "昨年は支出の増加により現金の流れが悪化し、今年の前半でも同様の傾向が見られます。収入の減少とコストの増加が重なり、現金の流れが非常に厳しい状況です。",
      "countermeasure": "銀行としては、企業が速やかに財務の再構築を行えるように、リストラ資金の提供や、不要な投資の見直しを支援するプログラムを提供することが求められます。また、戦略的な投資をサポートするための資金提供や、企業が新たな収益源を開拓できるような金融サービスを提供することも重要です。"
    }
  }
};

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 updateChartAndAction = () => {
    let selectedData: DataSet | undefined;

    if (activeComparison === '前年度比較') {
        selectedData = dummyPreviousYearData;
    } else {
        const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
        selectedData = dataSets[key];
    }

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

    const isPreviousYear = activeComparison === '前年度比較';
    const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData!.expense[i]);

    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 isPreviousYear ? [] : (ctx.p0DataIndex < 3 ? [] : [5, 5]); // 今年度は7月以降破線、前年度はすべて実線
                        },
                    },
                    pointBackgroundColor: function (context) {
                        const index = context.dataIndex;
                        const value = context.dataset.data[index] ?? 0;
                        return value < 0 ? 'red' : 'blue';
                    }
                },
                {
                    type: 'line',
                    label: '期末残高',
                    data: 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[]),
                    borderColor: 'black',
                    backgroundColor: 'black',
                    fill: false,
                    tension: 0.1,
                    borderWidth: 2,
                    pointStyle: 'rectRot',
                    pointRadius: 6,
                    pointHoverRadius: 8,
                    segment: {
                        borderDash: (ctx) => {
                            return isPreviousYear ? [] : (ctx.p0DataIndex < 3 ? [] : [5, 5]); // 今年度は7月以降破線、前年度はすべて実線
                        },
                    },
                    pointBackgroundColor: function (context) {
                        const index = context.dataIndex;
                        const value = context.dataset.data[index] ?? 0;
                        return value < 0 ? 'red' : 'black';
                    }
                },
            ],
        };

        chartRef.current.update();
    }

    if (activeComparison !== '前年度比較' && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
    }

    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
};
kirin-ri commented 2 months ago
const updateChartAndAction = () => {
    let selectedData: DataSet | undefined;

    if (activeComparison === '前年度比較') {
        selectedData = dummyPreviousYearData;
    } else {
        const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
        selectedData = dataSets[key];
    }

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

    const isPreviousYear = activeComparison === '前年度比較';
    const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData!.expense[i]);

    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 isPreviousYear ? 'rgba(153, 102, 255, 1.0)' : (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 isPreviousYear ? 'rgba(54, 162, 235, 1.0)' : (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 isPreviousYear ? [] : (ctx.p0DataIndex < 3 ? [] : [5, 5]); // 今年度は7月以降破線、前年度はすべて実線
                        },
                    },
                    pointBackgroundColor: function (context) {
                        const index = context.dataIndex;
                        const value = context.dataset.data[index] ?? 0;
                        return value < 0 ? 'red' : 'blue';
                    }
                },
                {
                    type: 'line',
                    label: '期末残高',
                    data: 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[]),
                    borderColor: 'black',
                    backgroundColor: 'black',
                    fill: false,
                    tension: 0.1,
                    borderWidth: 2,
                    pointStyle: 'rectRot',
                    pointRadius: 6,
                    pointHoverRadius: 8,
                    segment: {
                        borderDash: (ctx) => {
                            return isPreviousYear ? [] : (ctx.p0DataIndex < 3 ? [] : [5, 5]); // 今年度は7月以降破線、前年度はすべて実線
                        },
                    },
                    pointBackgroundColor: function (context) {
                        const index = context.dataIndex;
                        const value = context.dataset.data[index] ?? 0;
                        return value < 0 ? 'red' : 'black';
                    }
                },
            ],
        };

        chartRef.current.update();
    }

    if (activeComparison !== '前年度比較' && selectedData.action) {
        setCashFlow(selectedData.action.cashFlow);
        setCountermeasure(selectedData.action.countermeasure);
    }

    const actionBox = document.querySelector('.actionbox-message');
    if (actionBox) {
      actionBox.scrollTop = 0;
    }
};
kirin-ri commented 2 months ago
const yearLabelPlugin = {
    id: 'yearLabel',
    afterDraw: (chart) => {
        const ctx = chart.ctx;
        ctx.save();
        ctx.font = '16px Arial';
        ctx.fillStyle = 'black';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';

        const yearText = activeComparison === '前年度比較' ? '2023年' : '2024年';
        ctx.fillText(yearText, 10, 10); // 在图表的左上角绘制年份

        ctx.restore();
    }
};
useEffect(() => {
    ChartJS.register(customLegendPlugin, yearLabelPlugin);
    return () => {
      ChartJS.unregister(customLegendPlugin, yearLabelPlugin);
    };
}, []);
kirin-ri commented 2 months ago
ERROR in src/components/pages/financingPage.tsx:231:17
TS7006: Parameter 'chart' implicitly has an 'any' type.
    229 |   const yearLabelPlugin = {
    230 |     id: 'yearLabel',
  > 231 |     afterDraw: (chart) => {
        |                 ^^^^^
    232 |         const ctx = chart.ctx;
    233 |         ctx.save();
    234 |         ctx.font = '16px Arial';
kirin-ri commented 2 months ago
        const legendHeight = chart.legend?.height || 0;
        const offset = 10; // 增加与图例之间的距离
        const yPos = legendHeight + offset;

        ctx.fillText(yearText, chart.width / 2, yPos); // 在图表的最上部正中间绘制年份,并增加与图例的距离

        ctx.restore();