kirin-ri / memo

0 stars 0 forks source link

mock #28

Open kirin-ri opened 3 months ago

kirin-ri commented 3 months ago
(base) q_li@vm-I-DNA-daas-2:~/Desktop/work/catalog-web-app-demo/client$ git push -u origin master
To https://github.com/qmonus-test/mfgmock-catalog-web-app
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://github.com/qmonus-test/mfgmock-catalog-web-app'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{detail.amount}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const chartRef = useRef<ChartJS | null>(null);
  const [activeComparison, setActiveComparison] = useState('時系列比較');

  useEffect(() => {
    const chart = chartRef.current;
    if (chart) {
      const ctx = chart.ctx;
      if (ctx && chart.data.datasets[2].data) {
        (chart.data.datasets[2].data as number[]).forEach((value, index) => {
          if (value < 0) {
            const meta = chart.getDatasetMeta(2).data[index];
            if (meta) {
              const x = meta.x;
              const y = meta.y;
              ctx.fillStyle = 'red';
              ctx.font = 'bold 12px Arial';
              ctx.fillText('!', x - 5, y - 10);
            }
          }
        });
      }
    }
  }, []);

  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 data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        position: 'top' as const,
        labels: {
          font: {
            size: 16
          }
        }
      },
      tooltip: {
        callbacks: {
          label: function(context: any) {
            const label = context.dataset.label || '';
            const value = context.raw;
            return `${label}: ${value}`;
          }
        }
      },
    },
    scales: {
      y: {
        beginAtZero: true,
      }
    }
  };

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

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ絞り込み</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

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

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{detail.amount}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const chartRef = useRef<ChartJS | null>(null);
  const [activeComparison, setActiveComparison] = useState('時系列比較');

  useEffect(() => {
    const chart = chartRef.current;
    if (chart) {
      const ctx = chart.ctx;
      if (ctx && chart.data.datasets[2].data) {
        (chart.data.datasets[2].data as number[]).forEach((value, index) => {
          if (value < 0) {
            const meta = chart.getDatasetMeta(2).data[index];
            if (meta) {
              const x = meta.x;
              const y = meta.y;
              ctx.fillStyle = 'red';
              ctx.font = 'bold 12px Arial';
              ctx.fillText('!', x - 5, y - 10);
            }
          }
        });
      }
    }
  }, []);

  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 data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 1, // 较细的边框
        pointStyle: 'rectRot',
        pointRadius: 4, // 较小的方块
        pointHoverRadius: 6,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        position: 'top' as const,
        labels: {
          font: {
            size: 16
          }
        }
      },
      tooltip: {
        callbacks: {
          label: function(context: any) {
            const label = context.dataset.label || '';
            const value = context.raw;
            return `${label}: ${value}`;
          }
        }
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

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

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ絞り込み</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{detail.amount}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const chartRef = useRef<ChartJS | null>(null);
  const [activeComparison, setActiveComparison] = useState('時系列比較');

  useEffect(() => {
    const chart = chartRef.current;
    if (chart) {
      const ctx = chart.ctx;
      if (ctx && chart.data.datasets[2].data) {
        (chart.data.datasets[2].data as number[]).forEach((value, index) => {
          if (value < 0) {
            const meta = chart.getDatasetMeta(2).data[index];
            if (meta) {
              const x = meta.x;
              const y = meta.y;
              ctx.fillStyle = 'red';
              ctx.font = 'bold 12px Arial';
              ctx.fillText('!', x - 5, y - 10);
            }
          }
        });
      }
    }
  }, []);

  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 data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2, // 线条粗细
        pointStyle: 'rectRot',
        pointRadius: 6, // 方块大小
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        position: 'top' as const,
        labels: {
          generateLabels: function(chart) {
            const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
            originalLabels.forEach(label => {
              if (label.text === '資金繰り') {
                label.boxWidth = 40; // 设置为线条的宽度
                label.boxHeight = 2; // 设置为线条的高度
              } else {
                label.boxWidth = 20; // 保持其他图标为方块
                label.boxHeight = 20;
              }
            });
            return originalLabels;
          },
          font: {
            size: 16, // 标签字体大小
          },
        }
      },
      tooltip: {
        callbacks: {
          label: function(context: any) {
            const label = context.dataset.label || '';
            const value = context.raw;
            return `${label}: ${value}`;
          }
        }
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:133:36
TS7006: Parameter 'chart' implicitly has an 'any' type.
    131 |         position: 'top' as const,
    132 |         labels: {
  > 133 |           generateLabels: function(chart) {
        |                                    ^^^^^
    134 |             const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
    135 |             originalLabels.forEach(label => {
    136 |               if (label.text === '資金繰り') {

ERROR in src/components/pages/financingPage.tsx:137:23
TS2339: Property 'boxWidth' does not exist on type 'LegendItem'.
    135 |             originalLabels.forEach(label => {
    136 |               if (label.text === '資金繰り') {
  > 137 |                 label.boxWidth = 40; // 设置为线条的宽度
        |                       ^^^^^^^^
    138 |                 label.boxHeight = 2; // 设置为线条的高度
    139 |               } else {
    140 |                 label.boxWidth = 20; // 保持其他图标为方块

ERROR in src/components/pages/financingPage.tsx:138:23
TS2339: Property 'boxHeight' does not exist on type 'LegendItem'.
    136 |               if (label.text === '資金繰り') {
    137 |                 label.boxWidth = 40; // 设置为线条的宽度
  > 138 |                 label.boxHeight = 2; // 设置为线条的高度
        |                       ^^^^^^^^^
    139 |               } else {
    140 |                 label.boxWidth = 20; // 保持其他图标为方块
    141 |                 label.boxHeight = 20;

ERROR in src/components/pages/financingPage.tsx:140:23
TS2339: Property 'boxWidth' does not exist on type 'LegendItem'.
    138 |                 label.boxHeight = 2; // 设置为线条的高度
    139 |               } else {
  > 140 |                 label.boxWidth = 20; // 保持其他图标为方块
        |                       ^^^^^^^^
    141 |                 label.boxHeight = 20;
    142 |               }
    143 |             });

ERROR in src/components/pages/financingPage.tsx:141:23
TS2339: Property 'boxHeight' does not exist on type 'LegendItem'.
    139 |               } else {
    140 |                 label.boxWidth = 20; // 保持其他图标为方块
  > 141 |                 label.boxHeight = 20;
        |                       ^^^^^^^^^
    142 |               }
    143 |             });
    144 |             return originalLabels;
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip, LegendItem } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{detail.amount}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const chartRef = useRef<ChartJS | null>(null);
  const [activeComparison, setActiveComparison] = useState('時系列比較');

  useEffect(() => {
    const chart = chartRef.current;
    if (chart) {
      const ctx = chart.ctx;
      if (ctx && chart.data.datasets[2].data) {
        (chart.data.datasets[2].data as number[]).forEach((value, index) => {
          if (value < 0) {
            const meta = chart.getDatasetMeta(2).data[index];
            if (meta) {
              const x = meta.x;
              const y = meta.y;
              ctx.fillStyle = 'red';
              ctx.font = 'bold 12px Arial';
              ctx.fillText('!', x - 5, y - 10);
            }
          }
        });
      }
    }
  }, []);

  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 data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2, // 线条粗细
        pointStyle: 'rectRot',
        pointRadius: 6, // 方块大小
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        position: 'top' as const,
        labels: {
          generateLabels: function(chart: ChartJS) {
            const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
            originalLabels.forEach((label: LegendItem) => {
              if (label.text === '資金繰り') {
                label.lineWidth = 2; // 设置为线条的宽度
                label.strokeStyle = 'black'; // 设置线条颜色
              } else {
                label.boxWidth = 20; // 保持其他图标为方块
                label.boxHeight = 20;
              }
            });
            return originalLabels;
          },
          font: {
            size: 16, // 标签字体大小
          },
        }
      },
      tooltip: {
        callbacks: {
          label: function(context: any) {
            const label = context.dataset.label || '';
            const value = context.raw;
            return `${label}: ${value}`;
          }
        }
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

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

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ絞り込み</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:140:23
TS2339: Property 'boxWidth' does not exist on type 'LegendItem'.
    138 |                 label.strokeStyle = 'black'; // 设置线条颜色
    139 |               } else {
  > 140 |                 label.boxWidth = 20; // 保持其他图标为方块
        |                       ^^^^^^^^
    141 |                 label.boxHeight = 20;
    142 |               }
    143 |             });

ERROR in src/components/pages/financingPage.tsx:141:23
TS2339: Property 'boxHeight' does not exist on type 'LegendItem'.
    139 |               } else {
    140 |                 label.boxWidth = 20; // 保持其他图标为方块
  > 141 |                 label.boxHeight = 20;
        |                       ^^^^^^^^^
    142 |               }
    143 |             });
    144 |             return originalLabels;
kirin-ri commented 3 months ago
 const options = {
    responsive: true,
    plugins: {
      legend: {
        position: 'top' as const,
        labels: {
          generateLabels: function(chart: ChartJS) {
            const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
            originalLabels.forEach((label: LegendItem) => {
              if (label.text === '資金繰り') {
                label.lineWidth = 2; // 设置为线条的宽度
                label.strokeStyle = 'black'; // 设置线条颜色
                label.pointStyle = false; // 不使用点样式
              } else {
                label.pointStyle = true; // 保持其他图标为方块
              }
            });
            return originalLabels;
          },
          font: {
            size: 16, // 标签字体大小
          },
        }
      },
      tooltip: {
        callbacks: {
          label: function(context: any) {
            const label = context.dataset.label || '';
            const value = context.raw;
            return `${label}: ${value}`;
          }
        }
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:135:44
TS2304: Cannot find name 'LegendItem'.
    133 |           generateLabels: function(chart: ChartJS) {
    134 |             const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
  > 135 |             originalLabels.forEach((label: LegendItem) => {
        |                                            ^^^^^^^^^^
    136 |               if (label.text === '資金繰り') {
    137 |                 label.lineWidth = 2; // 设置为线条的宽度
    138 |                 label.strokeStyle = 'black'; // 设置线条颜色
kirin-ri commented 3 months ago
      legend: {
        position: 'top' as const,
        labels: {
          generateLabels: function(chart) {
            const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
            originalLabels.forEach(label => {
              if (label.text === '資金繰り') {
                label.lineWidth = 2; // 设置为线条的宽度
                label.strokeStyle = 'black'; // 设置线条颜色
                label.pointStyle = false; // 不使用点样式
              } else {
                label.pointStyle = true; // 保持其他图标为方块
              }
            });
            return originalLabels;
          },
          font: {
            size: 16, // 标签字体大小
          },
        }
      },
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:133:36
TS7006: Parameter 'chart' implicitly has an 'any' type.
    131 |         position: 'top' as const,
    132 |         labels: {
  > 133 |           generateLabels: function(chart) {
        |                                    ^^^^^
    134 |             const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
    135 |             originalLabels.forEach(label => {
    136 |               if (label.text === '資金繰り') {

ERROR in src/components/pages/financingPage.tsx:139:17
TS2322: Type 'false' is not assignable to type 'PointStyle | undefined'.
    137 |                 label.lineWidth = 2; // 设置为线条的宽度
    138 |                 label.strokeStyle = 'black'; // 设置线条颜色
  > 139 |                 label.pointStyle = false; // 不使用点样式
        |                 ^^^^^^^^^^^^^^^^
    140 |               } else {
    141 |                 label.pointStyle = true; // 保持其他图标为方块
    142 |               }

ERROR in src/components/pages/financingPage.tsx:141:17
TS2322: Type 'true' is not assignable to type 'PointStyle | undefined'.
    139 |                 label.pointStyle = false; // 不使用点样式
    140 |               } else {
  > 141 |                 label.pointStyle = true; // 保持其他图标为方块
        |                 ^^^^^^^^^^^^^^^^
    142 |               }
    143 |             });
    144 |             return originalLabels;``
kirin-ri commented 3 months ago
generateLabels: function(chart: ChartJS) {
  const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
  originalLabels.forEach(label => {
    if (label.text === '資金繰り') {
      label.lineWidth = 2; // 设置为线条的宽度
      label.strokeStyle = 'black'; // 设置线条颜色
      label.pointStyle = 'line'; // 设置为线条样式
    } else {
      label.pointStyle = 'rect'; // 保持其他图标为方块
    }
  });
  return originalLabels;
},
kirin-ri commented 3 months ago
generateLabels: function(chart: ChartJS) {
  const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
  originalLabels.forEach(label => {
    if (label.text === '資金繰り') {
      label.strokeStyle = label.fillStyle; // 使用同样的颜色
      label.lineWidth = 2; // 设置为线条的宽度
      label.borderDash = [4, 4]; // 设置虚线样式,可以移除该行保持实线
      label.usePointStyle = false; // 禁用默认的点样式
    } else {
      label.usePointStyle = true; // 保持其他图标为方块
    }
  });
  return originalLabels;
},
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:139:23
TS2339: Property 'borderDash' does not exist on type 'LegendItem'.
    137 |                 label.strokeStyle = label.fillStyle; // 使用同样的颜色
    138 |                 label.lineWidth = 2; // 设置为线条的宽度
  > 139 |                 label.borderDash = [4, 4]; // 设置虚线样式,可以移除该行保持实线
        |                       ^^^^^^^^^^
    140 |                 label.usePointStyle = false; // 禁用默认的点样式
    141 |               } else {
    142 |                 label.usePointStyle = true; // 保持其他图标为方块

ERROR in src/components/pages/financingPage.tsx:140:23
TS2551: Property 'usePointStyle' does not exist on type 'LegendItem'. Did you mean 'pointStyle'?
    138 |                 label.lineWidth = 2; // 设置为线条的宽度
    139 |                 label.borderDash = [4, 4]; // 设置虚线样式,可以移除该行保持实线
  > 140 |                 label.usePointStyle = false; // 禁用默认的点样式
        |                       ^^^^^^^^^^^^^
    141 |               } else {
    142 |                 label.usePointStyle = true; // 保持其他图标为方块
    143 |               }

ERROR in src/components/pages/financingPage.tsx:142:23
TS2551: Property 'usePointStyle' does not exist on type 'LegendItem'. Did you mean 'pointStyle'?
    140 |                 label.usePointStyle = false; // 禁用默认的点样式
    141 |               } else {
  > 142 |                 label.usePointStyle = true; // 保持其他图标为方块
        |                       ^^^^^^^^^^^^^
    143 |               }
    144 |             });
    145 |             return originalLabels;
kirin-ri commented 3 months ago
generateLabels: function(chart: ChartJS) {
  const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
  originalLabels.forEach(label => {
    if (label.text === '資金繰り') {
      label.lineWidth = 4; // 设置为较粗的线条
      label.fillStyle = label.strokeStyle; // 使用线条颜色作为填充颜色
      label.pointStyle = 'line'; // 将样式设置为线条
    } else {
      label.pointStyle = 'rect'; // 保持其他图标为方块
    }
  });
  return originalLabels;
},
kirin-ri commented 3 months ago
generateLabels: function(chart: ChartJS) {
  const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
  originalLabels.forEach(label => {
    if (label.text === '資金繰り') {
      label.pointStyle = 'line'; // 设置为线条样式
    }
  });
  return originalLabels;
},

// 自定义插件,绘制线条样式的图例
plugins: [{
  beforeDraw: function(chart) {
    const ctx = chart.ctx;
    const legend = chart.legend;

    legend.legendItems.forEach((label) => {
      if (label.text === '資金繰り') {
        const x = label.left + label.width / 2;
        const y = label.top + label.height / 2;

        ctx.save();
        ctx.strokeStyle = label.strokeStyle || 'black';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(x - 10, y);
        ctx.lineTo(x + 10, y);
        ctx.stroke();
        ctx.restore();
      }
    });
  }
}]
kirin-ri commented 3 months ago
import { useState, useEffect, useRef } from 'react';
import { Chart } from 'react-chartjs-2';
import ChartJS from 'chart.js/auto';

const InventoryTurnsPage = () => {
  const pageName = "年間平均在庫回転率";
  const [showAlert, setShowAlert] = useState(true);
  const [chartData, setChartData] = useState(getTimeSeriesData()); // 示例函数,定义在组件内或外部
  const chartRef = useRef<ChartJS | null>(null);

  const options = {
    responsive: true,
    plugins: {
      legend: {
        display: false, // 隐藏默认图例
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

  useEffect(() => {
    const chart = chartRef.current;
    if (chart) {
      const legendContainer = document.getElementById('legend-container');
      if (legendContainer) {
        legendContainer.innerHTML = chart.generateLegend();
      }
    }
  }, [chartData]);

  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="B製品の在庫回転率が悪化しています"
            onClose={() => setShowAlert(false)}
          />
        </div>
      )}
      <div className="inventory-main-content">
        <div className="inventory-left-container">
          <div className="inventory-graph-container">
            <Chart ref={chartRef} type="bar" data={chartData} options={options} />
            <div id="legend-container"></div> {/* 自定义图例容器 */}
          </div>
          <div className="inventory-additional-section">
            {/* 其余代码保持不变 */}
          </div>
        </div>
        <div className="right-container">
          {/* 右侧内容保持不变 */}
        </div>
      </div>
    </div>
  );
};

ChartJS.defaults.plugins.legendCallback = function(chart) {
  const legendItems = chart.legend.legendItems;
  let text = '<ul>';
  legendItems.forEach((legendItem) => {
    const style = legendItem.text === '資金繰り'
      ? `background: none; border-bottom: 2px solid ${legendItem.strokeStyle};`
      : `background-color: ${legendItem.fillStyle};`;

    text += `<li><span style="display:inline-block;width:20px;height:10px;${style}"></span>`;
    text += `${legendItem.text}</li>`;
  });
  text += '</ul>';
  return text;
};

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

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{detail.amount}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const chartRef = useRef<ChartJS | null>(null);
  const [activeComparison, setActiveComparison] = useState('時系列比較');

  useEffect(() => {
    const chart = chartRef.current;
    if (chart) {
      const ctx = chart.ctx;
      if (ctx && chart.data.datasets[2].data) {
        (chart.data.datasets[2].data as number[]).forEach((value, index) => {
          if (value < 0) {
            const meta = chart.getDatasetMeta(2).data[index];
            if (meta) {
              const x = meta.x;
              const y = meta.y;
              ctx.fillStyle = 'red';
              ctx.font = 'bold 12px Arial';
              ctx.fillText('!', x - 5, y - 10);
            }
          }
        });
      }
    }
  }, []);

  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 data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        position: 'top' as const,
        labels: {
          generateLabels: function(chart: ChartJS) {
            const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
            originalLabels.forEach(label => {
              if (label.text === '資金繰り') {
                label.pointStyle = 'line'; // 设置为线条样式
              }
            });
            return originalLabels;
          },
          font: {
            size: 16, // 标签字体大小
          },
        }
      },
      tooltip: {
        callbacks: {
          label: function(context: any) {
            const label = context.dataset.label || '';
            const value = context.raw;
            return `${label}: ${value}`;
          }
        }
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        }
      }
    }
  };

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

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ絞り込み</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

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

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{detail.amount}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const chartRef = useRef<ChartJS | null>(null);
  const [activeComparison, setActiveComparison] = useState('時系列比較');

  useEffect(() => {
    const chart = chartRef.current;
    if (chart) {
      const ctx = chart.ctx;
      if (ctx && chart.data.datasets[2].data) {
        (chart.data.datasets[2].data as number[]).forEach((value, index) => {
          if (value < 0) {
            const meta = chart.getDatasetMeta(2).data[index];
            if (meta) {
              const x = meta.x;
              const y = meta.y;
              ctx.fillStyle = 'red';
              ctx.font = 'bold 12px Arial';
              ctx.fillText('!', x - 5, y - 10);
            }
          }
        });
      }
    }
  }, []);

  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 data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        position: 'top' as const,
        labels: {
          generateLabels: function(chart: ChartJS) {
            const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
            originalLabels.forEach(label => {
              if (label.text === '資金繰り') {
                label.usePointStyle = true;
                label.pointStyle = 'line';
                label.lineWidth = 2; // 设置为线条的宽度
                label.strokeStyle = label.fillStyle; // 使用填充颜色作为线条颜色
              } else {
                label.usePointStyle = false; // 保持其他图标为方块
              }
            });
            return originalLabels;
          },
          font: {
            size: 16, // 标签字体大小
          },
        }
      },
      tooltip: {
        callbacks: {
          label: function(context: any) {
            const label = context.dataset.label || '';
            const value = context.raw;
            return `${label}: ${value}`;
          }
        }
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        }
      }
    }
  };

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

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ絞り込み</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:137:23
TS2551: Property 'usePointStyle' does not exist on type 'LegendItem'. Did you mean 'pointStyle'?
    135 |             originalLabels.forEach(label => {
    136 |               if (label.text === '資金繰り') {
  > 137 |                 label.usePointStyle = true;
        |                       ^^^^^^^^^^^^^
    138 |                 label.pointStyle = 'line';
    139 |                 label.lineWidth = 2; // 设置为线条的宽度
    140 |                 label.strokeStyle = label.fillStyle; // 使用填充颜色作为线条颜色

ERROR in src/components/pages/financingPage.tsx:142:23
TS2551: Property 'usePointStyle' does not exist on type 'LegendItem'. Did you mean 'pointStyle'?
    140 |                 label.strokeStyle = label.fillStyle; // 使用填充颜色作为线条颜色
    141 |               } else {
  > 142 |                 label.usePointStyle = false; // 保持其他图标为方块
        |                       ^^^^^^^^^^^^^
    143 |               }
    144 |             });
    145 |             return originalLabels;
kirin-ri commented 3 months ago
const options = {
  responsive: true,
  plugins: {
    legend: {
      position: 'top' as const,
      labels: {
        generateLabels: function(chart: ChartJS) {
          const originalLabels = ChartJS.defaults.plugins.legend.labels.generateLabels(chart);
          originalLabels.forEach(label => {
            if (label.text === '資金繰り') {
              label.pointStyle = 'line';
              label.lineWidth = 2; // 设置为线条的宽度
              label.strokeStyle = label.fillStyle; // 使用填充颜色作为线条颜色
            } else {
              label.pointStyle = 'rect'; // 保持其他图标为方块
            }
          });
          return originalLabels;
        },
        font: {
          size: 16, // 标签字体大小
        },
      }
    },
    tooltip: {
      callbacks: {
        label: function(context: any) {
          const label = context.dataset.label || '';
          const value = context.raw;
          return `${label}: ${value}`;
        }
      }
    },
  },
  scales: {
    y: {
      beginAtZero: true,
      title: {
        display: true,
        text: '百万円', // 纵轴单位
      }
    }
  }
};
kirin-ri commented 3 months ago
const options = {
  responsive: true,
  plugins: {
    legend: {
      display: false, // 我们将手动绘制图例
    },
  },
  scales: {
    y: {
      beginAtZero: true,
      title: {
        display: true,
        text: '百万円', // 纵轴单位
      },
      ticks: {
        callback: function(value) {
          return value / 1000000 + ' 百万円'; // 显示为百万元单位
        }
      }
    }
  }
};

// 自定义图例绘制插件
const customLegendPlugin = {
  id: 'customLegend',
  afterDraw: function(chart) {
    const ctx = chart.ctx;
    const legend = chart.legend;

    legend.legendItems.forEach((legendItem, i) => {
      const x = legend.left + legend.padding;
      const y = legend.top + (i * (legendItem.fontSize + legendItem.boxHeight));

      if (legendItem.text === '資金繰り') {
        ctx.save();
        ctx.strokeStyle = legendItem.strokeStyle;
        ctx.lineWidth = 2; // 设置线条的宽度
        ctx.beginPath();
        ctx.moveTo(x, y + legendItem.fontSize / 2);
        ctx.lineTo(x + legendItem.boxWidth, y + legendItem.fontSize / 2);
        ctx.stroke();
        ctx.restore();
      } else {
        ctx.save();
        ctx.fillStyle = legendItem.fillStyle;
        ctx.fillRect(x, y, legendItem.boxWidth, legendItem.boxHeight);
        ctx.restore();
      }

      ctx.textBaseline = 'middle';
      ctx.fillStyle = legendItem.fontColor;
      ctx.fillText(legendItem.text, x + legendItem.boxWidth + 10, y + legendItem.boxHeight / 2);
    });
  }
};

// 注册自定义插件
ChartJS.register(customLegendPlugin);

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  return (
    <div className="content-wrapper metrics-details">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>{pageName}</h1>
        </div>
      </section>
      <div className="main-content">
        <div className="left-container">
          <div className="graph-container">
            <Chart ref={chartRef} type="bar" data={data} options={options} />
          </div>
        </div>
        <div className="right-container">
          <div className="actionbox-title">
            <div>推奨アクション</div>
          </div>
          <div className='actionbox-message'>
            {/* 你的右侧内容 */}
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:152:23
TS7006: Parameter 'chart' implicitly has an 'any' type.
    150 | const customLegendPlugin = {
    151 |   id: 'customLegend',
  > 152 |   afterDraw: function(chart) {
        |                       ^^^^^
    153 |     const ctx = chart.ctx;
    154 |     const legend = chart.legend;
    155 |

ERROR in src/components/pages/financingPage.tsx:156:33
TS7006: Parameter 'legendItem' implicitly has an 'any' type.
    154 |     const legend = chart.legend;
    155 |
  > 156 |     legend.legendItems.forEach((legendItem, i) => {
        |                                 ^^^^^^^^^^
    157 |       const x = legend.left + legend.padding;
    158 |       const y = legend.top + (i * (legendItem.fontSize + legendItem.boxHeight));
    159 |

ERROR in src/components/pages/financingPage.tsx:156:45
TS7006: Parameter 'i' implicitly has an 'any' type.
    154 |     const legend = chart.legend;
    155 |
  > 156 |     legend.legendItems.forEach((legendItem, i) => {
        |                                             ^
    157 |       const x = legend.left + legend.padding;
    158 |       const y = legend.top + (i * (legendItem.fontSize + legendItem.boxHeight));
    159 |
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip, LegendItem, ChartTypeRegistry } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{detail.amount}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        display: false, // 我们将手动绘制图例
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

  // 自定义图例绘制插件
  const customLegendPlugin = {
    id: 'customLegend',
    afterDraw: function(chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
      const ctx = chart.ctx;
      const legend = chart.legend;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        const x = legend.left + legend.padding;
        const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));

        if (legendItem.text === '資金繰り') {
          ctx.save();
          ctx.strokeStyle = legendItem.strokeStyle as string;
          ctx.lineWidth = 2; // 设置线条的宽度
          ctx.beginPath();
          ctx.moveTo(x, y + (legendItem.fontSize as number) / 2);
          ctx.lineTo(x + legendItem.boxWidth, y + (legendItem.fontSize as number) / 2);
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(x, y, legendItem.boxWidth, legendItem.boxHeight);
          ctx.restore();
        }

        ctx.textBaseline = 'middle';
        ctx.fillStyle = legendItem.fontColor as string;
        ctx.fillText(legendItem.text, x + legendItem.boxWidth + 10, y + legendItem.boxHeight / 2);
      });
    }
  };

  // 注册自定义插件
  ChartJS.register(customLegendPlugin);
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:123:7
TS2532: Object is possibly 'undefined'.
    121 |       const legend = chart.legend;
    122 |
  > 123 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        |       ^^^^^^
    124 |         const x = legend.left + legend.padding;
    125 |         const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));
    126 |

ERROR in src/components/pages/financingPage.tsx:123:7
TS2532: Object is possibly 'undefined'.
    121 |       const legend = chart.legend;
    122 |
  > 123 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        |       ^^^^^^^^^^^^^^^^^^
    124 |         const x = legend.left + legend.padding;
    125 |         const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));
    126 |

ERROR in src/components/pages/financingPage.tsx:124:19
TS2532: Object is possibly 'undefined'.
    122 |
    123 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
  > 124 |         const x = legend.left + legend.padding;
        |                   ^^^^^^
    125 |         const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));
    126 |
    127 |         if (legendItem.text === '資金繰り') {

ERROR in src/components/pages/financingPage.tsx:124:33
TS2532: Object is possibly 'undefined'.
    122 |
    123 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
  > 124 |         const x = legend.left + legend.padding;
        |                                 ^^^^^^
    125 |         const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));
    126 |
    127 |         if (legendItem.text === '資金繰り') {

ERROR in src/components/pages/financingPage.tsx:124:40
TS2339: Property 'padding' does not exist on type 'LegendElement<keyof ChartTypeRegistry>'.
    122 |
    123 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
  > 124 |         const x = legend.left + legend.padding;
        |                                        ^^^^^^^
    125 |         const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));
    126 |
    127 |         if (legendItem.text === '資金繰り') {

ERROR in src/components/pages/financingPage.tsx:125:19
TS2532: Object is possibly 'undefined'.
    123 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
    124 |         const x = legend.left + legend.padding;
  > 125 |         const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));
        |                   ^^^^^^
    126 |
    127 |         if (legendItem.text === '資金繰り') {
    128 |           ctx.save();

ERROR in src/components/pages/financingPage.tsx:125:49
TS2339: Property 'fontSize' does not exist on type 'LegendItem'.
    123 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
    124 |         const x = legend.left + legend.padding;
  > 125 |         const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));
        |                                                 ^^^^^^^^
    126 |
    127 |         if (legendItem.text === '資金繰り') {
    128 |           ctx.save();

ERROR in src/components/pages/financingPage.tsx:125:81
TS2339: Property 'boxHeight' does not exist on type 'LegendItem'.
    123 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
    124 |         const x = legend.left + legend.padding;
  > 125 |         const y = legend.top + (i * (legendItem.fontSize as number + legendItem.boxHeight));
        |                                                                                 ^^^^^^^^^
    126 |
    127 |         if (legendItem.text === '資金繰り') {
    128 |           ctx.save();

ERROR in src/components/pages/financingPage.tsx:132:41
TS2339: Property 'fontSize' does not exist on type 'LegendItem'.
    130 |           ctx.lineWidth = 2; // 设置线条的宽度
    131 |           ctx.beginPath();
  > 132 |           ctx.moveTo(x, y + (legendItem.fontSize as number) / 2);
        |                                         ^^^^^^^^
    133 |           ctx.lineTo(x + legendItem.boxWidth, y + (legendItem.fontSize as number) / 2);
    134 |           ctx.stroke();
    135 |           ctx.restore();

ERROR in src/components/pages/financingPage.tsx:133:37
TS2339: Property 'boxWidth' does not exist on type 'LegendItem'.
    131 |           ctx.beginPath();
    132 |           ctx.moveTo(x, y + (legendItem.fontSize as number) / 2);
  > 133 |           ctx.lineTo(x + legendItem.boxWidth, y + (legendItem.fontSize as number) / 2);
        |                                     ^^^^^^^^
    134 |           ctx.stroke();
    135 |           ctx.restore();
    136 |         } else {

ERROR in src/components/pages/financingPage.tsx:133:63
TS2339: Property 'fontSize' does not exist on type 'LegendItem'.
    131 |           ctx.beginPath();
    132 |           ctx.moveTo(x, y + (legendItem.fontSize as number) / 2);
  > 133 |           ctx.lineTo(x + legendItem.boxWidth, y + (legendItem.fontSize as number) / 2);
        |                                                               ^^^^^^^^
    134 |           ctx.stroke();
    135 |           ctx.restore();
    136 |         } else {

ERROR in src/components/pages/financingPage.tsx:139:41
TS2339: Property 'boxWidth' does not exist on type 'LegendItem'.
    137 |           ctx.save();
    138 |           ctx.fillStyle = legendItem.fillStyle as string;
  > 139 |           ctx.fillRect(x, y, legendItem.boxWidth, legendItem.boxHeight);
        |                                         ^^^^^^^^
    140 |           ctx.restore();
    141 |         }
    142 |

ERROR in src/components/pages/financingPage.tsx:139:62
TS2339: Property 'boxHeight' does not exist on type 'LegendItem'.
    137 |           ctx.save();
    138 |           ctx.fillStyle = legendItem.fillStyle as string;
  > 139 |           ctx.fillRect(x, y, legendItem.boxWidth, legendItem.boxHeight);
        |                                                              ^^^^^^^^^
    140 |           ctx.restore();
    141 |         }
    142 |

ERROR in src/components/pages/financingPage.tsx:145:54
TS2339: Property 'boxWidth' does not exist on type 'LegendItem'.
    143 |         ctx.textBaseline = 'middle';
    144 |         ctx.fillStyle = legendItem.fontColor as string;
  > 145 |         ctx.fillText(legendItem.text, x + legendItem.boxWidth + 10, y + legendItem.boxHeight / 2);
        |                                                      ^^^^^^^^
    146 |       });
    147 |     }
    148 |   };

ERROR in src/components/pages/financingPage.tsx:145:84
TS2339: Property 'boxHeight' does not exist on type 'LegendItem'.
    143 |         ctx.textBaseline = 'middle';
    144 |         ctx.fillStyle = legendItem.fontColor as string;
  > 145 |         ctx.fillText(legendItem.text, x + legendItem.boxWidth + 10, y + legendItem.boxHeight / 2);
        |                                                                                    ^^^^^^^^^
    146 |       });
    147 |     }
    148 |   };

ERROR in src/components/pages/financingPage.tsx:154:5
TS2304: Cannot find name 'setActiveComparison'.
    152 |
    153 |   const handleComparisonClick = (type: string) => {
  > 154 |     setActiveComparison(type);
        |     ^^^^^^^^^^^^^^^^^^^
    155 |   };
    156 |
    157 |   return (

ERROR in src/components/pages/financingPage.tsx:166:12
TS2304: Cannot find name 'AlertBox'.
    164 |       {showAlert && (
    165 |         <div className="alert-container">
  > 166 |           <AlertBox
        |            ^^^^^^^^
    167 |             message="期中に資金がマイナスとなる期間があります"
    168 |             onClose={() => setShowAlert(false)}
    169 |           />

ERROR in src/components/pages/financingPage.tsx:200:46
TS2304: Cannot find name 'activeComparison'.
    198 |               <h2>データ比較</h2>
    199 |               <button
  > 200 |                 className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
        |                                              ^^^^^^^^^^^^^^^^
    201 |                 onClick={() => handleComparisonClick('時系列比較')}
    202 |               >
    203 |                 時系列比較
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip, LegendItem, ChartTypeRegistry } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{detail.amount}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const EmptyPage = () => {
  const pageName = '資金繰り表';
  const [showAlert, setShowAlert] = useState(true);
  const [activeComparison, setActiveComparison] = useState('時系列比較');
  const chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        display: false, // 我们将手动绘制图例
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

  // 自定义图例绘制插件
  const customLegendPlugin = {
    id: 'customLegend',
    afterDraw: function(chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
      const legend = chart?.legend;
      if (!legend) return;

      const ctx = chart.ctx;
      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        const x = legend.left + (legend.padding || 10);
        const y = legend.top + (i * (16 + 10)); // 假设 fontSize 为 16, boxHeight 为 10

        if (legendItem.text === '資金繰り') {
          ctx.save();
          ctx.strokeStyle = legendItem.strokeStyle as string;
          ctx.lineWidth = 2; // 设置线条的宽度
          ctx.beginPath();
          ctx.moveTo(x, y + 8); // 假设 fontSize 为 16, 所以除以2
          ctx.lineTo(x + 40, y + 8); // 假设 boxWidth 为 40
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(x, y, 40, 10); // 假设 boxWidth 为 40, boxHeight 为 10
          ctx.restore();
        }

        ctx.textBaseline = 'middle';
        ctx.fillStyle = legendItem.fontColor as string;
        ctx.fillText(legendItem.text, x + 50, y + 5); // 假设 boxWidth 为 40, boxHeight 为 10
      });
    }
  };

  // 注册自定义插件
  ChartJS.register(customLegendPlugin);
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:125:7
TS2532: Object is possibly 'undefined'.
    123 |
    124 |       const ctx = chart.ctx;
  > 125 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        |       ^^^^^^^^^^^^^^^^^^
    126 |         const x = legend.left + (legend.padding || 10);
    127 |         const y = legend.top + (i * (16 + 10)); // 假设 fontSize 为 16, boxHeight 为 10
    128 |

ERROR in src/components/pages/financingPage.tsx:126:41
TS2339: Property 'padding' does not exist on type 'LegendElement<keyof ChartTypeRegistry>'.
    124 |       const ctx = chart.ctx;
    125 |       legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
  > 126 |         const x = legend.left + (legend.padding || 10);
        |                                         ^^^^^^^
    127 |         const y = legend.top + (i * (16 + 10)); // 假设 fontSize 为 16, boxHeight 为 10
    128 |
    129 |         if (legendItem.text === '資金繰り') {

ERROR in src/components/pages/financingPage.tsx:164:12
TS2304: Cannot find name 'AlertBox'.
    162 |       {showAlert && (
    163 |         <div className="alert-container">
  > 164 |           <AlertBox
        |            ^^^^^^^^
    165 |             message="期中に資金がマイナスとなる期間があります"
    166 |             onClose={() => setShowAlert(false)}
    167 |           />

ERROR in src/components/pages/financingPage.tsx:199:32
TS2304: Cannot find name 'handleComparisonClick'.
    197 |               <button
    198 |                 className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
  > 199 |                 onClick={() => handleComparisonClick('時系列比較')}
        |                                ^^^^^^^^^^^^^^^^^^^^^
    200 |               >
    201 |                 時系列比較
    202 |               </button>
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip, LegendItem, ChartTypeRegistry } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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 chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        display: false, // 我们将手动绘制图例
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

  // 自定义图例绘制插件
  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;
      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        const x = (legend.left || 0) + 10; // 默认 padding 为 10
        const y = (legend.top || 0) + (i * (16 + 10)); // 假设 fontSize 为 16, boxHeight 为 10

        if (legendItem.text === '資金繰り') {
          ctx.save();
          ctx.strokeStyle = legendItem.strokeStyle as string;
          ctx.lineWidth = 2; // 设置线条的宽度
          ctx.beginPath();
          ctx.moveTo(x, y + 8); // 假设 fontSize 为 16, 所以除以2
          ctx.lineTo(x + 40, y + 8); // 假设 boxWidth 为 40
          ctx.stroke();
          ctx.restore();
        } else {
          ctx.save();
          ctx.fillStyle = legendItem.fillStyle as string;
          ctx.fillRect(x, y, 40, 10); // 假设 boxWidth 为 40, boxHeight 为 10
          ctx.restore();
        }

        ctx.textBaseline = 'middle';
        ctx.fillStyle = legendItem.fontColor as string;
        ctx.fillText(legendItem.text, x + 50, y + 5); // 假设 boxWidth 为 40, boxHeight 为 10
      });
    }
  };

  // 注册自定义插件
  ChartJS.register(customLegendPlugin);

  // 处理按钮点击的函数
  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip, LegendItem, ChartTypeRegistry } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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 chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    plugins: {
      legend: {
        display: false, // 我们将手动绘制图例
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

  // 自定义图例绘制插件
  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;

      legend.legendItems.forEach((legendItem: LegendItem, i: number) => {
        const y = legend.top || 20; // 图例的Y坐标

        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 = legendItem.fontColor as string;
        ctx.fillText(legendItem.text, currentX + 50, y); // 图标和文本之间的间距为 50

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

  // 注册自定义插件
  ChartJS.register(customLegendPlugin);

  // 处理按钮点击的函数
  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip, LegendItem, ChartTypeRegistry } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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 chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 40, // 增加顶部的填充,以确保图例与图表内容之间有足够的间距
      },
    },
    plugins: {
      legend: {
        display: false, // 我们将手动绘制图例
      },
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        },
        ticks: {
          callback: function(value) {
            return value / 1000000 + ' 百万円'; // 显示为百万元单位
          }
        }
      }
    }
  };

  // 自定义图例绘制插件
  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 yOffset = 20; // 在图例下方添加额外的距离,以确保与图表内容不重叠
      const y = legend.top || 20 + yOffset; // 图例的Y坐标,加上偏移量

      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 = legendItem.fontColor as string;
        ctx.fillText(legendItem.text, currentX + 50, y); // 图标和文本之间的间距为 50

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

  // 注册自定义插件
  ChartJS.register(customLegendPlugin);

  // 处理按钮点击的函数
  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };
kirin-ri commented 3 months ago
// 清除可能与图例重叠的区域
    ctx.clearRect(0, y - 20, chart.width, yOffset);
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip } from 'chart.js';
import { useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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 chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: 'rgba(153, 102, 255, 0.5)',
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: 'rgba(54, 162, 235, 0.5)',
      },
      {
        type: 'line' as const,
        label: '資金繰り',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 10,
      },
    },
    plugins: {
      legend: {
        display: true,
        labels:{
          color : 'white',
          boxWidth : 0,
          boxHeight : 0,
      }
      }
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        }
      }
    }
  };

  // 自定义图例绘制插件
  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 = 20; // 在图例下方添加额外的距离,以确保与图表内容不重叠

      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; // 移动到下一个图例项
      });
    }
  };

  // 注册自定义插件
  ChartJS.register(customLegendPlugin);

  // 处理按钮点击的函数
  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ絞り込み</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
const data = {
  labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
  datasets: [
    {
      type: 'bar' as const,
      label: '収入',
      data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.3)';
      },
    },
    {
      type: 'bar' as const,
      label: '支出',
      data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.3)';
      },
    },
    {
      type: 'line' as const,
      label: '資金繰り',
      data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
      borderColor: 'black',
      backgroundColor: 'black',
      fill: false,
      tension: 0.1,
      borderWidth: 2,
      borderDash: function(context: ScriptableContext<'line'>) {
        const index = context.dataIndex;
        return index < 3 ? [] : [5, 5]; // 4月到6月为实线,7月到3月为点线
      },
      pointStyle: 'rectRot',
      pointRadius: 6,
      pointHoverRadius: 8,
      pointBackgroundColor: (context: ScriptableContext<'line'>) => {
        const index = context.dataIndex;
        const value = context.dataset.data[index] as number;
        return value < 0 ? 'red' : 'black';
      }
    }
  ],
};
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip } from 'chart.js';
import { useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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 chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.3)';
        },
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.3)';
        },
      },
      {
        type: 'line' as const,
        label: '残高',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        borderDash: function(context: ScriptableContext<'line'>) {
          const index = context.dataIndex;
          return index < 3 ? [] : [5, 5]; // 4月到6月为实线,7月到3月为点线
        },
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 0,
      },
    },
    plugins: {
      legend: {
        display: true,
        labels:{
          color : 'white',
          boxWidth : 0,
          boxHeight : 0,
      }
      }
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        }
      }
    }
  };

  // 自定义图例绘制插件
  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; // 移动到下一个图例项
      });
    }
  };

  // 注册自定义插件
  ChartJS.register(customLegendPlugin);

  // 处理按钮点击的函数
  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ絞り込み</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
const data = {
  labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
  datasets: [
    {
      type: 'bar' as const,
      label: '収入',
      data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.3)';
      },
    },
    {
      type: 'bar' as const,
      label: '支出',
      data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.3)';
      },
    },
    {
      type: 'line' as const,
      label: '残高 (4月-6月)',
      data: [10, -5, -10, null, null, null, null, null, null, null, null, null], // 只包含4月到6月的数据
      borderColor: 'black',
      backgroundColor: 'black',
      fill: false,
      tension: 0.1,
      borderWidth: 2,
      pointStyle: 'rectRot',
      pointRadius: 6,
      pointHoverRadius: 8,
      pointBackgroundColor: (context: ScriptableContext<'line'>) => {
        const index = context.dataIndex;
        const value = context.dataset.data[index] as number;
        return value < 0 ? 'red' : 'black';
      }
    },
    {
      type: 'line' as const,
      label: '残高 (7月-3月)',
      data: [null, null, null, 5, 10, -15, 20, -10, 15, 5, -5, 10], // 只包含7月到3月的数据
      borderColor: 'black',
      backgroundColor: 'black',
      fill: false,
      tension: 0.1,
      borderWidth: 2,
      borderDash: [5, 5], // 虚线
      pointStyle: 'rectRot',
      pointRadius: 6,
      pointHoverRadius: 8,
      pointBackgroundColor: (context: ScriptableContext<'line'>) => {
        const index = context.dataIndex;
        const value = context.dataset.data[index] as number;
        return value < 0 ? 'red' : 'black';
      }
    }
  ],
};
kirin-ri commented 3 months ago
const data = {
  labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
  datasets: [
    {
      type: 'bar' as const,
      label: '収入',
      data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.3)';
      },
    },
    {
      type: 'bar' as const,
      label: '支出',
      data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.3)';
      },
    },
    {
      type: 'line' as const,
      label: '残高',
      data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
      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: (context: ScriptableContext<'line'>) => {
        const index = context.dataIndex;
        const value = context.dataset.data[index] as number;
        return value < 0 ? 'red' : 'black';
      }
    }
  ],
};

const options = {
  responsive: true,
  layout: {
    padding: {
      top: 0,
    },
  },
  plugins: {
    legend: {
      display: true,
      labels:{
        color : 'white',
        boxWidth : 0,
        boxHeight : 0,
      }
    }
  },
  scales: {
    y: {
      beginAtZero: true,
      title: {
        display: true,
        text: '百万円', // 纵轴单位
      }
    }
  }
};

export default EmptyPage;
kirin-ri commented 3 months ago

ERROR in src/components/pages/financingPage.tsx:90:24 TS7006: Parameter 'ctx' implicitly has an 'any' type. 88 | pointHoverRadius: 8, 89 | segment: {

90 | borderDash: (ctx) => { | ^^^ 91 | return ctx.p0DataIndex < 3 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线 92 | }, 93 | },

kirin-ri commented 3 months ago
const data = {
  labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
  datasets: [
    {
      type: 'bar' as const,
      label: '収入',
      data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.3)';
      },
    },
    {
      type: 'bar' as const,
      label: '支出',
      data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.3)';
      },
    },
    {
      type: 'line' as const,
      label: '残高',
      data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
      borderColor: 'black',
      backgroundColor: 'black',
      fill: false,
      tension: 0.1,
      borderWidth: 2,
      pointStyle: 'rectRot',
      pointRadius: 6,
      pointHoverRadius: 8,
      segment: {
        borderDash: (ctx: SegmentContext<'line'>) => {
          return ctx.p0DataIndex < 3 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
        },
      },
      pointBackgroundColor: (context: ScriptableContext<'line'>) => {
        const index = context.dataIndex;
        const value = context.dataset.data[index] as number;
        return value < 0 ? 'red' : 'black';
      }
    }
  ],
};
kirin-ri commented 3 months ago
const data = {
  labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
  datasets: [
    {
      type: 'bar' as const,
      label: '収入',
      data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.3)';
      },
    },
    {
      type: 'bar' as const,
      label: '支出',
      data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
      backgroundColor: function(context: ScriptableContext<'bar'>) {
        const index = context.dataIndex;
        return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.3)';
      },
    },
    {
      type: 'line' as const,
      label: '残高',
      data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
      borderColor: 'black',
      backgroundColor: 'black',
      fill: false,
      tension: 0.1,
      borderWidth: 2,
      pointStyle: 'rectRot',
      pointRadius: 6,
      pointHoverRadius: 8,
      segment: {
        borderDash: (ctx: { p0DataIndex: number }) => { // 手动定义类型
          return ctx.p0DataIndex < 3 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
        },
      },
      pointBackgroundColor: (context: ScriptableContext<'line'>) => {
        const index = context.dataIndex;
        const value = context.dataset.data[index] as number;
        return value < 0 ? 'red' : 'black';
      }
    }
  ],
};
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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 chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
        },
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
        },
      },
      {
        type: 'line' as const,
        label: '残高',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        segment: {
          borderDash: (ctx: { p0DataIndex: number }) => { // 手动定义类型
            return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
          },
        },
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 0,
      },
    },
    plugins: {
      legend: {
        display: true,
        labels:{
          color : 'white',
          boxWidth : 0,
          boxHeight : 0,
      }
      }
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        }
      }
    }
  };

  // 自定义图例绘制插件
  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; // 移动到下一个图例项
      });
    }
  };

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

  // 处理按钮点击的函数
  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ絞り込み</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

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

ChartJS.register(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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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 chartRef = useRef<ChartJS | null>(null);

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
        },
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
        },
      },
      {
        type: 'line' as const,
        label: '残高',
        data: [10, -5, -10, 5, 10, -15, 20, -10, 15, 5, -5, 10],
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        segment: {
          borderDash: (ctx: { p0DataIndex: number }) => { // 手动定义类型
            return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
          },
        },
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 0,
      },
    },
    plugins: {
      legend: {
        display: true,
        labels:{
          color : 'white',
          boxWidth : 0,
          boxHeight : 0,
      }
      }
    },
    scales: {
      y: {
        beginAtZero: true,
        title: {
          display: true,
          text: '百万円', // 纵轴单位
        }
      }
    }
  };

  // 自定义图例绘制插件
  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; // 移动到下一个图例项
      });
    }
  };

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

  // 处理按钮点击的函数
  const handleComparisonClick = (type: string) => {
    setActiveComparison(type);
  };

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select">
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <select className="filter-select">
                  <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'>
            <h6>問題点:</h6> 5月、6月、9月、11月、3月の期間において、資金が顕著なマイナスを示しており、これらの月には支出が収入を大幅に上回っています。この状況は会社のキャッシュフローに深刻な影響を与える可能性があります。<br></br><br></br>
            <h6>対策:</h6> 支出管理の強化: これらの月には非必須の支出を延期し、必須支出のみを優先します。また、各部署に支出削減を指示し、コスト削減のための具体的なアクションプランを策定します。<br></br><br></br>
            ①短期借入の検討: 一時的な資金不足を補うために、短期借入を検討します。これにより、資金繰りの安定性を確保します。<br></br>
            ②収入の増加: 収入の予測と計画を強化し、売上を増加させるためのプロモーション活動や新規顧客の開拓を行います。また、既存顧客からの追加受注を促進するためのインセンティブプランを導入します。<br></br>
            ③資金フローの予測: 定期的に資金フローを予測し、問題が発生する前に早期に対策を講じることができるようにします。これにより、予期せぬ資金不足を防ぎます。</div>
          <div className="collapsible-panels">
            <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
const dataSets = {
  '楽観-楽観': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '公司收入增长显著,支出控制良好,整体财务状况健康。建议继续保持当前的支出管理策略,同时考虑增加对创新项目的投资,以进一步推动收入增长。',
  },
  '楽観-中立': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '收入表现良好,但支出管理略有松懈,部分月度支出略高于预期。建议在支出管理上加强监督,优化成本控制,确保支出不超过预算。',
  },
  '楽観-悲観': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '尽管收入表现强劲,但支出明显超出预期,导致资金压力增大。建议紧急削减非必要支出,并寻找短期融资选项以缓解现金流压力。同时,审视各部门的预算执行情况,避免未来类似问题。',
  },
  '中立-楽観': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '收入保持稳定,支出管理良好,财务状况较为平稳。建议保持当前策略,逐步寻求收入增长的机会,同时确保支出效率。',
  },
  '中立-中立': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '公司收入和支出均保持在中等水平,财务状况未见明显波动。建议进行市场调研,寻找新的收入增长点,同时保持成本控制,确保财务健康。',
  },
  '中立-悲観': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '收入表现一般,但支出较高,导致财务状况紧张。建议立即审查并调整支出预算,寻找降低成本的具体措施,同时探索增加收入的可能性。',
  },
  '悲観-楽観': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '收入较低,但支出控制得当,现金流状况稳定。建议在保持支出控制的基础上,集中精力增加销售和收入,探索新市场和客户群体。',
  },
  '悲観-中立': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '收入较低,支出中等,财务状况稍显紧张。建议优先考虑收入提升策略,例如推出新的营销活动或产品优惠,同时加强支出控制。',
  },
  '悲観-悲観': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '收入低迷,支出较高,导致严重的现金流压力。建议立即启动紧急成本削减计划,并考虑短期融资以维持运营。同时,加快探索新的收入来源,并重新评估当前的支出结构。',
  },
};
kirin-ri commented 3 months ago
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}`;
    const selectedData = dataSets[key];

    if (chartRef.current) {
      chartRef.current.data.datasets[0].data = selectedData.income;
      chartRef.current.data.datasets[1].data = selectedData.expense;
      chartRef.current.data.datasets[2].data = selectedData.income.map((income, i) => income + selectedData.expense[i]);
      chartRef.current.update();
    }

    setActionMessage(selectedData.action);
  };

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

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: dataSets['楽観-楽観'].income,
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
        },
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: dataSets['楽観-楽観'].expense,
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
        },
      },
      {
        type: 'line' as const,
        label: '残高',
        data: dataSets['楽観-楽観'].income.map((income, i) => income + dataSets['楽観-楽観'].expense[i]),
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        segment: {
          borderDash: (ctx: { p0DataIndex: number }) => {
            return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
          },
        },
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

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

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

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select" onChange={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <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="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:61:26
TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ '\u697D\u89B3-\u697D\u89B3': { income: number[]; expense: number[]; action: string; }; '\u697D\u89B3-\u4E2D\u7ACB': { income: number[]; expense: number[]; action: string; }; '\u697D\u89B3-\u60B2\u89B3': { income: number[]; expense: number[]; action: string; }; '\u4E2D\u7ACB-\u697D\u89B3': { income: number[]; expen...'.
  No index signature with a parameter of type 'string' was found on type '{ '\u697D\u89B3-\u697D\u89B3': { income: number[]; expense: number[]; action: string; }; '\u697D\u89B3-\u4E2D\u7ACB': { income: number[]; expense: number[]; action: string; }; '\u697D\u89B3-\u60B2\u89B3': { income: number[]; expense: number[]; action: string; }; '\u4E2D\u7ACB-\u697D\u89B3': { income: number[]; expen...'.
    59 |   const updateChartAndAction = () => {
    60 |     const key = `${incomeLevel}-${expenseLevel}`;
  > 61 |     const selectedData = dataSets[key];
       |                          ^^^^^^^^^^^^^
    62 |
    63 |     if (chartRef.current) {
    64 |       chartRef.current.data.datasets[0].data = selectedData.income;

ERROR in src/components/pages/financingPage.tsx:66:73
TS7006: Parameter 'income' implicitly has an 'any' type.
    64 |       chartRef.current.data.datasets[0].data = selectedData.income;
    65 |       chartRef.current.data.datasets[1].data = selectedData.expense;
  > 66 |       chartRef.current.data.datasets[2].data = selectedData.income.map((income, i) => income + selectedData.expense[i]);
       |                                                                         ^^^^^^
    67 |       chartRef.current.update();
    68 |     }
    69 |

ERROR in src/components/pages/financingPage.tsx:66:81
TS7006: Parameter 'i' implicitly has an 'any' type.
    64 |       chartRef.current.data.datasets[0].data = selectedData.income;
    65 |       chartRef.current.data.datasets[1].data = selectedData.expense;
  > 66 |       chartRef.current.data.datasets[2].data = selectedData.income.map((income, i) => income + selectedData.expense[i]);
       |                                                                                 ^
    67 |       chartRef.current.update();
    68 |     }
    69 |

ERROR in src/components/pages/financingPage.tsx:150:67
TS2552: Cannot find name 'options'. Did you mean 'Option'?
    148 |         <div className="left-container">
    149 |           <div className="graph-container">
  > 150 |             <Chart ref={chartRef} type="bar" data={data} options={options} />
        |                                                                   ^^^^^^^
    151 |           </div>
    152 |           <div className="additional-section">
    153 |             <div className="data-filter">

ERROR in src/components/pages/financingPage.tsx:176:32
TS2304: Cannot find name 'handleComparisonClick'.
    174 |               <button
    175 |                 className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
  > 176 |                 onClick={() => handleComparisonClick('時系列比較')}
        |                                ^^^^^^^^^^^^^^^^^^^^^
    177 |               >
    178 |                 時系列比較
    179 |               </button>

ERROR in src/components/pages/financingPage.tsx:191:76
TS2304: Cannot find name 'details'.
    189 |           </div>
    190 |           <div className="collapsible-panels">
  > 191 |             <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
        |                                                                            ^^^^^^^
    192 |             <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
    193 |             <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
    194 |           </div>

ERROR in src/components/pages/financingPage.tsx:192:76
TS2304: Cannot find name 'details'.
    190 |           <div className="collapsible-panels">
    191 |             <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
  > 192 |             <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
        |                                                                            ^^^^^^^
    193 |             <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
    194 |           </div>
    195 |         </div>

ERROR in src/components/pages/financingPage.tsx:193:76
TS2304: Cannot find name 'details'.
    191 |             <CollapsiblePanel title="営業キャッシュフロー" money="343,564円" details={details} />
    192 |             <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
  > 193 |             <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
        |                                                                            ^^^^^^^
    194 |           </div>
    195 |         </div>
    196 |       </div>
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

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

// 定义数据集
const dataSets = {
  '楽観-楽観': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '公司收入增长显著,支出控制良好,整体财务状况健康。建议继续保持当前的支出管理策略,同时考虑增加对创新项目的投资,以进一步推动收入增长。',
  },
  '楽観-中立': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '收入表现良好,但支出管理略有松懈,部分月度支出略高于预期。建议在支出管理上加强监督,优化成本控制,确保支出不超过预算。',
  },
  '楽観-悲観': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '尽管收入表现强劲,但支出明显超出预期,导致资金压力增大。建议紧急削减非必要支出,并寻找短期融资选项以缓解现金流压力。同时,审视各部门的预算执行情况,避免未来类似问题。',
  },
  '中立-楽観': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '收入保持稳定,支出管理良好,财务状况较为平稳。建议保持当前策略,逐步寻求收入增长的机会,同时确保支出效率。',
  },
  '中立-中立': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '公司收入和支出均保持在中等水平,财务状况未见明显波动。建议进行市场调研,寻找新的收入增长点,同时保持成本控制,确保财务健康。',
  },
  '中立-悲観': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '收入表现一般,但支出较高,导致财务状况紧张。建议立即审查并调整支出预算,寻找降低成本的具体措施,同时探索增加收入的可能性。',
  },
  '悲観-楽観': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '收入较低,但支出控制得当,现金流状况稳定。建议在保持支出控制的基础上,集中精力增加销售和收入,探索新市场和客户群体。',
  },
  '悲観-中立': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '收入较低,支出中等,财务状况稍显紧张。建议优先考虑收入提升策略,例如推出新的营销活动或产品优惠,同时加强支出控制。',
  },
  '悲観-悲観': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '收入低迷,支出较高,导致严重的现金流压力。建议立即启动紧急成本削减计划,并考虑短期融资以维持运营。同时,加快探索新的收入来源,并重新评估当前的支出结构。',
  },
};

// 定义其他必要的组件和函数
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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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.datasets[0].data = selectedData.income;
      chartRef.current.data.datasets[1].data = selectedData.expense;
      chartRef.current.data.datasets[2].data = selectedData.income.map((income: number, i: number) => income + selectedData.expense[i]);
      chartRef.current.update();
    }

    setActionMessage(selectedData.action);
  };

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

  const data = {
    labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
    datasets: [
      {
        type: 'bar' as const,
        label: '収入',
        data: dataSets['楽観-楽観'].income,
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
        },
      },
      {
        type: 'bar' as const,
        label: '支出',
        data: dataSets['楽観-楽観'].expense,
        backgroundColor: function(context: ScriptableContext<'bar'>) {
          const index = context.dataIndex;
          return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
        },
      },
      {
        type: 'line' as const,
        label: '残高',
        data: dataSets['楽観-楽観'].income.map((income, i) => income + dataSets['楽観-楽観'].expense[i]),
        borderColor: 'black',
        backgroundColor: 'black',
        fill: false,
        tension: 0.1,
        borderWidth: 2,
        pointStyle: 'rectRot',
        pointRadius: 6,
        pointHoverRadius: 8,
        segment: {
          borderDash: (ctx: { p0DataIndex: number }) => {
            return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
          },
        },
        pointBackgroundColor: (context: ScriptableContext<'line'>) => {
          const index = context.dataIndex;
          const value = context.dataset.data[index] as number;
          return value < 0 ? 'red' : 'black';
        }
      }
    ],
  };

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 0,
      },
    },
    plugins: {
      legend: {
        display: true,
        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);
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  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; // 移动到下一个图例项
      });
    },
  };

  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={data} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select" onChange={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <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="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 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' as const,
                    label: '収入',
                    data: selectedData.income,
                    backgroundColor: function(context: ScriptableContext<'bar'>) {
                        const index = context.dataIndex;
                        return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
                    },
                },
                {
                    type: 'bar' as const,
                    label: '支出',
                    data: selectedData.expense,
                    backgroundColor: function(context: ScriptableContext<'bar'>) {
                        const index = context.dataIndex;
                        return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
                    },
                },
                {
                    type: 'line' as const,
                    label: '残高',
                    data: selectedData.income.map((income, i) => income + selectedData.expense[i]),
                    borderColor: 'black',
                    backgroundColor: 'black',
                    fill: false,
                    tension: 0.1,
                    borderWidth: 2,
                    pointStyle: 'rectRot',
                    pointRadius: 6,
                    pointHoverRadius: 8,
                    segment: {
                        borderDash: (ctx: { p0DataIndex: number }) => {
                            return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
                        },
                    },
                    pointBackgroundColor: (context: ScriptableContext<'line'>) => {
                        const index = context.dataIndex;
                        const value = context.dataset.data[index] as number;
                        return value < 0 ? 'red' : 'black';
                    }
                }
            ],
        };

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

    setActionMessage(selectedData.action);
};
kirin-ri commented 3 months ago
import { BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LegendItem, LinearScale, LineElement, PointElement, ScriptableContext, Title, Tooltip } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';

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

// 定义数据集
const dataSets = {
  '楽観-楽観': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '公司收入增长显著,支出控制良好,整体财务状况健康。建议继续保持当前的支出管理策略,同时考虑增加对创新项目的投资,以进一步推动收入增长。',
  },
  '楽観-中立': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '收入表现良好,但支出管理略有松懈,部分月度支出略高于预期。建议在支出管理上加强监督,优化成本控制,确保支出不超过预算。',
  },
  '楽観-悲観': {
    income: [25, 35, 30, 20, 35, 30, 25, 35, 30, 35, 25, 30],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '尽管收入表现强劲,但支出明显超出预期,导致资金压力增大。建议紧急削减非必要支出,并寻找短期融资选项以缓解现金流压力。同时,审视各部门的预算执行情况,避免未来类似问题。',
  },
  '中立-楽観': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '收入保持稳定,支出管理良好,财务状况较为平稳。建议保持当前策略,逐步寻求收入增长的机会,同时确保支出效率。',
  },
  '中立-中立': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '公司收入和支出均保持在中等水平,财务状况未见明显波动。建议进行市场调研,寻找新的收入增长点,同时保持成本控制,确保财务健康。',
  },
  '中立-悲観': {
    income: [20, 30, 25, 15, 30, 25, 20, 30, 25, 30, 20, 25],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '收入表现一般,但支出较高,导致财务状况紧张。建议立即审查并调整支出预算,寻找降低成本的具体措施,同时探索增加收入的可能性。',
  },
  '悲観-楽観': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-20, -30, -25, -15, -20, -25, -30, -20, -25, -15, -20, -25],
    action: '收入较低,但支出控制得当,现金流状况稳定。建议在保持支出控制的基础上,集中精力增加销售和收入,探索新市场和客户群体。',
  },
  '悲観-中立': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-25, -35, -30, -20, -25, -30, -35, -25, -30, -20, -25, -30],
    action: '收入较低,支出中等,财务状况稍显紧张。建议优先考虑收入提升策略,例如推出新的营销活动或产品优惠,同时加强支出控制。',
  },
  '悲観-悲観': {
    income: [15, 25, 20, 10, 25, 20, 15, 25, 20, 25, 15, 20],
    expense: [-30, -40, -35, -25, -30, -35, -40, -30, -35, -25, -30, -35],
    action: '收入低迷,支出较高,导致严重的现金流压力。建议立即启动紧急成本削减计划,并考虑短期融资以维持运营。同时,加快探索新的收入来源,并重新评估当前的支出结构。',
  },
};

// 定义其他必要的组件和函数
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: detail.alert ? 'red' : 'black' }}>
                <span>{detail.month}</span>
                <span>{detail.alert && <i style={{ color: 'red', marginRight: '5px' }} />}</span>
                <span>{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' as const,
                    label: '収入',
                    data: selectedData.income,
                    backgroundColor: function(context: ScriptableContext<'bar'>) {
                        const index = context.dataIndex;
                        return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
                    },
                },
                {
                    type: 'bar' as const,
                    label: '支出',
                    data: selectedData.expense,
                    backgroundColor: function(context: ScriptableContext<'bar'>) {
                        const index = context.dataIndex;
                        return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
                    },
                },
                {
                    type: 'line' as const,
                    label: '残高',
                    data: selectedData.income.map((income, i) => income + selectedData.expense[i]),
                    borderColor: 'black',
                    backgroundColor: 'black',
                    fill: false,
                    tension: 0.1,
                    borderWidth: 2,
                    pointStyle: 'rectRot',
                    pointRadius: 6,
                    pointHoverRadius: 8,
                    segment: {
                        borderDash: (ctx: { p0DataIndex: number }) => {
                            return ctx.p0DataIndex < 2 ? [] : [5, 5]; // 4月到6月实线,7月到3月虚线
                        },
                    },
                    pointBackgroundColor: (context: ScriptableContext<'line'>) => {
                        const index = context.dataIndex;
                        const value = context.dataset.data[index] as number;
                        return value < 0 ? 'red' : 'black';
                    }
                }
            ],
        };

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

    setActionMessage(selectedData.action);
  };

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

  const options = {
    responsive: true,
    layout: {
      padding: {
        top: 0,
      },
    },
    plugins: {
      legend: {
        display: true,
        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);
  };

  const details = [
    { month: '4月', amount: 'XXXXX円' },
    { month: '5月', amount: '▲XXXXX円', alert: true },
    { month: '6月', amount: '▲XXXXX円', alert: true },
    { month: '7月', amount: 'XXXXX円' },
    { month: '8月', amount: 'XXXXX円' },
    { month: '9月', amount: '▲XXXXX円', alert: true },
    { month: '10月', amount: 'XXXXX円' },
    { month: '11月', amount: '▲XXXXX円', alert: true },
    { month: '12月', amount: 'XXXXX円' },
    { month: '1月', amount: 'XXXXX円' },
    { month: '2月', amount: 'XXXXX円' },
    { month: '3月', amount: '▲XXXXX円', alert: true }
  ];

  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; // 移动到下一个图例项
      });
    },
  };

  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 || {}} options={options} />
          </div>
          <div className="additional-section">
            <div className="data-filter">
              <h2>データ予測</h2>
              <div className="filter-group">
                <button className="filter-btn">収入</button>
                <select className="filter-select" onChange={handleIncomeChange} value={incomeLevel}>
                  <option>楽観</option>
                  <option>中立</option>
                  <option>悲観</option>
                </select>
              </div>
              <div className="filter-group">
                <button className="filter-btn">支出</button>
                <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="343,564円" details={details} />
            <CollapsiblePanel title="投資キャッシュフロー" money="435,435円" details={details} />
            <CollapsiblePanel title="財務キャッシュフロー" money="567,232円" details={details} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default EmptyPage;
kirin-ri commented 3 months ago
ERROR in src/components/pages/financingPage.tsx:291:46
TS2322: Type 'ChartData<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint | null)[], unknown> | {}' is not assignable to type 'ChartData<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint | null)[], unknown>'.
  Property 'datasets' is missing in type '{}' but required in type 'ChartData<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint | null)[], unknown>'.
    289 |         <div className="left-container">
    290 |           <div className="graph-container">
  > 291 |             <Chart ref={chartRef} type="bar" data={chartRef.current?.data || {}} options={options} />
        |                                              ^^^^
    292 |           </div>
    293 |           <div className="additional-section">
    294 |             <div className="data-filter">
kirin-ri commented 3 months ago
{
  income: [31.2, 32.8, 30.5, 29.9, 33.2, 31.7, 32.1, 34.3, 32.6, 33.5, 30.9, 31.4],
  expense: [-26.7, -28.3, -27.5, -25.8, -27.9, -26.4, -28.1, -27.2, -28.4, -26.8, -27.1, -27.6],
  action: '会社の収益は順調に増加しており、支出も適切に管理されています。今後、現状の財務安定性を維持するために、リスク管理を強化しつつ、新たな投資機会を探ることが重要です。また、資金繰りの予測を定期的に見直し、潜在的な問題を早期に発見して対応することで、長期的な成長を確保できます。特に、成長を加速させるための研究開発や市場開拓に向けた資源配分の最適化が求められます。'
}
kirin-ri commented 3 months ago
{
  income: [31.2, 32.8, 30.5, 34.7, 33.2, 34.1, 35.3, 36.0, 35.6, 36.5, 34.9, 35.4],
  expense: [-26.7, -28.3, -27.5, -29.1, -28.4, -29.0, -29.7, -30.2, -29.8, -30.5, -29.3, -29.7],
  action: '4月から6月までの実績データによると、収益は安定して増加傾向にあり、支出も適切に管理されています。この傾向が続くと予測されるため、7月以降の収益はさらに増加し、支出もコントロールされた状態で推移すると期待されます。このような状況下では、現行の財務管理戦略を維持しつつ、新たな投資機会を積極的に探ることが重要です。特に、新規市場への参入や技術開発に向けた投資が、長期的な成長に寄与すると考えられます。一方で、予期せぬリスクに備えた資金繰りの予測と管理も引き続き強化する必要があります。この予測に基づき、現行の経営方針を再評価し、成長戦略をさらに強化することで、今後の安定した成長が見込まれます。'
}
kirin-ri commented 3 months ago
{
  income: [32.1, 30.9, 31.7, 33.0, 32.3, 33.4, 33.7, 34.5, 34.1, 35.0, 33.6, 34.2],
  expense: [-28.0, -29.5, -28.2, -30.1, -29.7, -30.4, -30.9, -31.3, -30.8, -31.2, -30.7, -31.0],
  action: '実績データから見ると、4月から6月にかけて収益は順調に推移していますが、支出が増加傾向にあるため、今後の財務状況には注意が必要です。7月以降も収益の増加は見込まれるものの、支出が同様に増加すると予測されます。このような状況では、支出管理の強化が重要となります。具体的には、コスト削減のための新たな取り組みや、支出項目の精査が求められます。さらに、収益性の高いプロジェクトにリソースを集中させることで、全体の利益率を向上させる戦略が効果的です。リスク管理を強化し、不測の事態に備えつつ、予測される支出の増加に対応するための具体的な計画を立てることが、今後の安定した成長に繋がるでしょう。'
}