Open kirin-ri opened 2 months ago
import { BarController, BarElement, CategoryScale, Chart as ChartJS, ChartTypeRegistry, Legend, LinearScale, LineController, LineElement, PointElement, Title, Tooltip } from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(BarController, LineController, CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [incomeLevel, setIncomeLevel] = useState('中立');
const [expenseLevel, setExpenseLevel] = useState('中立');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = () => {
const key = `${incomeLevel}-${expenseLevel}` as keyof typeof dataSets;
const selectedData = dataSets[key];
if (!selectedData) {
console.error(`No data found for key: ${incomeLevel}-${expenseLevel}`);
return;
}
const incomeExpenseDiff = selectedData.incomeExpenseDiff || selectedData.income.map((income, i) => income + selectedData.expense[i]);
const balance = selectedData.balance || selectedData.income.reduce((acc: number[], income, i) => {
let newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + income + selectedData.expense[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: selectedData.income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: selectedData.expense,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
if (selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
const actionBox = document.querySelector('.actionbox-message');
if (actionBox) {
actionBox.scrollTop = 0;
}
};
useEffect(() => {
updateChartAndAction();
}, [incomeLevel, expenseLevel]);
const handleIncomeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setIncomeLevel(e.target.value);
};
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setExpenseLevel(e.target.value);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10; // 第一行Y坐标
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">支出</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観:0.8倍">楽観:前年度支出の0.8倍</option>
<option value="中立:相当">中立:前年度支出と相当</option>
<option value="悲観:1.2倍">悲観:前年度支出の1.2倍</option>
</select>
</div>
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}> {/* 全体のインデントをゼロに設定 */}
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance =10.0
const dataSets: { [key: string]: DataSet } = {
"楽観-楽観": {
income: [34.0, 35.5, 34.5, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0],
expense: [-33.0, -34.0, -35.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0],
incomeExpenseDiff: [1.0, 1.5, -0.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0],
balance: [11.0, 12.5, 12.0, 14.0, 16.5, 19.5, 23.0, 27.0, 31.5, 36.5, 42.0, 48.0],
action: {
cashFlow: "昨年の残高推移は比較的安定していました。6月の当月収支がマイナスになっていて、収入が前年同期よりも若干低下しているが、支出の管理がより効率的であったため、全体としての残高推移は安定しています。",
countermeasure: "-"
}
},
"楽観-中立": {
income: [34.0, 35.5, 34.5, 35.5, 36.0, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5, 39.0],
expense: [-33.0, -34.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0, -35.0],
incomeExpenseDiff: [1.0, 1.5, -0.5, 0.5, 1.0, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
balance: [11.0, 12.5, 12.0, 12.5, 13.5, 14.5, 16.0, 18.0, 20.5, 23.5, 27.0, 31.0],
action: {
cashFlow: "昨年は残高推移が年間を通して安定していました。今年の前半では、6月の当月収支がマイナスになっています。収入は前年並みで推移している一方で、支出がやや増加しています。",
countermeasure: "-"
}
},
"楽観-悲観": {
income: [34.0, 35.5, 34.5, 35.0, 35.5, 36.0, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5],
expense: [-33.0, -34.0, -35.0, -36.0, -36.5, -37.0, -37.5, -38.0, -38.5, -39.0, -39.5, -40.0],
incomeExpenseDiff: [1.0, 1.5, -0.5, -1.0, -1.0, -1.0, -1.5, -1.5, -1.5, -1.5, -1.5, -1.5],
balance: [11.0, 12.5, 12.0, 11.0, 10.0, 9.0, 7.5, 6.0, 4.5, 3.0, 1.5, 0.0],
action: {
cashFlow: "昨年は収入が安定していたものの、設備投資や労務費の増加により、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。特に支出が収入を上回るリスクが高まっています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
"中立-楽観": {
income: [34.0, 35.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5],
expense: [-33.0, -34.0, -35.0, -33.0, -32.5, -32.0, -31.5, -31.0, -30.5, -30.0, -29.5, -29.0],
incomeExpenseDiff: [1.0, 1.5, -0.5, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5],
balance: [11.0, 12.5, 12.0, 13.5, 15.5, 18.0, 21.0, 24.5, 28.5, 33.0, 38.0, 43.5],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立-中立": {
income: [34.0, 35.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5],
expense: [-33.0, -34.0, -35.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0, -34.0],
incomeExpenseDiff: [1.0, 1.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5],
balance: [11.0, 12.5, 12.0, 12.5, 13.0, 13.5, 14.0, 14.5, 15.0, 15.5, 16.0, 16.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"中立-悲観": {
income: [34.0, 35.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5, 34.5],
expense: [-33.0, -34.0, -35.0, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0],
incomeExpenseDiff: [1.0, 1.5, -0.5, -1.5, -2.5, -3.5, -4.5, -5.5, -6.5, -7.5, -8.5, -9.5],
balance: [11.0, 12.5, 12.0, 10.5, 8.0, 4.5, 0.0, -5.5, -12.0, -19.5, -28.0, -37.5],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
"悲観-楽観": {
income: [34.0, 35.5, 34.5, 33.5, 33.0, 32.5, 32.0, 31.5, 31.0, 30.5, 30.0, 29.5],
expense: [-33.0, -34.0, -35.0, -33.0, -32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0],
incomeExpenseDiff: [1.0, 1.5, -0.5, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5],
balance: [11.0, 12.5, 12.0, 12.5, 13.5, 15.0, 17.0, 19.5, 22.5, 26.0, 30.0, 34.5],
action: {
cashFlow: "昨年は収入の減少と支出の抑制により、残高推移が後半にかけて改善されました。今年の前半では収入の減少が続き、支出の抑制も限界に達しているため、6月の当月収支がマイナスになっています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n計画を見直し、過剰在庫を削減することで資金を効率的に運用\n短期および中長期の追加融資\n投資家からの資金調達など、手段の多様化"
}
},
"悲観-中立": {
income: [34.0, 35.5, 34.5, 33.0, 32.5, 32.0, 31.5, 31.0, 30.5, 30.0, 29.5, 29.0],
expense: [-33.0, -34.0, -35.0, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5, -34.5],
incomeExpenseDiff: [1.0, 1.5, -0.5, -1.5, -2.0, -2.5, -3.0, -3.5, -4.0, -4.5, -5.0, -5.5],
balance: [11.0, 12.5, 12.0, 10.5, 8.5, 6.0, 3.0, -0.5, -4.5, -9.0, -14.0, -19.5],
action: {
cashFlow: "昨年の残高推移は、支出の抑制により後半にかけて若干の改善が見られましたが、今年の前半では収入の減少が続き、支出の増加が懸念されています。6月の当月収支がマイナスになっていて、残高推移の悪化に注意が必要です。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n現金回収を早めるため、請求の迅速化\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
"悲観-悲観": {
income: [34.0, 35.5, 34.5, 33.0, 32.5, 32.0, 31.5, 31.0, 30.5, 30.0, 29.5, 29.0],
expense: [-33.0, -34.0, -35.0, -34.5, -35.0, -35.5, -36.0, -36.5, -37.0, -37.5, -38.0, -38.5],
incomeExpenseDiff: [1.0, 1.5, -0.5, -1.5, -2.5, -3.5, -4.5, -5.5, -6.5, -7.5, -8.5, -9.5],
balance: [11.0, 12.5, 12.0, 10.5, 8.0, 4.5, 0.0, -5.5, -12.0, -19.5, -28.0, -37.5],
action: {
cashFlow: "昨年は支出の増加により残高推移が悪化し、今年の前半でも同様の傾向が見られます。収入の減少と支出の増加が重なり、6月の当月収支がマイナスになっていて、残高推移が非常に厳しい状況です。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n現金回収を早めるため、請求の迅速化\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
}
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
収入の予測はいらなくなったので、関連の資材を全部消したい。
また、支出の選択欄の更新をお願いしたい。
完全のコードをください。
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<p className="forecast-instruction">
支出予測値を入力してください。<br />
空欄の場合、予測値は推定されます。
</p>
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5</span>
<span>5月</span>
<span>-14</span>
</div>
<div className="row">
<span>6月</span>
<span>-19</span>
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const defaultIncome = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const defaultExpense = [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5]; // デフォルトの支出データを設定
updateChartAndAction(defaultIncome, defaultExpense, false);
}, [expenseLevel]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setExpenseLevel(e.target.value);
// ダミーデータを使用してグラフと推奨アクションを更新
const selectedData = dataSets[e.target.value];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
// カスタム予測を閉じる
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedLevel = e.target.value;
setExpenseLevel(selectedLevel);
const selectedData = dataSets[selectedLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
setIsCustomForecastOpen(false);
};
useEffect(() => {
const selectedData = dataSets[expenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
}, [expenseLevel]);
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<p className="forecast-instruction">
支出予測値を入力してください。<br />
空欄の場合、予測値は推定されます。
</p>
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5</span>
<span>5月</span>
<span>-14</span>
</div>
<div className="row">
<span>6月</span>
<span>-19</span>
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const defaultIncome = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const defaultExpense = [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5]; // デフォルトの支出データを設定
updateChartAndAction(defaultIncome, defaultExpense, false);
}, []);
useEffect(() => {
const selectedData = dataSets[expenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
}, [expenseLevel]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedLevel = e.target.value;
setExpenseLevel(selectedLevel);
const selectedData = dataSets[selectedLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<p className="forecast-instruction">
支出予測値を入力してください。<br />
空欄の場合、予測値は推定されます。
</p>
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5</span>
<span>5月</span>
<span>-14</span>
</div>
<div className="row">
<span>6月</span>
<span>-19</span>
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const defaultIncome = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const defaultExpense = [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5]; // デフォルトの支出データを設定
updateChartAndAction(defaultIncome, defaultExpense, false);
}, []);
useEffect(() => {
const selectedData = dataSets[expenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
}, [expenseLevel]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedLevel = e.target.value;
setExpenseLevel(selectedLevel);
const selectedData = dataSets[selectedLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
// Chart.jsのコンポーネントを登録
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
// 型定義
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
// 折りたたみパネルのコンポーネント
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
// アラートボックスのコンポーネント
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
// カスタム支出予測コンポーネント
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<p className="forecast-instruction">
支出予測値を入力してください。<br />
空欄の場合、予測値は推定されます。
</p>
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5</span>
<span>5月</span>
<span>-14</span>
</div>
<div className="row">
<span>6月</span>
<span>-19</span>
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
// メインのページコンポーネント
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const defaultIncome = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const defaultExpense = [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5];
updateChartAndAction(defaultIncome, defaultExpense, false);
}, []);
useEffect(() => {
const selectedData = dataSets[expenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
}, [expenseLevel]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedLevel = e.target.value;
setExpenseLevel(selectedLevel);
const selectedData = dataSets[selectedLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
// キャッシュフロー詳細データ
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
// グラフと推奨アクションを更新
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
// カスタム予測を閉じる
setIsCustomForecastOpen(false);
};
useEffect(() => {
const selectedData = dataSets[expenseLevel]; // 選択されたデータセットを取得
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
}
}, [expenseLevel]); // `expenseLevel`を依存関係として追加
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<p className="forecast-instruction">
支出予測値を入力してください。<br />
空欄の場合、予測値は推定されます。
</p>
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5</span>
<span>5月</span>
<span>-14</span>
</div>
<div className="row">
<span>6月</span>
<span>-19</span>
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const selectedData = dataSets[expenseLevel]; // 選択されたデータセットを取得
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
}
}, [expenseLevel]); // `expenseLevel`を依存関係として追加
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
// グラフと推奨アクションを更新
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
// カスタム予測を閉じる
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
import { useState, useEffect, useRef } from "react";
import { Chart } from 'react-chartjs-2';
import {
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend,
Chart as ChartJS,
} from 'chart.js';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
const EmptyPage = () => {
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const [showReferenceWindow, setShowReferenceWindow] = useState(false);
const [windowPosition, setWindowPosition] = useState({ x: 100, y: 100 });
const chartRef = useRef<ChartJS | null>(null);
const windowRef = useRef<HTMLDivElement | null>(null);
// Function to handle window movement
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
const startX = e.clientX - windowPosition.x;
const startY = e.clientY - windowPosition.y;
const handleMouseMove = (moveEvent: MouseEvent) => {
setWindowPosition({
x: moveEvent.clientX - startX,
y: moveEvent.clientY - startY,
});
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
// Render Movable Window
const renderReferenceWindow = () => (
<div
className="reference-window"
style={{ top: windowPosition.y, left: windowPosition.x }}
ref={windowRef}
onMouseDown={handleMouseDown}
>
<div className="window-header">
参考資料
<button onClick={() => setShowReferenceWindow(false)}>✖</button>
</div>
<div className="window-content">
{/* Add any content you want here */}
<p>ここに参考資料の内容を追加できます。</p>
</div>
</div>
);
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>資金繰り表</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
{/* Your existing content */}
</div>
<button
className="reference-btn"
onClick={() => setShowReferenceWindow(true)}
style={{ position: 'fixed', bottom: '20px', right: '20px' }}
>
参考資料を開く
</button>
{showReferenceWindow && renderReferenceWindow()}
</div>
);
};
export default EmptyPage;
.reference-btn {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
.reference-window {
position: fixed;
width: 300px;
height: 200px;
background-color: white;
border: 1px solid #ddd;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
cursor: move;
}
.window-header {
background-color: #f0f0f0;
padding: 10px;
cursor: grab;
display: flex;
justify-content: space-between;
align-items: center;
}
.window-header button {
background: none;
border: none;
font-size: 1.2em;
cursor: pointer;
}
.window-content {
padding: 15px;
}
import { useState, useEffect, useRef } from "react";
import { Chart } from 'react-chartjs-2';
import {
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend,
Chart as ChartJS,
} from 'chart.js';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const [showReferenceWindow, setShowReferenceWindow] = useState(false);
const [windowPosition, setWindowPosition] = useState({ x: 100, y: 100 });
const chartRef = useRef<ChartJS | null>(null);
const windowRef = useRef<HTMLDivElement | null>(null);
// Function to handle window movement
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
const startX = e.clientX - windowPosition.x;
const startY = e.clientY - windowPosition.y;
const handleMouseMove = (moveEvent: MouseEvent) => {
setWindowPosition({
x: moveEvent.clientX - startX,
y: moveEvent.clientY - startY,
});
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
// Render Movable Window
const renderReferenceWindow = () => (
<div
className="reference-window"
style={{ top: windowPosition.y, left: windowPosition.x }}
ref={windowRef}
onMouseDown={handleMouseDown}
>
<div className="window-header">
参考資料
<button onClick={() => setShowReferenceWindow(false)}>✖</button>
</div>
<div className="window-content">
{/* Add any content you want here */}
<p>ここに参考資料の内容を追加できます。</p>
</div>
</div>
);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const selectedData = dataSets[expenseLevel]; // 選択されたデータセットを取得
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
}
}, [expenseLevel]); // `expenseLevel`を依存関係として追加
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
// グラフと推奨アクションを更新
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
// カスタム予測を閉じる
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
<button
className="reference-btn"
onClick={() => setShowReferenceWindow(true)}
style={{ position: 'fixed', bottom: '20px', right: '20px' }}
>
参考資料を開く
</button>
{showReferenceWindow && renderReferenceWindow()}
</div>
);
};
const handleButtonClick = () => {
if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setWindowPosition({
x: rect.left,
y: rect.top - 200, // 窗口的高度假设为200px
});
setShowReferenceWindow(true);
}
};
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<p className="forecast-instruction">
支出予測値を入力してください。<br />
空欄の場合、予測値は推定されます。
</p>
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5</span>
<span>5月</span>
<span>-14</span>
</div>
<div className="row">
<span>6月</span>
<span>-19</span>
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const [showReferenceWindow, setShowReferenceWindow] = useState(false);
const [windowPosition, setWindowPosition] = useState({ x: 0, y:0 });
const windowRef = useRef<HTMLDivElement | null>(null);
// Function to handle window movement
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
const startX = e.clientX - windowPosition.x;
const startY = e.clientY - windowPosition.y;
const handleMouseMove = (moveEvent: MouseEvent) => {
setWindowPosition({
x: moveEvent.clientX - startX,
y: moveEvent.clientY - startY,
});
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
// Render Movable Window
const renderReferenceWindow = () => (
<div
className="reference-window"
style={{ top: windowPosition.y, left: windowPosition.x }}
ref={windowRef}
onMouseDown={handleMouseDown}
>
<div className="window-header">
参考資料
<button onClick={() => setShowReferenceWindow(false)}>✖</button>
</div>
<div className="window-content">
{/* Add any content you want here */}
<p>ここに参考資料の内容を追加できます。</p>
</div>
</div>
);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const selectedData = dataSets[expenseLevel]; // 選択されたデータセットを取得
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
}
}, [expenseLevel]); // `expenseLevel`を依存関係として追加
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
// グラフと推奨アクションを更新
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
// カスタム予測を閉じる
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
<button
className="reference-btn"
onClick={() => setShowReferenceWindow(true)}
style={{ position: 'fixed', bottom: '20px', right: '20px' }}
>
?
</button>
{showReferenceWindow && renderReferenceWindow()}
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
const handleButtonClick = () => {
if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setWindowPosition({
x: rect.left,
y: rect.top, // 窗口的初始y位置与按钮一致
});
setShowReferenceWindow(true); // 显示小窗并隐藏按钮
}
};
ERROR in src/components/pages/financingPage.tsx:245:11
TS2304: Cannot find name 'buttonRef'.
243 |
244 | const handleButtonClick = () => {
> 245 | if (buttonRef.current) {
| ^^^^^^^^^
246 | const rect = buttonRef.current.getBoundingClientRect();
247 | setWindowPosition({
248 | x: rect.left,
ERROR in src/components/pages/financingPage.tsx:246:22
TS2304: Cannot find name 'buttonRef'.
244 | const handleButtonClick = () => {
245 | if (buttonRef.current) {
> 246 | const rect = buttonRef.current.getBoundingClientRect();
| ^^^^^^^^^
247 | setWindowPosition({
248 | x: rect.left,
249 | y: rect.top, // 窗口的初始y位置与按钮一致
{/* 仅在showReferenceWindow为false时显示按钮 */}
{!showReferenceWindow && (
<button
ref={buttonRef}
className="reference-btn"
onClick={handleButtonClick}
style={{ position: 'fixed', bottom: '20px', right: '20px' }}
>
?
</button>
)}
{/* 显示小窗 */}
{showReferenceWindow && renderReferenceWindow()}
const [showSidebar, setShowSidebar] = useState(false);
{/* 右上角的小按钮 */}
<button
className="tiny-button"
onClick={handleSidebarToggle}
style={{ position: 'fixed', top: '10px', right: '10px', width: '30px', height: '30px', fontSize: '14px' }}
>
☰
</button>
{/* 侧边栏 */}
{showSidebar && (
<div className="sidebar">
<div className="sidebar-header">
<button onClick={handleSidebarToggle}>✖</button>
</div>
<div className="sidebar-content">
<h2>参考内容</h2>
<p>这里是参考内容的示例,可以根据需要进行更改。</p>
</div>
</div>
)}
</div>
.sidebar {
position: fixed;
right: 0;
top: 0;
width: 300px;
height: 100%;
background-color: white;
border-left: 1px solid #ccc;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
padding: 20px;
}
.sidebar-header {
display: flex;
justify-content: flex-end;
}
.tiny-button {
cursor: pointer;
border: none;
background: #007bff;
color: white;
border-radius: 50%;
}
const handleSidebarToggle = () => {
setShowSidebar(!showSidebar); // 切换侧边栏的显示状态
};
<button
className="tiny-button"
onClick={handleSidebarToggle}
style={{ position: 'absolute', top: '10px', right: '10px', width: '30px', height: '30px', fontSize: '14px' }}
>
☰
</button>
{/* 侧边栏,放置在 main-content 内 */}
{showSidebar && (
<div className="sidebar" style={{
position: 'absolute', // 相对于 main-content 进行绝对定位
right: '0', // 靠右显示
top: '0', // 顶部对齐
height: '100%', // 高度占满 main-content
width: '250px', // 可以根据需要调整侧边栏宽度
backgroundColor: 'white',
borderLeft: '1px solid #ccc',
boxShadow: '-2px 0 5px rgba(0, 0, 0, 0.1)',
zIndex: 1000,
padding: '20px'
}}>
<div className="sidebar-header">
<button onClick={handleSidebarToggle}>✖</button>
</div>
<div className="sidebar-content">
<h2>参考内容</h2>
<p>这里是参考内容的示例,可以根据需要进行更改。</p>
</div>
</div>
)}
const calculateAverage = (values) => {
const validValues = values.filter(value => value !== '' && value !== '-');
const sum = validValues.reduce((acc, val) => acc + parseFloat(val), 0);
return validValues.length ? sum / validValues.length : 0;
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
// 计算输入部分的平均值
const avgExpense = calculateAverage(Object.values(customExpenses));
const updatedExpenses = [
...baseExpense.map(value => value + avgExpense), // 将基本的-9.5, -14, -19加上平均值
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// 使用更新后的数据更新图表和推荐行动
updateChartAndAction(income, updatedExpenses, true);
// 滚动到图表显示区域
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム入力</button>
{isCustomForecastOpen && (
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span> -9.5 百万円</span>
<span>5月</span>
<span> -14 百万円</span>
</div>
<div className="row">
<span>6月</span>
<span> -19 百万円</span>
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const selectedData = dataSets[expenseLevel]; // 選択されたデータセットを取得
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
}
}, [expenseLevel]); // `expenseLevel`を依存関係として追加
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
// グラフと推奨アクションを更新
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
// カスタム予測を閉じる
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
.content-wrapper {
padding: 0; /* 确保没有内边距 */
}
.page-cover {
margin: 0; /* 确保没有外边距 */
}
.page-cover-title-frame {
margin: 0; /* 确保没有外边距 */
padding: 0; /* 确保没有内边距 */
}
.alert-container {
padding: 0;
margin: 0;
}
.alert-box {
background-color: white;
color: black;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin: 0; /* 确保没有外边距 */
display: flex;
justify-content: space-between; /* 确保按钮在右侧 */
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.alert-content {
display: flex;
align-items: center;
justify-content: center; /* 确保内容居中 */
flex-grow: 1;
}
.alert-icon {
color: red;
font-size: 2rem; /* 调整感叹号的大小 */
margin-right: 10px; /* 感叹号与文字之间的间距 */
}
.alert-message {
text-align: center; /* 文本居中对齐 */
font-size: 1.2rem;
}
.close-btn {
background-color: transparent;
border: 1px solid red;
border-radius: 15px; /* 椭圆形 */
color: red;
cursor: pointer;
font-size: 1rem;
padding: 5px 10px;
margin-left: 10px;
}
.main-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
margin-top: 20px;
padding-left: 20px; /* 增加左侧边距,避免图表贴边 */
box-sizing: border-box; /* 确保内外边距包含在宽度内 */
}
.left-container {
flex: 1;
max-width: 48%; /* 图表宽度 */
height: auto; /* 图表高度 */
padding: 20px;
margin-left: 2%;
border-radius: 5px;
}
.graph-container {
background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 30px;
border: 1px solid #000;
}
.right-container {
flex: 1;
max-width: 48%;
padding: 20px;
border-radius: 5px;
margin-right: 2%;
}
.actionbox-title {
text-align: center;
background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
border-radius: 5px;
font-size: 1rem;
color: white;
padding: 5px;
}
.actionbox-message{
background-color: #f1f1f1;
color: #000;
font-size: 1rem;
height:278px;
overflow-y: auto;
padding: 20px;
}
.actionbox-message::-webkit-scrollbar {
width: 12px;
}
.actionbox-message::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.actionbox-message::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 10px;
border: 3px solid #f1f1f1;
}
.actionbox-message::-webkit-scrollbar-thumb:hover {
background: #555;
}
.action-section h3{
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
.collapsible-panels {
margin-top: 30px;
padding: 20px;
background-color: #f9f9f9;
}
.collapsible-panel {
margin-bottom: 10px;
width: 100%; /* 确保折叠面板宽度填满容器 */
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #f1f1f1;
cursor: pointer;
border-radius: 5px;
}
.panel-title {
font-weight: bold;
font-size: 1.2rem;
}
.panel-money {
font-weight: bold;
margin-left: auto;
font-size: 1.2rem;
}
.panel-content {
padding: 10px;
background-color: #ffffff;
border: 1px solid #f1f1f1;
border-radius: 0 0 5px 5px;
}
.details-container {
display: grid;
grid-template-columns: 1fr 1fr;
width: fit-content;
gap: 10px;
}
.detail-item {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.detail-item span {
margin-right: 5px;
text-align: left;
white-space: nowrap;
}
/* 新增样式 */
.additional-section {
display: flex;
margin-top: 10px;
}
.data-filter, .data-comparison {
height: auto;
width: 50%;
padding: 20px;
}
.data-filter h2, .data-comparison h2 {
margin-bottom: 20px;
text-align: center;
font-size: 1rem; /* 调整字体大小 */
color: white; /* 白色字体 */
background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
padding: 10px; /* 增加内边距以确保背景颜色显示为长方形 */
border-radius: 5px; /* 圆角 */
}
.filter-group, .comparison-group {
display: flex;
justify-content: space-between;
align-items: center;
}
.comparison-btn {
width: 30%;
padding: 10px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
}
.filter-btn {
width: 40%;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
font-size: 0.9rem;
text-align: center;
margin-right: 10px;
white-space: nowrap;
}
.filter-select {
width: 60%;
padding: 10px;
border: 1px solid #ccc;
background-color: white;
border-radius: 20px;
font-size: 1rem;
text-align: center;
}
.comparison-btn {
width: 100%;
margin-bottom: 10px;
border-radius: 20px;
background-color: #f1f1f1;
color: black;
}
.comparison-btn.active {
background-color: #3498db;
}
.custom-expense-forecast {
width: 100%;
}
.custom-expense-btn {
width: 40%;
margin-left: 20px;
padding: 5px;
background-color: #3498db;
border-radius: 10px;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}
.custom-expense-btn:hover {
background-color: #007bff;
}
.form-container {
width: 90%;
margin-left: 20px;
background-color: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.update-btn {
width: 100%;
padding: 2px 1px;
background-color: #28a745;
border-radius: 10px;
color: white;
border: none;
cursor: pointer;
margin-top: 10px;
font-size: 16px;
}
.update-btn:hover {
background-color: #218838;
}
.form-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.row span {
flex: 1; /* 各要素が均等に分かれる */
text-align: center; /* テキストを中央に配置 */
}
.row input {
max-width: 10%; /* 入力欄の幅を枠の1/4に制限 */
text-align: center;
}
.forecast-instruction {
margin-bottom: 10px;
color: #333;
font-size: 12px;
line-height: 1.5;
}
.form-content {
display: flex;
flex-wrap: wrap; /* 要素を複数行に配置 */
gap: 10px; /* 要素間のスペースを確保 */
justify-content: space-between; /* 左右に要素を配置 */
}
.row {
display: flex;
align-items: center;
width: 48%; /* 左右に2列表示するための幅調整 */
gap: 10px;
margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}
.row span {
flex: 1; /* 各要素が均等に分かれる */
text-align: center; /* テキストを中央に配置 */
}
.row input {
flex: 1; /* 入力欄が残りのスペースを占有 */
text-align: center;
}
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5 百万円</span>
</div>
<div className="row">
<span>5月</span>
<span>-14 百万円</span>
</div>
<div className="row">
<span>6月</span>
<span>-19 百万円</span>
</div>
<div className="row">
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
</div>
<div className="row">
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
</div>
<div className="row">
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
</div>
<div className="row">
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
</div>
<div className="row">
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5 百万円</span>
</div>
<div className="row">
<span>5月</span>
<span>-14 百万円</span>
</div>
<div className="row">
<span>6月</span>
<span>-19 百万円</span>
</div>
<div className="row">
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
</div>
<div className="row">
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
</div>
<div className="row">
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
</div>
<div className="row">
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
</div>
<div className="row">
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
.form-content {
display: flex;
flex-wrap: wrap; /* 要素を複数行に配置 */
gap: 10px; /* 要素間のスペースを確保 */
justify-content: space-between; /* 左右に要素を配置 */
}
.row {
display: flex;
align-items: center;
width: 48%; /* 左右に2列表示するための幅調整 */
gap: 10px;
margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}
.row span {
flex: 1; /* 各要素が均等に分かれる */
text-align: center; /* テキストを中央に配置 */
}
.row input {
width: 60px; /* 入力欄の幅を指定して短くする */
text-align: center;
}
.form-content {
display: flex;
flex-wrap: wrap; /* 要素を複数行に配置 */
gap: 10px; /* 要素間のスペースを確保 */
justify-content: space-between; /* 左右に要素を配置 */
}
.row {
display: flex;
align-items: center;
width: 48%; /* 左右に2列表示するための幅調整 */
gap: 10px;
margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}
.row span {
width: 40px; /* 「月」の幅を固定して揃える */
text-align: center; /* テキストを中央に配置 */
}
.row input {
width: 60px; /* 入力欄の幅を固定 */
text-align: center;
}
.amount-text {
width: 80px; /* 「百万円」の幅を固定して揃える */
text-align: left; /* テキストを左揃え */
}
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span className="amount-text">-9.5 百万円</span>
</div>
<div className="row">
<span>5月</span>
<span className="amount-text">-14 百万円</span>
</div>
<div className="row">
<span>6月</span>
<span className="amount-text">-19 百万円</span>
</div>
<div className="row">
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
.form-content {
display: flex;
flex-wrap: wrap; /* 要素を複数行に配置 */
gap: 10px; /* 要素間のスペースを確保 */
justify-content: space-between; /* 左右に要素を配置 */
}
.row {
display: flex;
align-items: center;
width: 48%; /* 左右に2列表示するための幅調整 */
gap: 10px;
margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}
.row span {
width: 40px; /* 「月」の幅を固定して揃える */
text-align: center; /* テキストを中央に配置 */
}
.row input {
width: 60px; /* 入力欄の幅を固定 */
text-align: center;
}
.amount-text {
width: 80px; /* 「百万円」の幅を固定して揃える */
text-align: left; /* テキストを左揃え */
}
<div className="form-container">
<div className="form-content">
{/* 左側(4月から9月) */}
<div className="row">
<span>4月</span>
<input type="text" value={customExpenses['4月']} onChange={(e) => handleExpenseChange('4月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>5月</span>
<input type="text" value={customExpenses['5月']} onChange={(e) => handleExpenseChange('5月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>6月</span>
<input type="text" value={customExpenses['6月']} onChange={(e) => handleExpenseChange('6月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
{/* 右側(10月から3月) */}
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
Issues checking in progress...
ERROR in src/components/pages/financingPage.tsx:165:33
TS7053: Element implicitly has an 'any' type because expression of type '"4月"' can't be used to index type 'CustomExpenses'.
Property '4月' does not exist on type 'CustomExpenses'.
163 | <div className="row">
164 | <span>4月</span>
> 165 | <input type="text" value={customExpenses['4月']} onChange={(e) => handleExpenseChange('4月', e.target.value)} />
| ^^^^^^^^^^^^^^^^^^^^
166 | <span className="amount-text">百万円</span>
167 | </div>
168 | <div className="row">
ERROR in src/components/pages/financingPage.tsx:165:92
TS2345: Argument of type '"4月"' is not assignable to parameter of type 'keyof CustomExpenses'.
163 | <div className="row">
164 | <span>4月</span>
> 165 | <input type="text" value={customExpenses['4月']} onChange={(e) => handleExpenseChange('4月', e.target.value)} />
| ^^^^
166 | <span className="amount-text">百万円</span>
167 | </div>
168 | <div className="row">
ERROR in src/components/pages/financingPage.tsx:170:33
TS7053: Element implicitly has an 'any' type because expression of type '"5月"' can't be used to index type 'CustomExpenses'.
Property '5月' does not exist on type 'CustomExpenses'.
168 | <div className="row">
169 | <span>5月</span>
> 170 | <input type="text" value={customExpenses['5月']} onChange={(e) => handleExpenseChange('5月', e.target.value)} />
| ^^^^^^^^^^^^^^^^^^^^
171 | <span className="amount-text">百万円</span>
172 | </div>
173 | <div className="row">
ERROR in src/components/pages/financingPage.tsx:170:92
TS2345: Argument of type '"5月"' is not assignable to parameter of type 'keyof CustomExpenses'.
168 | <div className="row">
169 | <span>5月</span>
> 170 | <input type="text" value={customExpenses['5月']} onChange={(e) => handleExpenseChange('5月', e.target.value)} />
| ^^^^
171 | <span className="amount-text">百万円</span>
172 | </div>
173 | <div className="row">
ERROR in src/components/pages/financingPage.tsx:175:33
TS7053: Element implicitly has an 'any' type because expression of type '"6月"' can't be used to index type 'CustomExpenses'.
Property '6月' does not exist on type 'CustomExpenses'.
173 | <div className="row">
174 | <span>6月</span>
> 175 | <input type="text" value={customExpenses['6月']} onChange={(e) => handleExpenseChange('6月', e.target.value)} />
| ^^^^^^^^^^^^^^^^^^^^
176 | <span className="amount-text">百万円</span>
177 | </div>
178 | <div className="row">
ERROR in src/components/pages/financingPage.tsx:175:92
TS2345: Argument of type '"6月"' is not assignable to parameter of type 'keyof CustomExpenses'.
173 | <div className="row">
174 | <span>6月</span>
> 175 | <input type="text" value={customExpenses['6月']} onChange={(e) => handleExpenseChange('6月', e.target.value)} />
| ^^^^
176 | <span className="amount-text">百万円</span>
177 | </div>
178 | <div className="row">
.form-content {
display: flex;
flex-wrap: wrap; /* 要素を複数行に配置 */
gap: 10px; /* 要素間のスペースを確保 */
justify-content: space-between; /* 左右に要素を配置 */
}
.row {
display: flex;
align-items: center; /* 垂直方向に中央揃え */
width: 48%; /* 左右に2列表示するための幅調整 */
gap: 10px;
margin-bottom: 10px; /* 下にマージンを追加して要素間のスペースを確保 */
}
.row span {
width: 40px; /* 「月」の幅を固定して揃える */
text-align: center; /* テキストを中央に配置 */
}
.row input {
width: 60px; /* 入力欄の幅を固定 */
text-align: center;
}
.amount-text {
width: 60px; /* 「百万円」の幅を入力欄と揃えて固定 */
text-align: center; /* テキストを中央に配置 */
}
<div className="form-container">
<div className="form-content">
{/* 左側(4月から9月) */}
<div className="row">
<span>4月</span>
<input type="text" value={customExpenses['4月']} onChange={(e) => handleExpenseChange('4月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>5月</span>
<input type="text" value={customExpenses['5月']} onChange={(e) => handleExpenseChange('5月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>6月</span>
<input type="text" value={customExpenses['6月']} onChange={(e) => handleExpenseChange('6月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
{/* 右側(10月から3月) */}
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
<div className="row">
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
<span className="amount-text">百万円</span>
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
.content-wrapper {
padding: 0; /* 确保没有内边距 */
}
.page-cover {
margin: 0; /* 确保没有外边距 */
}
.page-cover-title-frame {
margin: 0; /* 确保没有外边距 */
padding: 0; /* 确保没有内边距 */
}
.alert-container {
padding: 0;
margin: 0;
}
.alert-box {
background-color: white;
color: black;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin: 0; /* 确保没有外边距 */
display: flex;
justify-content: space-between; /* 确保按钮在右侧 */
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.alert-content {
display: flex;
align-items: center;
justify-content: center; /* 确保内容居中 */
flex-grow: 1;
}
.alert-icon {
color: red;
font-size: 2rem; /* 调整感叹号的大小 */
margin-right: 10px; /* 感叹号与文字之间的间距 */
}
.alert-message {
text-align: center; /* 文本居中对齐 */
font-size: 1.2rem;
}
.close-btn {
background-color: transparent;
border: 1px solid red;
border-radius: 15px; /* 椭圆形 */
color: red;
cursor: pointer;
font-size: 1rem;
padding: 5px 10px;
margin-left: 10px;
}
.main-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
margin-top: 20px;
padding-left: 20px; /* 增加左侧边距,避免图表贴边 */
box-sizing: border-box; /* 确保内外边距包含在宽度内 */
}
.left-container {
flex: 1;
max-width: 48%; /* 图表宽度 */
height: auto; /* 图表高度 */
padding: 20px;
margin-left: 2%;
border-radius: 5px;
}
.graph-container {
background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 30px;
border: 1px solid #000;
}
.right-container {
flex: 1;
max-width: 48%;
padding: 20px;
border-radius: 5px;
margin-right: 2%;
}
.actionbox-title {
text-align: center;
background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
border-radius: 5px;
font-size: 1rem;
color: white;
padding: 5px;
}
.actionbox-message{
background-color: #f1f1f1;
color: #000;
font-size: 1rem;
height:278px;
overflow-y: auto;
padding: 20px;
}
.actionbox-message::-webkit-scrollbar {
width: 12px;
}
.actionbox-message::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.actionbox-message::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 10px;
border: 3px solid #f1f1f1;
}
.actionbox-message::-webkit-scrollbar-thumb:hover {
background: #555;
}
.action-section h3{
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
.collapsible-panels {
margin-top: 30px;
padding: 20px;
background-color: #f9f9f9;
}
.collapsible-panel {
margin-bottom: 10px;
width: 100%; /* 确保折叠面板宽度填满容器 */
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #f1f1f1;
cursor: pointer;
border-radius: 5px;
}
.panel-title {
font-weight: bold;
font-size: 1.2rem;
}
.panel-money {
font-weight: bold;
margin-left: auto;
font-size: 1.2rem;
}
.panel-content {
padding: 10px;
background-color: #ffffff;
border: 1px solid #f1f1f1;
border-radius: 0 0 5px 5px;
}
.details-container {
display: grid;
grid-template-columns: 1fr 1fr;
width: fit-content;
gap: 10px;
}
.detail-item {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.detail-item span {
margin-right: 5px;
text-align: left;
white-space: nowrap;
}
/* 新增样式 */
.additional-section {
display: flex;
margin-top: 10px;
}
.data-filter, .data-comparison {
height: auto;
width: 50%;
padding: 20px;
}
.data-filter h2, .data-comparison h2 {
margin-bottom: 20px;
text-align: center;
font-size: 1rem; /* 调整字体大小 */
color: white; /* 白色字体 */
background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
padding: 10px; /* 增加内边距以确保背景颜色显示为长方形 */
border-radius: 5px; /* 圆角 */
}
.filter-group, .comparison-group {
display: flex;
justify-content: space-between;
align-items: center;
}
.comparison-btn {
width: 30%;
padding: 10px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
}
.filter-btn {
width: 40%;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
font-size: 0.9rem;
text-align: center;
margin-right: 10px;
white-space: nowrap;
}
.filter-select {
width: 60%;
padding: 10px;
border: 1px solid #ccc;
background-color: white;
border-radius: 20px;
font-size: 1rem;
text-align: center;
}
.comparison-btn {
width: 100%;
margin-bottom: 10px;
border-radius: 20px;
background-color: #f1f1f1;
color: black;
}
.comparison-btn.active {
background-color: #3498db;
}
.custom-expense-forecast {
width: 100%;
}
.custom-expense-btn {
margin-top: 15px;
width: 100%;
margin-left: 0px;
padding: 5px;
background-color: #3498db;
border-radius: 10px;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}
.custom-expense-btn:hover {
background-color: #007bff;
}
.form-container {
width: 200%;
flex-wrap: wrap;
background-color: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.update-btn {
width: 100%;
padding: 2px 1px;
background-color: #28a745;
border-radius: 10px;
color: white;
border: none;
cursor: pointer;
margin-top: 10px;
font-size: 16px;
}
.update-btn:hover {
background-color: #218838;
}
.form-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.row span {
width: 10%;
flex: 1; /* 各要素が均等に分かれる */
text-align: center; /* テキストを中央に配置 */
}
.row input {
max-width: 10%; /* 入力欄の幅を枠の1/4に制限 */
text-align: center;
}
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム入力</button>
{isCustomForecastOpen && (
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5 百万円</span>
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
</div>
<div className="row">
<span>5月</span>
<span> -14 百万円</span>
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
</div>
<div className="row">
<span>6月</span>
<span> -19 百万円</span>
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
</div>
<div className="row">
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
</div>
<div className="row">
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const selectedData = dataSets[expenseLevel]; // 選択されたデータセットを取得
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
}
}, [expenseLevel]); // `expenseLevel`を依存関係として追加
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
// グラフと推奨アクションを更新
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
// カスタム予測を閉じる
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5 百万円</span>
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />百万円
</div>
<div className="row">
<span>5月</span>
<span> -14 百万円</span>
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />百万円
</div>
<div className="row">
<span>6月</span>
<span> -19 百万円</span>
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />百万円
</div>
<div className="row">
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />百万円
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />百万円
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />百万円
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />百万円
</div>
<div className="row">
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />百万円
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />百万円
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
.content-wrapper {
padding: 0; /* 确保没有内边距 */
}
.page-cover {
margin: 0; /* 确保没有外边距 */
}
.page-cover-title-frame {
margin: 0; /* 确保没有外边距 */
padding: 0; /* 确保没有内边距 */
}
.alert-container {
padding: 0;
margin: 0;
}
.alert-box {
background-color: white;
color: black;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin: 0; /* 确保没有外边距 */
display: flex;
justify-content: space-between; /* 确保按钮在右侧 */
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.alert-content {
display: flex;
align-items: center;
justify-content: center; /* 确保内容居中 */
flex-grow: 1;
}
.alert-icon {
color: red;
font-size: 2rem; /* 调整感叹号的大小 */
margin-right: 10px; /* 感叹号与文字之间的间距 */
}
.alert-message {
text-align: center; /* 文本居中对齐 */
font-size: 1.2rem;
}
.close-btn {
background-color: transparent;
border: 1px solid red;
border-radius: 15px; /* 椭圆形 */
color: red;
cursor: pointer;
font-size: 1rem;
padding: 5px 10px;
margin-left: 10px;
}
.main-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
margin-top: 20px;
padding-left: 20px; /* 增加左侧边距,避免图表贴边 */
box-sizing: border-box; /* 确保内外边距包含在宽度内 */
}
.left-container {
flex: 1;
max-width: 48%; /* 图表宽度 */
height: auto; /* 图表高度 */
padding: 20px;
margin-left: 2%;
border-radius: 5px;
}
.graph-container {
background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 30px;
border: 1px solid #000;
}
.right-container {
flex: 1;
max-width: 48%;
padding: 20px;
border-radius: 5px;
margin-right: 2%;
}
.actionbox-title {
text-align: center;
background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
border-radius: 5px;
font-size: 1rem;
color: white;
padding: 5px;
}
.actionbox-message{
background-color: #f1f1f1;
color: #000;
font-size: 1rem;
height:278px;
overflow-y: auto;
padding: 20px;
}
.actionbox-message::-webkit-scrollbar {
width: 12px;
}
.actionbox-message::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.actionbox-message::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 10px;
border: 3px solid #f1f1f1;
}
.actionbox-message::-webkit-scrollbar-thumb:hover {
background: #555;
}
.action-section h3{
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
.collapsible-panels {
margin-top: 30px;
padding: 20px;
background-color: #f9f9f9;
}
.collapsible-panel {
margin-bottom: 10px;
width: 100%; /* 确保折叠面板宽度填满容器 */
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #f1f1f1;
cursor: pointer;
border-radius: 5px;
}
.panel-title {
font-weight: bold;
font-size: 1.2rem;
}
.panel-money {
font-weight: bold;
margin-left: auto;
font-size: 1.2rem;
}
.panel-content {
padding: 10px;
background-color: #ffffff;
border: 1px solid #f1f1f1;
border-radius: 0 0 5px 5px;
}
.details-container {
display: grid;
grid-template-columns: 1fr 1fr;
width: fit-content;
gap: 10px;
}
.detail-item {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.detail-item span {
margin-right: 5px;
text-align: left;
white-space: nowrap;
}
/* 新增样式 */
.additional-section {
display: flex;
margin-top: 10px;
}
.data-filter, .data-comparison {
height: auto;
width: 50%;
padding: 20px;
}
.data-filter h2, .data-comparison h2 {
margin-bottom: 20px;
text-align: center;
font-size: 1rem; /* 调整字体大小 */
color: white; /* 白色字体 */
background-color: var(--sidebar-theme-color); /* 深蓝色背景 */
padding: 10px; /* 增加内边距以确保背景颜色显示为长方形 */
border-radius: 5px; /* 圆角 */
}
.filter-group, .comparison-group {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.comparison-btn {
width: 30%;
padding: 10px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
}
.filter-btn {
width: 40%;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
font-size: 0.9rem;
text-align: center;
margin-right: 10px;
white-space: nowrap;
}
.filter-select {
width: 60%;
padding: 10px;
border: 1px solid #ccc;
background-color: white;
border-radius: 20px;
font-size: 1rem;
text-align: center;
}
.comparison-btn {
width: 100%;
margin-bottom: 10px;
border-radius: 20px;
background-color: #f1f1f1;
color: black;
}
.comparison-btn.active {
background-color: #3498db;
}
.custom-expense-forecast {
width: 100%;
}
.custom-expense-btn {
width: 100%;
margin-top: 5px;
padding: 5px;
background-color: #3498db;
border-radius: 10px;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}
.custom-expense-btn:hover {
background-color: #007bff;
}
.form-container {
background-color: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.update-btn {
width: 100%;
padding: 2px 1px;
background-color: #28a745;
border-radius: 10px;
color: white;
border: none;
cursor: pointer;
margin-top: 10px;
font-size: 16px;
}
.update-btn:hover {
background-color: #218838;
}
.form-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.row span {
flex: 1; /* 各要素が均等に分かれる */
text-align: center; /* テキストを中央に配置 */
}
.row input {
max-width: 22%; /* 入力欄の幅を枠の1/4に制限 */
text-align: center;
}
.forecast-instruction {
margin-bottom: 10px;
color: #333;
font-size: 12px;
line-height: 1.5;
}
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState } from "react";
import { Chart } from 'react-chartjs-2';
ChartJS.register(
BarController,
LineController,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend
);
type DataSet = {
income: number[];
expense: number[];
incomeExpenseDiff?: number[];
balance?: number[];
action?: {
cashFlow: string;
countermeasure: string;
};
};
type CustomExpenses = {
'7月': string;
'8月': string;
'9月': string;
'10月': string;
'11月': string;
'12月': string;
'1月': string;
'2月': string;
'3月': string;
};
const CollapsiblePanel = ({ title, money, details }: { title: string; money: string; details: { month: string, amount: string, alert?: boolean }[] }) => {
const [isOpen, setIsOpen] = useState(false);
const togglePanel = () => {
setIsOpen(!isOpen);
};
return (
<div className="collapsible-panel">
<div className="panel-header" onClick={togglePanel}>
<div className="panel-title">{title}</div>
<div className="panel-money">{money}</div>
</div>
{isOpen && (
<div className="panel-content">
<div className="details-container">
{details.map((detail, index) => (
<div key={index} className="detail-item" style={{ color: 'black' }}>
<span>{detail.month}</span>
<span style={{ color: detail.alert ? 'red' : 'black', marginLeft: '5px' }}>
{detail.amount}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const AlertBox = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className="alert-box">
<div className="alert-content">
<i className="fa fa-exclamation-circle alert-icon" aria-hidden="true"></i>
<span className="alert-message">{message}</span>
</div>
<button className="close-btn" onClick={onClose}>非表示</button>
</div>
);
};
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }: { updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; chartRef: React.RefObject<ChartJS>; isCustomForecastOpen: boolean; setIsCustomForecastOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState); // カスタム予測の開閉状態を管理
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100); // フォームが開いた後にスクロールする
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
// カスタムデータを使用してグラフと推奨アクションを更新
updateChartAndAction(income, updatedExpenses, true);
// グラフ表示エリアにスクロール
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5</span>
<span>5月</span>
<span>-14</span>
</div>
<div className="row">
<span>6月</span>
<span>-19</span>
<span>7月</span>
<input type="text" value={customExpenses['7月']} onChange={(e) => handleExpenseChange('7月', e.target.value)} />
</div>
<div className="row">
<span>8月</span>
<input type="text" value={customExpenses['8月']} onChange={(e) => handleExpenseChange('8月', e.target.value)} />
<span>9月</span>
<input type="text" value={customExpenses['9月']} onChange={(e) => handleExpenseChange('9月', e.target.value)} />
</div>
<div className="row">
<span>10月</span>
<input type="text" value={customExpenses['10月']} onChange={(e) => handleExpenseChange('10月', e.target.value)} />
<span>11月</span>
<input type="text" value={customExpenses['11月']} onChange={(e) => handleExpenseChange('11月', e.target.value)} />
</div>
<div className="row">
<span>12月</span>
<input type="text" value={customExpenses['12月']} onChange={(e) => handleExpenseChange('12月', e.target.value)} />
<span>1月</span>
<input type="text" value={customExpenses['1月']} onChange={(e) => handleExpenseChange('1月', e.target.value)} />
</div>
<div className="row">
<span>2月</span>
<input type="text" value={customExpenses['2月']} onChange={(e) => handleExpenseChange('2月', e.target.value)} />
<span>3月</span>
<input type="text" value={customExpenses['3月']} onChange={(e) => handleExpenseChange('3月', e.target.value)} />
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
const EmptyPage = () => {
const pageName = '資金繰り表';
const [showAlert, setShowAlert] = useState(true);
const [expenseLevel, setExpenseLevel] = useState('中立');
const [activeComparison, setActiveComparison] = useState('時系列比較');
const [cashFlow, setCashFlow] = useState('');
const [countermeasure, setCountermeasure] = useState('');
const [isCustomForecastOpen, setIsCustomForecastOpen] = useState(false);
const chartRef = useRef<ChartJS | null>(null);
const options = {
responsive: true,
layout: {
padding: {
top: 40,
bottom: 0,
left: 0,
right: 0,
},
},
plugins: {
legend: {
display: false,
labels: {
color: 'white',
boxWidth: 0,
boxHeight: 0,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '[百万円]',
},
},
},
};
const updateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => {
const incomeExpenseDiff = income.map((incomeValue, i) => incomeValue + expenses[i]);
const balance = income.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : initialBalance) + incomeValue + expenses[i];
acc.push(newBalance);
return acc;
}, [] as number[]);
if (chartRef.current) {
chartRef.current.data = {
labels: ['4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', '1月', '2月', '3月'],
datasets: [
{
type: 'bar',
label: '収入',
data: income,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(153, 102, 255, 0.5)' : 'rgba(153, 102, 255, 0.2)';
},
},
{
type: 'bar',
label: '支出',
data: expenses,
backgroundColor: function (context) {
const index = context.dataIndex;
return index < 3 ? 'rgba(54, 162, 235, 0.5)' : 'rgba(54, 162, 235, 0.2)';
},
},
{
type: 'line',
label: '当月収支',
data: incomeExpenseDiff,
borderColor: 'blue',
backgroundColor: 'blue',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'circle',
pointRadius: 4,
pointHoverRadius: 6,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'blue';
}
},
{
type: 'line',
label: '月末残高',
data: balance,
borderColor: 'black',
backgroundColor: 'black',
fill: false,
tension: 0.1,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 6,
pointHoverRadius: 8,
segment: {
borderDash: (ctx) => {
return ctx.p0DataIndex < 3 ? [] : [5, 5];
},
},
pointBackgroundColor: function (context) {
const index = context.dataIndex;
const value = context.dataset.data[index] ?? 0;
return value < 0 ? 'red' : 'black';
}
},
{
type: 'line',
label: '前年度残高',
data: [10.0, 12.0, 11.5, 5.5, 9.0, 4.5, 8.5, 8.0, 9.5, 4.0, 9.5, 10.0],
borderColor: 'gray',
backgroundColor: 'gray',
fill: false,
borderWidth: 2,
pointStyle: 'rectRot',
pointRadius: 4,
pointHoverRadius: 6,
},
],
};
chartRef.current.update();
}
// 推奨アクションの更新
if (isCustom) {
setCashFlow('-');
setCountermeasure('-');
} else {
const selectedData = dataSets[expenseLevel];
if (selectedData && selectedData.action) {
setCashFlow(selectedData.action.cashFlow);
setCountermeasure(selectedData.action.countermeasure);
}
}
};
useEffect(() => {
const selectedData = dataSets[expenseLevel]; // 選択されたデータセットを取得
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false); // グラフとアクションを更新
}
}, [expenseLevel]); // `expenseLevel`を依存関係として追加
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
// グラフと推奨アクションを更新
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
// カスタム予測を閉じる
setIsCustomForecastOpen(false);
};
useEffect(() => {
const customLegendPlugin = {
id: 'customLegend',
afterDraw: function (chart: ChartJS<keyof ChartTypeRegistry, unknown[], unknown>) {
const legend = chart?.legend;
if (!legend || !legend.legendItems) return;
const ctx = chart.ctx;
const itemWidth = chart.width / 4;
let currentX = (chart.width - itemWidth * 3) / 2;
let currentY = 10;
legend.legendItems.forEach((legendItem, i) => {
if (i === 3) {
currentX = (chart.width - itemWidth * 2) / 2;
currentY += 20;
}
if (legendItem.text === '当月収支' || legendItem.text === '前年度残高' || legendItem.text === '月末残高') {
ctx.save();
ctx.strokeStyle = legendItem.text === '当月収支' ? 'blue' : legendItem.text === '前年度残高' ? 'gray' : 'black';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX + 40, currentY);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.fillStyle = legendItem.fillStyle as string;
ctx.fillRect(currentX, currentY - 5, 40, 10);
ctx.restore();
}
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.fillText(legendItem.text, currentX + 50, currentY);
currentX += itemWidth;
});
},
};
ChartJS.register(customLegendPlugin);
return () => {
ChartJS.unregister(customLegendPlugin);
};
}, []);
const defaultData = {
labels: [],
datasets: []
};
return (
<div className="content-wrapper metrics-details">
<section className="page-cover">
<div className="page-cover-title-frame">
<h1>{pageName}</h1>
</div>
</section>
{showAlert && (
<div className="alert-container">
<AlertBox
message="期中に当月収支がマイナスになる期間があります"
onClose={() => setShowAlert(false)}
/>
</div>
)}
<div className="main-content">
<div className="left-container">
<div className="graph-container">
<Chart ref={chartRef} type="bar" data={chartRef.current?.data || defaultData} options={options} />
</div>
<div className="additional-section">
<div className="data-filter">
<h2>データ予測</h2>
<div className="filter-group">
<div className="filter-btn">
<div>支出</div>
<div className='filter-btn-before'>(前年度比)</div>
</div>
<select className="filter-select" onChange={handleExpenseChange} value={expenseLevel}>
<option value="楽観">楽観:0.8倍</option>
<option value="中立">中立:1.0倍</option>
<option value="悲観">悲観:1.2倍</option>
</select>
</div>
<CustomExpenseForecast updateChartAndAction={updateChartAndAction} chartRef={chartRef} isCustomForecastOpen={isCustomForecastOpen} setIsCustomForecastOpen={setIsCustomForecastOpen} />
</div>
<div className="data-comparison">
<h2>データ比較</h2>
<button
className={`comparison-btn ${activeComparison === '時系列比較' ? 'active' : ''}`}
>
時系列比較
</button>
</div>
</div>
</div>
<div className="right-container">
<div className="actionbox-title">
<div>推奨アクション</div>
</div>
<div className="actionbox-message">
<div className="action-section">
<h3>残高推移</h3>
<p>{cashFlow}</p>
</div>
<div className="action-section">
<h3>対策</h3>
<ul style={{ paddingLeft: '0' }}>
{countermeasure.split('\n').map((item, index) => (
item.trim() === '-' ? (
<li key={index} style={{ listStyleType: 'none' }}>{item}</li>
) : (
<li key={index} style={{ listStyleType: index === 0 ? 'none' : 'disc', marginLeft: index === 0 ? '0' : '20px' }}>
{item}
</li>
)
))}
</ul>
</div>
</div>
<div className="collapsible-panels">
<CollapsiblePanel title="営業キャッシュフロー" money="32,990,433円" details={operatingCashFlow} />
<CollapsiblePanel title="投資キャッシュフロー" money="▲25,947,004円" details={investingCashFlow} />
<CollapsiblePanel title="財務キャッシュフロー" money="▲6,415,126円" details={financingCashFlow} />
</div>
</div>
</div>
</div>
);
};
const initialBalance = 10.0;
const dataSets: { [key: string]: DataSet } = {
"楽観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 0.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -16.5 * 0.8, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、6月の当月収支がマイナスになっています。支出管理が良好であったため、残高推移は比較的安定しています。",
countermeasure: "-"
}
},
"中立": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5, -15.0, -2.0, -10.5, -8.0, -16.5, -1.0, -10.5, -7.5],
action: {
cashFlow: "昨年の残高推移は非常に安定していました。今年の前半では、6月の当月収支がマイナスになっていますが、収入と支出のバランスが取れており、全体として安定した残高推移を維持しています。今後も同様のパターンが続くと予想されます。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n売掛金の管理を徹底し、現金回収の迅速化\n予測可能なキャッシュフローを維持するための計画的な支出管理\n短期および中長期の資金計画の策定"
}
},
"悲観": {
income: [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0],
expense: [-9.5, -14.0, -19.0, -3.5 * 1.2, -15.0 * 1.2, -2.0 * 1.2, -10.5 * 1.2, -8.0 * 1.2, -16.5 * 1.2, -1.0 * 1.2, -10.5 * 1.2, -7.5 * 1.2],
action: {
cashFlow: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
const operatingCashFlow = [
{ month: '4月', amount: '34,035,567円' },
{ month: '5月', amount: '30,407,343円' },
{ month: '6月', amount: '34,528,390円' },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const investingCashFlow = [
{ month: '4月', amount: '▲21,502,456円', alert: true },
{ month: '5月', amount: '▲34,023,289円', alert: true },
{ month: '6月', amount: '▲22,315,267円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
const financingCashFlow = [
{ month: '4月', amount: '▲11,504,456円', alert: true },
{ month: '5月', amount: '5,005,275円' },
{ month: '6月', amount: '▲12,746,198円', alert: true },
{ month: '7月', amount: '-' },
{ month: '8月', amount: '-' },
{ month: '9月', amount: '-' },
{ month: '10月', amount: '-' },
{ month: '11月', amount: '-' },
{ month: '12月', amount: '-' },
{ month: '1月', amount: '-' },
{ month: '2月', amount: '-' },
{ month: '3月', amount: '-' },
];
export default EmptyPage;
const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState);
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
updateChartAndAction(income, updatedExpenses, true);
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5 百万円</span> {/* 添加单位 */}
<span>5月</span>
<span>-14 百万円</span> {/* 添加单位 */}
</div>
<div className="row">
<span>6月</span>
<span>-19 百万円</span> {/* 添加单位 */}
<span>7月</span>
<input
type="text"
value={customExpenses['7月']}
onChange={(e) => handleExpenseChange('7月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
</div>
<div className="row">
<span>8月</span>
<input
type="text"
value={customExpenses['8月']}
onChange={(e) => handleExpenseChange('8月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
<span>9月</span>
<input
type="text"
value={customExpenses['9月']}
onChange={(e) => handleExpenseChange('9月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
</div>
<div className="row">
<span>10月</span>
<input
type="text"
value={customExpenses['10月']}
onChange={(e) => handleExpenseChange('10月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
<span>11月</span>
<input
type="text"
value={customExpenses['11月']}
onChange={(e) => handleExpenseChange('11月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
</div>
<div className="row">
<span>12月</span>
<input
type="text"
value={customExpenses['12月']}
onChange={(e) => handleExpenseChange('12月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
<span>1月</span>
<input
type="text"
value={customExpenses['1月']}
onChange={(e) => handleExpenseChange('1月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
</div>
<div className="row">
<span>2月</span>
<input
type="text"
value={customExpenses['2月']}
onChange={(e) => handleExpenseChange('2月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
<span>3月</span>
<input
type="text"
value={customExpenses['3月']}
onChange={(e) => handleExpenseChange('3月', e.target.value)}
/>
<span>百万円</span> {/* 添加单位 */}
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
ERROR in src/components/pages/financingPage.tsx:97:34
TS7031: Binding element 'updateChartAndAction' implicitly has an 'any' type.
95 | };
96 |
> 97 | const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
| ^^^^^^^^^^^^^^^^^^^^
98 | const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
99 | '7月': '',
100 | '8月': '',
ERROR in src/components/pages/financingPage.tsx:97:56
TS7031: Binding element 'chartRef' implicitly has an 'any' type.
95 | };
96 |
> 97 | const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
| ^^^^^^^^
98 | const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
99 | '7月': '',
100 | '8月': '',
ERROR in src/components/pages/financingPage.tsx:97:66
TS7031: Binding element 'isCustomForecastOpen' implicitly has an 'any' type.
95 | };
96 |
> 97 | const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
| ^^^^^^^^^^^^^^^^^^^^
98 | const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
99 | '7月': '',
100 | '8月': '',
ERROR in src/components/pages/financingPage.tsx:97:88
TS7031: Binding element 'setIsCustomForecastOpen' implicitly has an 'any' type.
95 | };
96 |
> 97 | const CustomExpenseForecast = ({ updateChartAndAction, chartRef, isCustomForecastOpen, setIsCustomForecastOpen }) => {
| ^^^^^^^^^^^^^^^^^^^^^^^
98 | const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
99 | '7月': '',
100 | '8月': '',
(
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<span>-9.5</span>
<span>百万円</span>
<span>5月</span>
<span>-14</span>
<span>百万円</span>
</div>
<div className="row">
<span>6月</span>
<span>-19</span>
<span>百万円</span>
<span>7月</span>
<input
type="text"
value={customExpenses['7月']}
onChange={(e) => handleExpenseChange('7月', e.target.value)}
/>
<span>百万円</span>
</div>
<div className="row">
<span>8月</span>
<input
type="text"
value={customExpenses['8月']}
onChange={(e) => handleExpenseChange('8月', e.target.value)}
/>
<span>百万円</span>
<span>9月</span>
<input
type="text"
value={customExpenses['9月']}
onChange={(e) => handleExpenseChange('9月', e.target.value)}
/>
<span>百万円</span>
</div>
<div className="row">
<span>10月</span>
<input
type="text"
value={customExpenses['10月']}
onChange={(e) => handleExpenseChange('10月', e.target.value)}
/>
<span>百万円</span>
<span>11月</span>
<input
type="text"
value={customExpenses['11月']}
onChange={(e) => handleExpenseChange('11月', e.target.value)}
/>
<span>百万円</span>
</div>
<div className="row">
<span>12月</span>
<input
type="text"
value={customExpenses['12月']}
onChange={(e) => handleExpenseChange('12月', e.target.value)}
/>
<span>百万円</span>
<span>1月</span>
<input
type="text"
value={customExpenses['1月']}
onChange={(e) => handleExpenseChange('1月', e.target.value)}
/>
<span>百万円</span>
</div>
<div className="row">
<span>2月</span>
<input
type="text"
value={customExpenses['2月']}
onChange={(e) => handleExpenseChange('2月', e.target.value)}
/>
<span>百万円</span>
<span>3月</span>
<input
type="text"
value={customExpenses['3月']}
onChange={(e) => handleExpenseChange('3月', e.target.value)}
/>
<span>百万円</span>
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
)
.form-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.row {
display: grid;
grid-template-columns: repeat(6, 1fr); /* 使用网格布局进行对齐,设置6列 */
align-items: center; /* 垂直居中对齐 */
gap: 10px;
}
.row span, .row input {
text-align: center; /* 文本居中 */
margin-right: 5px; /* 添加右边距 */
}
const CustomExpenseForecast = ({
updateChartAndAction,
chartRef,
isCustomForecastOpen,
setIsCustomForecastOpen,
}: {
updateChartAndAction: UpdateChartAndAction;
chartRef: ChartRef;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: SetIsCustomForecastOpen;
}) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState);
if (newState) {
setTimeout(() => {
const formContainer = document.querySelector('.form-container');
if (formContainer) {
formContainer.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
}
};
const handleExpenseChange = (month: keyof CustomExpenses, value: string) => {
setCustomExpenses(prevState => ({
...prevState,
[month]: value
}));
};
const handleUpdate = () => {
const income = [11.5, 13.5, 13.0, 7.0, 10.5, 6.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0];
const baseExpense = [-9.5, -14.0, -19.0];
const updatedExpenses = [
...baseExpense,
...Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : estimateExpense();
})
];
updateChartAndAction(income, updatedExpenses, true);
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
const estimateExpense = () => {
return -10; // 任意の推定値
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<div className="form-content">
<div className="row">
<span>4月</span>
<div className="number-unit">
<span>-9.5</span><span className="unit">百万円</span>
</div>
<span>5月</span>
<div className="number-unit">
<span>-14</span><span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span>6月</span>
<div className="number-unit">
<span>-19</span><span className="unit">百万円</span>
</div>
<span>7月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['7月']}
onChange={(e) => handleExpenseChange('7月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span>8月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['8月']}
onChange={(e) => handleExpenseChange('8月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
<span>9月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['9月']}
onChange={(e) => handleExpenseChange('9月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span>10月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['10月']}
onChange={(e) => handleExpenseChange('10月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
<span>11月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['11月']}
onChange={(e) => handleExpenseChange('11月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span>12月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['12月']}
onChange={(e) => handleExpenseChange('12月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
<span>1月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['1月']}
onChange={(e) => handleExpenseChange('1月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span>2月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['2月']}
onChange={(e) => handleExpenseChange('2月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
<span>3月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['3月']}
onChange={(e) => handleExpenseChange('3月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
.form-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.row {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 设置为4列布局 */
align-items: center; /* 垂直居中对齐 */
gap: 10px;
}
.row span {
text-align: right; /* 月份名称右对齐 */
white-space: nowrap; /* 保持文本不换行 */
}
.number-unit {
display: flex;
align-items: center; /* 数字和单位垂直居中 */
}
.number-unit span {
margin-right: 5px; /* 数字和单位之间的间距 */
}
.number-unit input {
max-width: 60px; /* 控制输入框宽度 */
text-align: right; /* 输入框内文字右对齐 */
margin-right: 5px; /* 输入框和单位之间的间距 */
}
.unit {
white-space: nowrap; /* 确保单位不会换行 */
text-align: left; /* 单位左对齐 */
}
支出の選択欄の内容を調整し、 選択後に表示する文言は楽観:0.8倍、中立:相当、悲観:1.2倍にしたい。プルダウンの内容は既存のまま、もしくは調整してほしい。