Open kirin-ri opened 2 months ago
ERROR in src/components/pages/financingPage.tsx:102:25
TS2304: Cannot find name 'UpdateChartAndAction'.
100 | setIsCustomForecastOpen,
101 | }: {
> 102 | updateChartAndAction: UpdateChartAndAction;
| ^^^^^^^^^^^^^^^^^^^^
103 | chartRef: ChartRef;
104 | isCustomForecastOpen: boolean;
105 | setIsCustomForecastOpen: SetIsCustomForecastOpen;
ERROR in src/components/pages/financingPage.tsx:103:13
TS2304: Cannot find name 'ChartRef'.
101 | }: {
102 | updateChartAndAction: UpdateChartAndAction;
> 103 | chartRef: ChartRef;
| ^^^^^^^^
104 | isCustomForecastOpen: boolean;
105 | setIsCustomForecastOpen: SetIsCustomForecastOpen;
106 | }) => {
ERROR in src/components/pages/financingPage.tsx:105:28
TS2304: Cannot find name 'SetIsCustomForecastOpen'.
103 | chartRef: ChartRef;
104 | isCustomForecastOpen: boolean;
> 105 | setIsCustomForecastOpen: SetIsCustomForecastOpen;
| ^^^^^^^^^^^^^^^^^^^^^^^
106 | }) => {
107 | const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
108 | '7月': '',
// 类型定义
type UpdateChartAndAction = (income: number[], expenses: number[], isCustom: boolean) => void;
type ChartRef = React.RefObject<ChartJS>;
type SetIsCustomForecastOpen = Dispatch<SetStateAction<boolean>>;``
import React, { Dispatch, SetStateAction, useRef } from 'react';
import { Chart as ChartJS } from 'chart.js';
// 在组件内直接定义类型
const CustomExpenseForecast = ({
updateChartAndAction,
chartRef,
isCustomForecastOpen,
setIsCustomForecastOpen,
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void; // 直接定义函数类型
chartRef: React.RefObject<ChartJS>; // 直接定义 Chart 的 Ref 类型
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<SetStateAction<boolean>>; // 直接定义状态更新函数类型
}) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': '',
'8月': '',
'9月': '',
'10月': '',
'11月': '',
'12月': '',
'1月': '',
'2月': '',
'3月': ''
});
.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; /* 数字和单位垂直居中 */
justify-content: flex-end; /* 数字和单位右对齐 */
}
.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; /* 单位左对齐 */
margin-left: 2px; /* 添加左边距,使单位与输入框紧邻 */
}
const CustomExpenseForecast = ({
updateChartAndAction,
chartRef,
isCustomForecastOpen,
setIsCustomForecastOpen,
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<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>
<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>
);
};
const CustomExpenseForecast = ({
updateChartAndAction,
chartRef,
isCustomForecastOpen,
setIsCustomForecastOpen,
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<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>
<div className="number-unit">
<input
type="text"
value="-9.5"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
<span>5月</span>
<div className="number-unit">
<input
type="text"
value="-14"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span>6月</span>
<div className="number-unit">
<input
type="text"
value="-19"
disabled
className="disabled-input"
/>
<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;
width: 100%; /* 确保内容宽度为100% */
}
.row {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 设置为4列均分布局 */
align-items: center; /* 垂直居中对齐 */
gap: 10px;
justify-items: start; /* 将每列的内容左对齐 */
}
.row span {
text-align: right; /* 月份名称右对齐 */
white-space: nowrap; /* 保持文本不换行 */
margin-right: 8px; /* 添加右边距,隔开月份和后面的数字或输入框 */
}
.number-unit {
display: flex;
align-items: center; /* 数字和单位垂直居中 */
}
.number-unit input {
max-width: 60px; /* 控制输入框宽度 */
text-align: right; /* 输入框内文字右对齐 */
margin-right: 5px; /* 输入框和单位之间的间距 */
}
.unit {
white-space: nowrap; /* 确保单位不会换行 */
text-align: left; /* 单位左对齐 */
margin-left: 2px; /* 添加左边距,使单位与输入框紧邻 */
}
.disabled-input {
background-color: #f9f9f9; /* 设置禁用输入框的背景色 */
color: #333; /* 禁用输入框的文本颜色 */
border: none; /* 移除禁用输入框的边框 */
pointer-events: none; /* 禁用输入框的鼠标事件 */
}
.form-content {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%; /* 确保内容宽度为100% */
}
.row {
display: grid;
grid-template-columns: auto auto auto auto; /* 使用auto布局以根据内容调整列宽 */
align-items: center; /* 垂直居中对齐 */
gap: 5px; /* 减少列之间的间距 */
justify-items: start; /* 将每列的内容左对齐 */
}
.row span {
text-align: right; /* 月份名称右对齐 */
white-space: nowrap; /* 保持文本不换行 */
margin-right: 4px; /* 减少右边距,减少月份和数字之间的空隙 */
}
.number-unit {
display: flex;
align-items: center; /* 数字和单位垂直居中 */
}
.number-unit input {
max-width: 60px; /* 控制输入框宽度 */
text-align: right; /* 输入框内文字右对齐 */
margin-right: 5px; /* 输入框和单位之间的间距 */
}
.unit {
white-space: nowrap; /* 确保单位不会换行 */
text-align: left; /* 单位左对齐 */
margin-left: 2px; /* 添加左边距,使单位与输入框紧邻 */
}
.disabled-input {
background-color: #f9f9f9; /* 设置禁用输入框的背景色 */
color: #333; /* 禁用输入框的文本颜色 */
border: none; /* 移除禁用输入框的边框 */
pointer-events: none; /* 禁用输入框的鼠标事件 */
}
.form-content {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%; /* 确保内容宽度为100% */
}
.row {
display: flex; /* 使用flex布局来确保对齐 */
align-items: center; /* 垂直居中对齐 */
gap: 15px; /* 控制列之间的间距 */
}
.row .month {
width: 40px; /* 固定月份列的宽度,确保对齐 */
text-align: right; /* 月份名称右对齐 */
white-space: nowrap; /* 保持文本不换行 */
margin-right: 5px; /* 控制月份和数字之间的间距 */
}
.number-unit {
display: flex;
align-items: center; /* 数字和单位垂直居中 */
}
.number-unit input {
max-width: 60px; /* 控制输入框宽度 */
text-align: right; /* 输入框内文字右对齐 */
margin-right: 5px; /* 输入框和单位之间的间距 */
}
.unit {
white-space: nowrap; /* 确保单位不会换行 */
text-align: left; /* 单位左对齐 */
margin-left: 2px; /* 添加左边距,使单位与输入框紧邻 */
}
.disabled-input {
background-color: #f9f9f9; /* 设置禁用输入框的背景色 */
color: #333; /* 禁用输入框的文本颜色 */
border: none; /* 移除禁用输入框的边框 */
pointer-events: none; /* 禁用输入框的鼠标事件 */
}
const CustomExpenseForecast = ({
updateChartAndAction,
chartRef,
isCustomForecastOpen,
setIsCustomForecastOpen,
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<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">
{/* 第一列:4月到9月 */}
<div className="column">
<div className="row">
<span className="month">4月</span>
<div className="number-unit">
<input
type="text"
value="-9.5"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">5月</span>
<div className="number-unit">
<input
type="text"
value="-14"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">6月</span>
<div className="number-unit">
<input
type="text"
value="-19"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">8月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['8月']}
onChange={(e) => handleExpenseChange('8月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
{/* 第二列:10月到3月 */}
<div className="column">
<div className="row">
<span className="month">10月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['10月']}
onChange={(e) => handleExpenseChange('10月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">12月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['12月']}
onChange={(e) => handleExpenseChange('12月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">2月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['2月']}
onChange={(e) => handleExpenseChange('2月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
</div>
<button className="update-btn" onClick={handleUpdate}>予測</button>
</div>
)}
</div>
);
};
.form-content {
display: flex;
gap: 20px; /* 增加列之间的间距 */
width: 100%; /* 确保内容宽度为100% */
}
.column {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1; /* 两列等宽 */
}
.row {
display: flex; /* 使用flex布局来确保对齐 */
align-items: center; /* 垂直居中对齐 */
}
.month {
width: 40px; /* 固定月份列的宽度,确保对齐 */
text-align: right; /* 月份名称右对齐 */
white-space: nowrap; /* 保持文本不换行 */
margin-right: 5px; /* 控制月份和数字之间的间距 */
}
.number-unit {
display: flex;
align-items: center; /* 数字和单位垂直居中 */
}
.number-unit input {
max-width: 60px; /* 控制输入框宽度 */
text-align: right; /* 输入框内文字右对齐 */
margin-right: 5px; /* 输入框和单位之间的间距 */
}
.unit {
white-space: nowrap; /* 确保单位不会换行 */
text-align: left; /* 单位左对齐 */
margin-left: 2px; /* 添加左边距,使单位与输入框紧邻 */
}
.disabled-input {
background-color: #f9f9f9; /* 设置禁用输入框的背景色 */
color: #333; /* 禁用输入框的文本颜色 */
border: none; /* 移除禁用输入框的边框 */
pointer-events: none; /* 禁用输入框的鼠标事件 */
}
.form-content {
display: flex;
gap: 20px; /* 增加列之间的间距 */
width: 100%; /* 确保内容宽度为100% */
justify-content: space-between; /* 确保两列内容均匀分布 */
}
.column {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1; /* 两列等宽 */
}
.row {
display: flex; /* 使用flex布局来确保对齐 */
align-items: center; /* 垂直居中对齐 */
justify-content: flex-start; /* 确保每行内容靠左对齐 */
}
.month {
width: 50px; /* 调整月份列的宽度,确保对齐 */
text-align: right; /* 月份名称右对齐 */
white-space: nowrap; /* 保持文本不换行 */
margin-right: 10px; /* 控制月份和数字之间的间距 */
}
.number-unit {
display: flex;
align-items: center; /* 数字和单位垂直居中 */
}
.number-unit input {
width: 60px; /* 固定输入框宽度 */
text-align: right; /* 输入框内文字右对齐 */
margin-right: 5px; /* 输入框和单位之间的间距 */
padding: 2px 4px; /* 添加内边距以改善视觉效果 */
}
.unit {
white-space: nowrap; /* 确保单位不会换行 */
text-align: left; /* 单位左对齐 */
margin-left: 2px; /* 添加左边距,使单位与输入框紧邻 */
}
.disabled-input {
background-color: #f9f9f9; /* 设置禁用输入框的背景色 */
color: #333; /* 禁用输入框的文本颜色 */
border: none; /* 移除禁用输入框的边框 */
pointer-events: none; /* 禁用输入框的鼠标事件 */
padding: 2px 4px; /* 添加内边距以改善视觉效果 */
}
.form-content {
display: flex;
gap: 20px; /* 增加列之间的间距 */
width: 100%; /* 确保内容宽度为100% */
justify-content: space-between; /* 确保两列内容均匀分布 */
}
.column {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1; /* 两列等宽 */
}
.row {
display: flex; /* 使用flex布局来确保对齐 */
align-items: center; /* 垂直居中对齐 */
justify-content: flex-start; /* 确保每行内容靠左对齐 */
}
.month {
width: 50px; /* 调整月份列的宽度,确保对齐 */
text-align: right; /* 月份名称右对齐 */
white-space: nowrap; /* 保持文本不换行 */
margin-right: 10px; /* 控制月份和数字之间的间距 */
}
.number-unit {
display: flex;
align-items: center; /* 数字和单位垂直居中 */
}
.number-unit input,
.number-unit .disabled-input {
width: 60px; /* 固定输入框宽度 */
text-align: right; /* 输入框内文字右对齐 */
margin-right: 5px; /* 输入框和单位之间的间距 */
padding: 4px; /* 添加内边距以改善视觉效果 */
box-sizing: border-box; /* 确保padding和border计算在内 */
height: 30px; /* 设置统一的高度 */
border: 1px solid #ccc; /* 为禁用输入框添加边框 */
background-color: #fff; /* 确保禁用输入框的背景色与可编辑的相同 */
color: #333; /* 设置禁用输入框的文本颜色 */
}
.number-unit .disabled-input {
pointer-events: none; /* 禁用输入框的鼠标事件 */
background-color: #f9f9f9; /* 设置禁用输入框的背景色 */
border: 1px solid #ccc; /* 确保禁用输入框与可编辑输入框的边框一致 */
}
.unit {
white-space: nowrap; /* 确保单位不会换行 */
text-align: left; /* 单位左对齐 */
margin-left: 2px; /* 添加左边距,使单位与输入框紧邻 */
}
.form-content {
display: flex;
gap: 20px; /* 增加列之间的间距 */
width: 100%; /* 确保内容宽度为100% */
justify-content: space-between; /* 确保两列内容均匀分布 */
}
.column {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1; /* 两列等宽 */
}
.row {
display: flex; /* 使用flex布局来确保对齐 */
align-items: center; /* 垂直居中对齐 */
justify-content: flex-start; /* 确保每行内容靠左对齐 */
}
.month {
width: 50px; /* 调整月份列的宽度,确保对齐 */
text-align: right; /* 月份名称右对齐 */
white-space: nowrap; /* 保持文本不换行 */
margin-right: 15px; /* 增加月份和数字之间的间距 */
}
.number-unit {
display: flex;
align-items: center; /* 数字和单位垂直居中 */
}
.number-unit input,
.number-unit .disabled-input {
width: 60px; /* 固定输入框宽度 */
text-align: right; /* 输入框内文字右对齐 */
margin-right: 5px; /* 输入框和单位之间的间距 */
padding: 4px; /* 添加内边距以改善视觉效果 */
box-sizing: border-box; /* 确保padding和border计算在内 */
height: 30px; /* 设置统一的高度 */
border: 1px solid #ccc; /* 为禁用输入框添加边框 */
background-color: #fff; /* 确保禁用输入框的背景色与可编辑的相同 */
color: #333; /* 设置禁用输入框的文本颜色 */
}
.number-unit .disabled-input {
pointer-events: none; /* 禁用输入框的鼠标事件 */
background-color: #f9f9f9; /* 设置禁用输入框的背景色 */
border: 1px solid #ccc; /* 确保禁用输入框与可编辑输入框的边框一致 */
}
.unit {
white-space: nowrap; /* 确保单位不会换行 */
text-align: left; /* 单位左对齐 */
margin-left: 2px; /* 添加左边距,使单位与输入框紧邻 */
}
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>
);
};
import React, { Dispatch, SetStateAction } from 'react';
const CustomExpenseForecast = ({
updateChartAndAction,
chartRef,
isCustomForecastOpen,
setIsCustomForecastOpen,
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<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">
{/* 第一列:4月到9月 */}
<div className="column">
<div className="row">
<span className="month">4月</span>
<div className="number-unit">
<input
type="text"
value="-9.5"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">5月</span>
<div className="number-unit">
<input
type="text"
value="-14"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">6月</span>
<div className="number-unit">
<input
type="text"
value="-19"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">8月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['8月']}
onChange={(e) => handleExpenseChange('8月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
{/* 第二列:10月到3月 */}
<div className="column">
<div className="row">
<span className="month">10月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['10月']}
onChange={(e) => handleExpenseChange('10月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">12月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['12月']}
onChange={(e) => handleExpenseChange('12月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">2月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['2月']}
onChange={(e) => handleExpenseChange('2月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
</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 {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { useEffect, useRef, useState, Dispatch, SetStateAction } 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,
selectedLevel,
expenses,
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<SetStateAction<boolean>>;
selectedLevel: string; // 当前选择的选项
expenses: number[]; // 当前选项下的支出数据
}) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': expenses[3]?.toString() || '',
'8月': expenses[4]?.toString() || '',
'9月': expenses[5]?.toString() || '',
'10月': expenses[6]?.toString() || '',
'11月': expenses[7]?.toString() || '',
'12月': expenses[8]?.toString() || '',
'1月': expenses[9]?.toString() || '',
'2月': expenses[10]?.toString() || '',
'3月': expenses[11]?.toString() || '',
});
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 updatedExpenses = Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : 0;
});
updateChartAndAction(income, updatedExpenses, true);
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
// 监听选项的变化以更新自定义输入框的值
useEffect(() => {
setCustomExpenses({
'7月': expenses[3]?.toString() || '',
'8月': expenses[4]?.toString() || '',
'9月': expenses[5]?.toString() || '',
'10月': expenses[6]?.toString() || '',
'11月': expenses[7]?.toString() || '',
'12月': expenses[8]?.toString() || '',
'1月': expenses[9]?.toString() || '',
'2月': expenses[10]?.toString() || '',
'3月': expenses[11]?.toString() || '',
});
}, [selectedLevel, expenses]);
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>支出のカスタム予測</button>
{isCustomForecastOpen && (
<div className="form-container">
<div className="form-content">
{/* 第一列:4月到9月 */}
<div className="column">
<div className="row">
<span className="month">4月</span>
<div className="number-unit">
<input
type="text"
value="-9.5"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">5月</span>
<div className="number-unit">
<input
type="text"
value="-14"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">6月</span>
<div className="number-unit">
<input
type="text"
value="-19"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">8月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['8月']}
onChange={(e) => handleExpenseChange('8月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
{/* 第二列:10月到3月 */}
<div className="column">
<div className="row">
<span className="month">10月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['10月']}
onChange={(e) => handleExpenseChange('10月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">12月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['12月']}
onChange={(e) => handleExpenseChange('12月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">2月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['2月']}
onChange={(e) => handleExpenseChange('2月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
</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 [expenses, setExpenses] = useState<number[]>(dataSets['中立'].expense);
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]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
// 更新支出数据
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
setExpenses(selectedData.expense); // 更新自定义预测输入框的数据
updateChartAndAction(selectedData.income, selectedData.expense, 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}
selectedLevel={expenseLevel}
expenses={expenses}
/>
</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>
);
};
useEffect(() => {
setCustomExpenses({
'7月': (Math.round(expenses[3] * 10) / 10).toString() || '',
'8月': (Math.round(expenses[4] * 10) / 10).toString() || '',
'9月': (Math.round(expenses[5] * 10) / 10).toString() || '',
'10月': (Math.round(expenses[6] * 10) / 10).toString() || '',
'11月': (Math.round(expenses[7] * 10) / 10).toString() || '',
'12月': (Math.round(expenses[8] * 10) / 10).toString() || '',
'1月': (Math.round(expenses[9] * 10) / 10).toString() || '',
'2月': (Math.round(expenses[10] * 10) / 10).toString() || '',
'3月': (Math.round(expenses[11] * 10) / 10).toString() || '',
});
}, [selectedLevel, expenses]);
'7月': expenses[3] !== undefined ? expenses[3].toFixed(1) : '',
'8月': expenses[4] !== undefined ? expenses[4].toFixed(1) : '',
'9月': expenses[5] !== undefined ? expenses[5].toFixed(1) : '',
'10月': expenses[6] !== undefined ? expenses[6].toFixed(1) : '',
'11月': expenses[7] !== undefined ? expenses[7].toFixed(1) : '',
'12月': expenses[8] !== undefined ? expenses[8].toFixed(1) : '',
'1月': expenses[9] !== undefined ? expenses[9].toFixed(1) : '',
'2月': expenses[10] !== undefined ? expenses[10].toFixed(1) : '',
'3月': expenses[11] !== undefined ? expenses[11].toFixed(1) : '',
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 fixedExpenses = [-9.5, -14.0, -19.0];
// 从自定义输入中获取的支出数据
const updatedExpenses = Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : 0;
});
// 合并固定支出数据和自定义支出数据
const completeExpenses = [...fixedExpenses, ...updatedExpenses];
// 更新图表和推荐行动
updateChartAndAction(income, completeExpenses, 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 { Dispatch, SetStateAction, 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,
selectedLevel,
expenses,
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<SetStateAction<boolean>>;
selectedLevel: string;
expenses: number[];
}) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': expenses[3]?.toFixed(1) || '',
'8月': expenses[4]?.toFixed(1) || '',
'9月': expenses[5]?.toFixed(1) || '',
'10月': expenses[6]?.toFixed(1) || '',
'11月': expenses[7]?.toFixed(1) || '',
'12月': expenses[8]?.toFixed(1) || '',
'1月': expenses[9]?.toFixed(1)|| '',
'2月': expenses[10]?.toFixed(1) || '',
'3月': expenses[11]?.toFixed(1) || '',
});
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 fixedExpenses = [-9.5, -14.0, -19.0];
// 从自定义输入中获取的支出数据
const updatedExpenses = Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : 0;
});
// 合并固定支出数据和自定义支出数据
const completeExpenses = [...fixedExpenses, ...updatedExpenses];
// 更新图表和推荐行动
updateChartAndAction(income, completeExpenses, true);
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
useEffect(() => {
setCustomExpenses({
'7月': expenses[3]?.toString() || '',
'8月': expenses[4]?.toString() || '',
'9月': expenses[5]?.toString() || '',
'10月': expenses[6]?.toString() || '',
'11月': expenses[7]?.toString() || '',
'12月': expenses[8]?.toString() || '',
'1月': expenses[9]?.toString() || '',
'2月': expenses[10]?.toString() || '',
'3月': expenses[11]?.toString() || '',
});
}, [selectedLevel, expenses]);
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="column">
<div className="row">
<span className="month">4月</span>
<div className="number-unit">
<input
type="text"
value="-9.5"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">5月</span>
<div className="number-unit">
<input
type="text"
value="-14"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">6月</span>
<div className="number-unit">
<input
type="text"
value="-19"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">8月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['8月']}
onChange={(e) => handleExpenseChange('8月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
<div className="column">
<div className="row">
<span className="month">10月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['10月']}
onChange={(e) => handleExpenseChange('10月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">12月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['12月']}
onChange={(e) => handleExpenseChange('12月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">2月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['2月']}
onChange={(e) => handleExpenseChange('2月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
</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 [expenses, setExpenses] = useState<number[]>(dataSets['中立'].expense);
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]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
setExpenses(selectedData.expense);
updateChartAndAction(selectedData.income, selectedData.expense, 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}
selectedLevel={expenseLevel}
expenses={expenses}
/>
</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, -2.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -13.2, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、5月と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: "昨年の残高推移は非常に安定していました。今年の前半では、5月と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: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、5月と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 InfoButton = ({ onClick }: { onClick: () => void }) => {
return (
<button className="info-button" onClick={onClick}>
<i className="fa fa-info-circle" aria-hidden="true"></i>
</button>
);
};
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const handleMouseDown = (e: React.MouseEvent) => {
const panel = panelRef.current;
if (panel) {
const shiftX = e.clientX - panel.getBoundingClientRect().left;
const shiftY = e.clientY - panel.getBoundingClientRect().top;
const moveAt = (pageX: number, pageY: number) => {
panel.style.left = pageX - shiftX + 'px';
panel.style.top = pageY - shiftY + 'px';
};
const onMouseMove = (event: MouseEvent) => {
moveAt(event.pageX, event.pageY);
};
document.addEventListener('mousemove', onMouseMove);
document.onmouseup = function () {
document.removeEventListener('mousemove', onMouseMove);
panel.onmouseup = null;
};
}
};
return (
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<button className="close-info-panel" onClick={onClose}>×</button>
<div className="info-content">
{/* 社外資料や景気指標に関する内容をここに追加 */}
<p>ここに景気指標の情報が表示されます。</p>
</div>
</div>
);
};
const [isInfoPanelOpen, setIsInfoPanelOpen] = useState(false);
<div className="content-wrapper metrics-details">
{/* 省略其他内容 */}
{isInfoPanelOpen && <InfoPanel onClose={() => setIsInfoPanelOpen(false)} />}
{!isInfoPanelOpen && <InfoButton onClick={() => setIsInfoPanelOpen(true)} />}
</div>
.info-button {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.info-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
height: 200px;
background-color: white;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
overflow: auto;
padding: 10px;
z-index: 1001;
cursor: move;
}
.close-info-panel {
position: absolute;
top: 5px;
right: 5px;
background: none;
border: none;
font-size: 16px;
cursor: pointer;
}
.info-content {
padding: 10px;
font-size: 14px;
color: #333;
}
.info-button {
position: fixed;
bottom: 20px;
right: 20px;
background-color: rgba(0, 123, 255, 0.7); /* 半透明背景 */
color: white;
border: none;
border-radius: 50%;
width: 40px; /* 更小的尺寸 */
height: 40px;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
transition: background-color 0.3s;
}
.info-button:hover {
background-color: rgba(0, 123, 255, 0.9);
}
.info-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
height: 250px;
background-color: white;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
overflow: auto;
z-index: 1001;
cursor: move;
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f5f5f5;
padding: 5px 10px;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.close-info-panel {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
}
.info-content {
padding: 10px;
font-size: 14px;
color: #333;
}
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<div className="info-panel-header">
<span>社外資料と景気指標の参考</span>
<button className="close-info-panel" onClick={onClose}>×</button>
</div>
<div className="info-content">
{/* 示例文章内容 */}
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
<p>現時点では、国際貿易摩擦やデジタル経済への移行などの課題に直面しています。これらの課題に対応するため、日本政府および民間企業は、持続可能な成長を目指して戦略を再評価し、多くの取り組みを進めています。</p>
<p>今後の日本経済の発展においては、労働力の流動性向上、AIおよびロボット技術の活用、さらにはエネルギー政策の見直しなど、さまざまな分野での改革が期待されています。これらの変化が企業の戦略にどのような影響を与えるのか、注視する必要があります。</p>
<p>さらに、最近の金融政策とその影響についても考慮する必要があります。日本銀行は長期にわたる超低金利政策を実施しており、この政策が企業投資と消費者行動に与える影響について、さらに分析が必要です。</p>
</div>
</div>
.info-content::-webkit-scrollbar {
width: 5px; /* 更细的滚动条宽度 */
}
.info-content::-webkit-scrollbar-thumb {
background-color: #cccccc; /* 滚动条的颜色 */
border-radius: 10px; /* 圆角 */
}
.info-content::-webkit-scrollbar-track {
background: transparent; /* 滚动条轨道背景透明 */
}
.info-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
height: 250px;
background-color: white;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 1001;
cursor: move;
overflow: hidden; /* 隐藏面板的整体滚动条 */
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #007bff; /* 替换为您页面一致的颜色 */
padding: 5px 10px;
border-bottom: 1px solid #ddd;
font-weight: bold;
color: white; /* 确保文字在新背景下可读 */
cursor: default; /* 不让用户拖动标题部分 */
}
.close-info-panel {
background: none;
border: none;
font-size: 16px;
color: white; /* 保持关闭按钮在新标题背景色上可见 */
cursor: pointer;
}
.info-content {
height: calc(100% - 40px); /* 内容区域高度减去标题栏的高度 */
overflow-y: auto; /* 只在内容区域滚动 */
padding: 10px;
font-size: 14px;
color: #333;
box-sizing: border-box; /* 确保padding不会影响高度 */
}
/* 自定义滚动条样式 */
.info-content::-webkit-scrollbar {
width: 5px; /* 更细的滚动条宽度 */
}
.info-content::-webkit-scrollbar-thumb {
background-color: #cccccc; /* 滚动条的颜色 */
border-radius: 10px; /* 圆角 */
}
.info-content::-webkit-scrollbar-track {
background: transparent; /* 滚动条轨道背景透明 */
}
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [position, setPosition] = useState({ top: '20px', left: '20px' });
const [size, setSize] = useState({ width: 390, height: 325 });
const handleMouseDown = (e: React.MouseEvent) => {
if (e.target !== panelRef.current) return;
setIsDragging(true);
const shiftX = e.clientX - panelRef.current!.getBoundingClientRect().left;
const shiftY = e.clientY - panelRef.current!.getBoundingClientRect().top;
const onMouseMove = (event: MouseEvent) => {
if (!isDragging) return;
const newLeft = event.clientX - shiftX;
const newTop = event.clientY - shiftY;
const boundedLeft = Math.max(0, Math.min(window.innerWidth - size.width, newLeft));
const boundedTop = Math.max(0, Math.min(window.innerHeight - size.height, newTop));
setPosition({ top: `${boundedTop}px`, left: `${boundedLeft}px` });
};
const onMouseUp = () => {
setIsDragging(false);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const handleResizeMouseDown = (e: React.MouseEvent) => {
e.stopPropagation();
setIsResizing(true);
const startX = e.clientX;
const startY = e.clientY;
const startWidth = size.width;
const startHeight = size.height;
const onMouseMove = (event: MouseEvent) => {
if (!isResizing) return;
const newWidth = startWidth + (event.clientX - startX);
const newHeight = startHeight + (event.clientY - startY);
setSize({
width: Math.max(300, Math.min(window.innerWidth - position.left, newWidth)), // 限制最小宽度为300
height: Math.max(200, Math.min(window.innerHeight - position.top, newHeight)), // 限制最小高度为200
});
};
const onMouseUp = () => {
setIsResizing(false);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
return (
<div
className="info-panel"
ref={panelRef}
onMouseDown={handleMouseDown}
style={{ top: position.top, left: position.left, width: `${size.width}px`, height: `${size.height}px` }}
>
<div className="info-panel-header">
<span>社外資料と景気指標の参考</span>
<button className="close-info-panel" onClick={onClose}>×</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
{/* 其他内容省略 */}
</div>
{/* 调整大小手柄 */}
<div className="resize-handle" onMouseDown={handleResizeMouseDown}></div>
</div>
);
};
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [position, setPosition] = useState({ top: '20px', left: '20px' });
const [size, setSize] = useState({ width: 390, height: 325 });
const handleMouseDown = (e: React.MouseEvent) => {
if (e.target !== panelRef.current) return;
setIsDragging(true);
const shiftX = e.clientX - panelRef.current!.getBoundingClientRect().left;
const shiftY = e.clientY - panelRef.current!.getBoundingClientRect().top;
const onMouseMove = (event: MouseEvent) => {
if (!isDragging) return;
const newLeft = event.clientX - shiftX;
const newTop = event.clientY - shiftY;
const boundedLeft = Math.max(0, Math.min(window.innerWidth - size.width, newLeft));
const boundedTop = Math.max(0, Math.min(window.innerHeight - size.height, newTop));
setPosition({ top: `${boundedTop}px`, left: `${boundedLeft}px` });
};
const onMouseUp = () => {
setIsDragging(false);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const handleResizeMouseDown = (e: React.MouseEvent) => {
e.stopPropagation();
setIsResizing(true);
const startX = e.clientX;
const startY = e.clientY;
const startWidth = size.width;
const startHeight = size.height;
const onMouseMove = (event: MouseEvent) => {
if (!isResizing) return;
const newWidth = startWidth + (event.clientX - startX);
const newHeight = startHeight + (event.clientY - startY);
setSize({
width: Math.max(300, Math.min(window.innerWidth - position.left, newWidth)), // 限制最小宽度为300
height: Math.max(200, Math.min(window.innerHeight - position.top, newHeight)), // 限制最小高度为200
});
};
const onMouseUp = () => {
setIsResizing(false);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
return (
<div
className="info-panel"
ref={panelRef}
onMouseDown={handleMouseDown}
style={{ top: position.top, left: position.left, width: `${size.width}px`, height: `${size.height}px` }}
>
<div className="info-panel-header">
<span>社外資料と景気指標の参考</span>
<button className="close-info-panel" onClick={onClose}>×</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
{/* 其他内容省略 */}
</div>
{/* 调整大小手柄 */}
<div className="resize-handle" onMouseDown={handleResizeMouseDown}></div>
</div>
);
};
Compiled successfully!
You can now view catalog-webapp in the browser.
Local: http://localhost:3000
On Your Network: http://10.128.1.4:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
webpack compiled successfully
ERROR in src/components/pages/financingPage.tsx:397:59
TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
395 |
396 | setSize({
> 397 | width: Math.max(300, Math.min(window.innerWidth - position.left, newWidth)), // 限制最小宽度为300
| ^^^^^^^^^^^^^
398 | height: Math.max(200, Math.min(window.innerHeight - position.top, newHeight)), // 限制最小高度为200
399 | });
400 | };
ERROR in src/components/pages/financingPage.tsx:398:61
TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
396 | setSize({
397 | width: Math.max(300, Math.min(window.innerWidth - position.left, newWidth)), // 限制最小宽度为300
> 398 | height: Math.max(200, Math.min(window.innerHeight - position.top, newHeight)), // 限制最小高度为200
| ^^^^^^^^^^^^
399 | });
400 | };
401 |
const handleResizeMouseDown = (e: React.MouseEvent) => {
e.stopPropagation();
setIsResizing(true);
const startX = e.clientX;
const startY = e.clientY;
const startWidth = size.width;
const startHeight = size.height;
const onMouseMove = (event: MouseEvent) => {
if (!isResizing) return;
const newWidth = startWidth + (event.clientX - startX);
const newHeight = startHeight + (event.clientY - startY);
// 使用 parseFloat 将 position.left 和 position.top 转换为数字
const boundedLeft = parseFloat(position.left);
const boundedTop = parseFloat(position.top);
setSize({
width: Math.max(300, Math.min(window.innerWidth - boundedLeft, newWidth)), // 限制最小宽度为300
height: Math.max(200, Math.min(window.innerHeight - boundedTop, newHeight)), // 限制最小高度为200
});
};
const onMouseUp = () => {
setIsResizing(false);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
.info-panel {
position: fixed;
background-color: white;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 1001;
cursor: move;
overflow: hidden;
border-radius: 10px;
resize: none; /* 禁止使用原生的调整大小功能 */
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #007bff; /* 替换为您页面一致的颜色 */
padding: 5px 10px;
border-bottom: 1px solid #ddd;
font-weight: bold;
color: white;
cursor: default;
}
.close-info-panel {
background: none;
border: none;
font-size: 16px;
color: white;
cursor: pointer;
}
.info-content {
height: calc(100% - 40px);
overflow-y: auto;
padding: 10px;
font-size: 14px;
color: #333;
box-sizing: border-box;
}
.resize-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
cursor: nwse-resize; /* 显示调整大小的光标 */
background-color: transparent; /* 可见性 */
}
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
ChartTypeRegistry,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js';
import { Dispatch, SetStateAction, 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,
selectedLevel,
expenses,
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<SetStateAction<boolean>>;
selectedLevel: string;
expenses: number[];
}) => {
const [customExpenses, setCustomExpenses] = useState<CustomExpenses>({
'7月': expenses[3]?.toFixed(1) || '',
'8月': expenses[4]?.toFixed(1) || '',
'9月': expenses[5]?.toFixed(1) || '',
'10月': expenses[6]?.toFixed(1) || '',
'11月': expenses[7]?.toFixed(1) || '',
'12月': expenses[8]?.toFixed(1) || '',
'1月': expenses[9]?.toFixed(1)|| '',
'2月': expenses[10]?.toFixed(1) || '',
'3月': expenses[11]?.toFixed(1) || '',
});
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 fixedExpenses = [-9.5, -14.0, -19.0];
// 从自定义输入中获取的支出数据
const updatedExpenses = Object.keys(customExpenses).map(month => {
const value = customExpenses[month as keyof CustomExpenses];
return value !== '' ? parseFloat(value) : 0;
});
// 合并固定支出数据和自定义支出数据
const completeExpenses = [...fixedExpenses, ...updatedExpenses];
// 更新图表和推荐行动
updateChartAndAction(income, completeExpenses, true);
if (chartRef.current && chartRef.current.canvas) {
const parentNode = chartRef.current.canvas.parentNode as Element;
parentNode.scrollIntoView({ behavior: 'smooth' });
}
};
useEffect(() => {
setCustomExpenses({
'7月': expenses[3]?.toString() || '',
'8月': expenses[4]?.toString() || '',
'9月': expenses[5]?.toString() || '',
'10月': expenses[6]?.toString() || '',
'11月': expenses[7]?.toString() || '',
'12月': expenses[8]?.toString() || '',
'1月': expenses[9]?.toString() || '',
'2月': expenses[10]?.toString() || '',
'3月': expenses[11]?.toString() || '',
});
}, [selectedLevel, expenses]);
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="column">
<div className="row">
<span className="month">4月</span>
<div className="number-unit">
<input
type="text"
value="-9.5"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">5月</span>
<div className="number-unit">
<input
type="text"
value="-14"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">6月</span>
<div className="number-unit">
<input
type="text"
value="-19"
disabled
className="disabled-input"
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">8月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['8月']}
onChange={(e) => handleExpenseChange('8月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
<div className="column">
<div className="row">
<span className="month">10月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['10月']}
onChange={(e) => handleExpenseChange('10月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">12月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['12月']}
onChange={(e) => handleExpenseChange('12月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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 className="month">2月</span>
<div className="number-unit">
<input
type="text"
value={customExpenses['2月']}
onChange={(e) => handleExpenseChange('2月', e.target.value)}
/>
<span className="unit">百万円</span>
</div>
</div>
<div className="row">
<span className="month">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>
</div>
<button className="update-btn" onClick={handleUpdate}>設定</button>
</div>
)}
</div>
);
};
const InfoButton = ({ onClick }: { onClick: () => void }) => {
return (
<button className="info-button" onClick={onClick} title='teset'>
<i className="fa fa-info-circle" aria-hidden="true"></i>
</button>
);
};
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const handleMouseDown = (e: React.MouseEvent) => {
const panel = panelRef.current;
if (panel) {
const shiftX = e.clientX - panel.getBoundingClientRect().left;
const shiftY = e.clientY - panel.getBoundingClientRect().top;
const moveAt = (pageX: number, pageY: number) => {
panel.style.left = pageX - shiftX + 'px';
panel.style.top = pageY - shiftY + 'px';
};
const onMouseMove = (event: MouseEvent) => {
moveAt(event.pageX, event.pageY);
};
document.addEventListener('mousemove', onMouseMove);
document.onmouseup = function () {
document.removeEventListener('mousemove', onMouseMove);
panel.onmouseup = null;
};
}
};
return (
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<div className="info-panel-header">
<span>参考</span>
<button className="close-info-panel" onClick={onClose}>×</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
<p>現時点では、国際貿易摩擦やデジタル経済への移行などの課題に直面しています。これらの課題に対応するため、日本政府および民間企業は、持続可能な成長を目指して戦略を再評価し、多くの取り組みを進めています。</p>
<p>今後の日本経済の発展においては、労働力の流動性向上、AIおよびロボット技術の活用、さらにはエネルギー政策の見直しなど、さまざまな分野での改革が期待されています。これらの変化が企業の戦略にどのような影響を与えるのか、注視する必要があります。</p>
<p>さらに、最近の金融政策とその影響についても考慮する必要があります。日本銀行は長期にわたる超低金利政策を実施しており、この政策が企業投資と消費者行動に与える影響について、さらに分析が必要です。</p>
</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 [expenses, setExpenses] = useState<number[]>(dataSets['中立'].expense);
const [isInfoPanelOpen, setIsInfoPanelOpen] = useState(false);
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]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
setExpenses(selectedData.expense);
updateChartAndAction(selectedData.income, selectedData.expense, 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}
selectedLevel={expenseLevel}
expenses={expenses}
/>
</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>
{isInfoPanelOpen && <InfoPanel onClose={() => setIsInfoPanelOpen(false)} />}
{!isInfoPanelOpen && <InfoButton onClick={() => setIsInfoPanelOpen(true)} />}
</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, -2.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -13.2, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、5月と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: "昨年の残高推移は非常に安定していました。今年の前半では、5月と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: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、5月と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 {
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);
width: 200%;
transform: scale(1.0);
}
.update-btn {
width: 25%;
padding: 2px 1px;
background-color: #28a745;
border-radius: 10px;
color: white;
border: none;
cursor: pointer;
margin: 10px auto 0 auto;
display: block;
font-size: 16px;
}
.update-btn:hover {
background-color: #218838;
}
.form-content {
display: flex;
gap: 20px;
width: 100%;
justify-content: space-between;
}
.column {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
}
.month {
flex: 1;
width: 50px;
display: flex;
text-align: center;
white-space: nowrap;
margin-left: 10%;
}
.number-unit {
flex: 2;
display: flex;
align-items: center;
}
.number-unit input,
.number-unit .disabled-input {
width: 60px;
text-align: right;
margin-right: 5px;
padding: 4px;
box-sizing: border-box;
height: 30px;
border: 1px solid #ccc;
background-color: #fff;
color: #333;
}
.number-unit .disabled-input {
pointer-events: none;
background-color: #f9f9f9;
border: 1px solid #ccc;
}
.unit {
white-space: nowrap;
text-align: left;
margin-left: 2px;
}
.info-button {
position: fixed;
bottom: 20px;
right: 10px;
background-color: rgba(0, 123, 255, 0.7); /* 半透明背景 */
color: white;
border: none;
border-radius: 50%;
width: 30px; /* 更小的尺寸 */
height: 30px;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
transition: background-color 0.3s;
}
.info-button:hover {
background-color: rgba(0, 123, 255, 0.9);
}
.info-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 350px;
height: 350px;
background-color: white;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 1001;
cursor: move;
overflow: hidden;
border-radius: 5px;
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
color: white;
background-color: var(--sidebar-theme-color);
padding: 5px 10px;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.close-info-panel {
background: none;
border: none;
font-size: 16px;
color: white; /* 保持关闭按钮在新标题背景色上可见 */
cursor: pointer;
}
.info-content {
height: calc(100% - 40px); /* 内容区域高度减去标题栏的高度 */
overflow-y: auto; /* 只在内容区域滚动 */
padding: 10px;
font-size: 14px;
color: #333;
box-sizing: border-box; /* 确保padding不会影响高度 */
cursor: default; /* 不让用户拖动标题部分 */
}
/* 自定义滚动条样式 */
.info-content::-webkit-scrollbar {
width: 5px; /* 更细的滚动条宽度 */
}
.info-content::-webkit-scrollbar-thumb {
background-color: #cccccc; /* 滚动条的颜色 */
border-radius: 10px; /* 圆角 */
}
.info-content::-webkit-scrollbar-track {
background: transparent; /* 滚动条轨道背景透明 */
}
const onMouseMove = (event: MouseEvent) => {
if (event.clientX < 0 || event.clientY < 0 || event.clientX > window.innerWidth || event.clientY > window.innerHeight) {
// 鼠标移出窗口边界时,停止拖动
document.removeEventListener('mousemove', onMouseMove);
panel.onmouseup = null;
return;
}
moveAt(event.pageX, event.pageY);
};
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const panel = panelRef.current;
if (panel) {
// 在mousedown事件发生时,重新计算偏移量
const shiftX = e.clientX - panel.getBoundingClientRect().left;
const shiftY = e.clientY - panel.getBoundingClientRect().top;
const moveAt = (pageX: number, pageY: number) => {
panel.style.left = pageX - shiftX + 'px';
panel.style.top = pageY - shiftY + 'px';
};
const onMouseMove = (event: MouseEvent) => {
moveAt(event.pageX, event.pageY);
};
document.addEventListener('mousemove', onMouseMove);
panel.onmouseup = function () {
document.removeEventListener('mousemove', onMouseMove);
panel.onmouseup = null;
};
}
};
// 当折叠部分展开时,重新计算小窗位置
const handleCollapseExpand = () => {
const panel = panelRef.current;
if (panel) {
const rect = panel.getBoundingClientRect();
panel.style.left = `${rect.left}px`;
panel.style.top = `${rect.top}px`;
}
};
// 示例的折叠部分展开事件
const toggleCollapse = () => {
setIsCollapsed(!isCollapsed);
handleCollapseExpand(); // 重新计算小窗位置
};
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const panel = panelRef.current;
if (panel) {
// 在mousedown事件发生时,重新计算偏移量
const shiftX = e.clientX - panel.getBoundingClientRect().left;
const shiftY = e.clientY - panel.getBoundingClientRect().top;
const moveAt = (pageX: number, pageY: number) => {
panel.style.left = pageX - shiftX + 'px';
panel.style.top = pageY - shiftY + 'px';
};
const onMouseMove = (event: MouseEvent) => {
moveAt(event.pageX, event.pageY);
};
document.addEventListener('mousemove', onMouseMove);
panel.onmouseup = function () {
document.removeEventListener('mousemove', onMouseMove);
panel.onmouseup = null;
};
}
};
// 当折叠部分展开时,重新计算小窗位置
const handleCollapseExpand = () => {
const panel = panelRef.current;
if (panel) {
const rect = panel.getBoundingClientRect();
panel.style.left = `${rect.left}px`;
panel.style.top = `${rect.top}px`;
}
};
// 示例的折叠部分展开事件
const toggleCollapse = () => {
setIsCollapsed(!isCollapsed);
handleCollapseExpand(); // 重新计算小窗位置
};
return (
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<div className="info-panel-header">
<span>関連情報</span>
<button className="close-info-panel" onClick={onClose}>ー</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
<p>現時点では、国際貿易摩擦やデジタル経済への移行などの課題に直面しています。これらの課題に対応するため、日本政府および民間企業は、持続可能な成長を目指して戦略を再評価し、多くの取り組みを進めています。</p>
<p>今後の日本経済の発展においては、労働力の流動性向上、AIおよびロボット技術の活用、さらにはエネルギー政策の見直しなど、さまざまな分野での改革が期待されています。これらの変化が企業の戦略にどのような影響を与えるのか、注視する必要があります。</p>
<p>さらに、最近の金融政策とその影響についても考慮する必要があります。日本銀行は長期にわたる超低金利政策を実施しており、この政策が企業投資と消費者行動に与える影響について、さらに分析が必要です。</p>
</div>
</div>
);
};
ERROR in src/components/pages/financingPage.tsx:388:5
TS2304: Cannot find name 'setIsCollapsed'.
386 | // 示例的折叠部分展开事件
387 | const toggleCollapse = () => {
> 388 | setIsCollapsed(!isCollapsed);
| ^^^^^^^^^^^^^^
389 | handleCollapseExpand(); // 重新计算小窗位置
390 | };
391 |
ERROR in src/components/pages/financingPage.tsx:388:21
TS2304: Cannot find name 'isCollapsed'.
386 | // 示例的折叠部分展开事件
387 | const toggleCollapse = () => {
> 388 | setIsCollapsed(!isCollapsed);
| ^^^^^^^^^^^
389 | handleCollapseExpand(); // 重新计算小窗位置
390 | };
391 |
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const [isCollapsed, setIsCollapsed] = useState(false); // 添加状态管理
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const panel = panelRef.current;
if (panel) {
const shiftX = e.clientX - panel.getBoundingClientRect().left;
const shiftY = e.clientY - panel.getBoundingClientRect().top;
const moveAt = (pageX: number, pageY: number) => {
panel.style.left = pageX - shiftX + 'px';
panel.style.top = pageY - shiftY + 'px';
};
const onMouseMove = (event: MouseEvent) => {
moveAt(event.pageX, event.pageY);
};
document.addEventListener('mousemove', onMouseMove);
panel.onmouseup = function () {
document.removeEventListener('mousemove', onMouseMove);
panel.onmouseup = null;
};
}
};
// 当折叠部分展开时,重新计算小窗位置
const handleCollapseExpand = () => {
const panel = panelRef.current;
if (panel) {
const rect = panel.getBoundingClientRect();
panel.style.left = `${rect.left}px`;
panel.style.top = `${rect.top}px`;
}
};
// 折叠部分展开/折叠事件
const toggleCollapse = () => {
setIsCollapsed(!isCollapsed);
handleCollapseExpand(); // 重新计算小窗位置
};
return (
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<div className="info-panel-header">
<span>関連情報</span>
<button className="close-info-panel" onClick={onClose}>ー</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
<p>現時点では、国際貿易摩擦やデジタル経済への移行などの課題に直面しています。これらの課題に対応するため、日本政府および民間企業は、持続可能な成長を目指して戦略を再評価し、多くの取り組みを進めています。</p>
<p>今後の日本経済の発展においては、労働力の流動性向上、AIおよびロボット技術の活用、さらにはエネルギー政策の見直しなど、さまざまな分野での改革が期待されています。これらの変化が企業の戦略にどのような影響を与えるのか、注視する必要があります。</p>
<p>さらに、最近の金融政策とその影響についても考慮する必要があります。日本銀行は長期にわたる超低金利政策を実施しており、この政策が企業投資と消費者行動に与える影響について、さらに分析が必要です。</p>
</div>
</div>
);
};
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const [isCollapsed, setIsCollapsed] = useState(false); // 添加状态管理
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const panel = panelRef.current;
if (panel) {
const shiftX = e.clientX - panel.getBoundingClientRect().left;
const shiftY = e.clientY - panel.getBoundingClientRect().top;
const moveAt = (pageX: number, pageY: number) => {
panel.style.left = pageX - shiftX + 'px';
panel.style.top = pageY - shiftY + 'px';
};
const onMouseMove = (event: MouseEvent) => {
moveAt(event.pageX, event.pageY);
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
};
// 当折叠部分展开时,重新计算小窗位置
const handleCollapseExpand = () => {
const panel = panelRef.current;
if (panel) {
const rect = panel.getBoundingClientRect();
panel.style.left = `${rect.left}px`;
panel.style.top = `${rect.top}px`;
}
};
// 折叠部分展开/折叠事件
const toggleCollapse = () => {
setIsCollapsed(!isCollapsed);
handleCollapseExpand(); // 重新计算小窗位置
};
return (
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<div className="info-panel-header">
<span>関連情報</span>
<button className="close-info-panel" onClick={onClose}>ー</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
<p>現時点では、国際貿易摩擦やデジタル経済への移行などの課題に直面しています。これらの課題に対応するため、日本政府および民間企業は、持続可能な成長を目指して戦略を再評価し、多くの取り組みを進めています。</p>
<p>今後の日本経済の発展においては、労働力の流動性向上、AIおよびロボット技術の活用、さらにはエネルギー政策の見直しなど、さまざまな分野での改革が期待されています。これらの変化が企業の戦略にどのような影響を与えるのか、注視する必要があります。</p>
<p>さらに、最近の金融政策とその影響についても考慮する必要があります。日本銀行は長期にわたる超低金利政策を実施しており、この政策が企業投資と消費者行動に与える影響について、さらに分析が必要です。</p>
</div>
</div>
);
};
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const [isCollapsed, setIsCollapsed] = useState(false); // 折叠状态管理
const [initialPosition, setInitialPosition] = useState({ x: 0, y: 0 });
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const panel = panelRef.current;
if (panel) {
const shiftX = e.clientX - panel.getBoundingClientRect().left;
const shiftY = e.clientY - panel.getBoundingClientRect().top;
const moveAt = (pageX: number, pageY: number) => {
panel.style.left = pageX - shiftX + 'px';
panel.style.top = pageY - shiftY + 'px';
};
const onMouseMove = (event: MouseEvent) => {
moveAt(event.pageX, event.pageY);
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
};
// 当折叠部分展开时,重新计算小窗位置
const handleCollapseExpand = () => {
const panel = panelRef.current;
if (panel) {
const rect = panel.getBoundingClientRect();
// 重新计算面板的位置
panel.style.left = `${initialPosition.x}px`;
panel.style.top = `${initialPosition.y}px`;
}
};
// 折叠部分展开/折叠事件
const toggleCollapse = () => {
setIsCollapsed(!isCollapsed);
handleCollapseExpand(); // 重新计算小窗位置
};
// 在组件挂载时初始化位置
useEffect(() => {
const panel = panelRef.current;
if (panel) {
const rect = panel.getBoundingClientRect();
setInitialPosition({ x: rect.left, y: rect.top });
}
}, []);
useEffect(() => {
if (isCollapsed) {
handleCollapseExpand();
}
}, [isCollapsed]);
return (
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<div className="info-panel-header">
<span>関連情報</span>
<button className="close-info-panel" onClick={onClose}>ー</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
<p>現時点では、国際貿易摩擦やデジタル経済への移行などの課題に直面しています。これらの課題に対応するため、日本政府および民間企業は、持続可能な成長を目指して戦略を再評価し、多くの取り組みを進めています。</p>
<p>今後の日本経済の発展においては、労働力の流動性向上、AIおよびロボット技術の活用、さらにはエネルギー政策の見直しなど、さまざまな分野での改革が期待されています。これらの変化が企業の戦略にどのような影響を与えるのか、注視する必要があります。</p>
<p>さらに、最近の金融政策とその影響についても考慮する必要があります。日本銀行は長期にわたる超低金利政策を実施しており、この政策が企業投資と消費者行動に与える影響について、さらに分析が必要です。</p>
</div>
</div>
);
};
const InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [initialMousePosition, setInitialMousePosition] = useState({ x: 0, y: 0 });
const [initialPanelPosition, setInitialPanelPosition] = useState({ x: 0, y: 0 });
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const panel = panelRef.current;
if (panel) {
// 记录初始鼠标位置
setInitialMousePosition({ x: e.clientX, y: e.clientY });
// 记录初始面板位置
const rect = panel.getBoundingClientRect();
setInitialPanelPosition({ x: rect.left, y: rect.top });
setIsDragging(true);
}
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const panel = panelRef.current;
if (panel) {
// 计算新的位置
const dx = e.clientX - initialMousePosition.x;
const dy = e.clientY - initialMousePosition.y;
panel.style.left = `${initialPanelPosition.x + dx}px`;
panel.style.top = `${initialPanelPosition.y + dy}px`;
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
} else {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging]);
// 当折叠部分展开时,重新计算小窗位置
const handleCollapseExpand = () => {
const panel = panelRef.current;
if (panel) {
const rect = panel.getBoundingClientRect();
panel.style.left = `${rect.left}px`;
panel.style.top = `${rect.top}px`;
}
};
// 折叠部分展开/折叠事件
const toggleCollapse = () => {
handleCollapseExpand(); // 重新计算小窗位置
};
return (
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<div className="info-panel-header">
<span>関連情報</span>
<button className="close-info-panel" onClick={onClose}>ー</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
<p>現時点では、国際貿易摩擦やデジタル経済への移行などの課題に直面しています。これらの課題に対応するため、日本政府および民間企業は、持続可能な成長を目指して戦略を再評価し、多くの取り組みを進めています。</p>
<p>今後の日本経済の発展においては、労働力の流動性向上、AIおよびロボット技術の活用、さらにはエネルギー政策の見直しなど、さまざまな分野での改革が期待されています。これらの変化が企業の戦略にどのような影響を与えるのか、注視する必要があります。</p>
<p>さらに、最近の金融政策とその影響についても考慮する必要があります。日本銀行は長期にわたる超低金利政策を実施しており、この政策が企業投資と消費者行動に与える影響について、さらに分析が必要です。</p>
</div>
</div>
);
};
経済産業省が発表する「機械受注統計調査報告」によると、製造業の新規受注額の増減は経済活動の先行指標とされます。たとえば、2024年1月の製造業新規受注指数が前月比2.0%増加した場合、今後の生産活動が増加する可能性を示唆します。
東京証券取引所で取引される「日経平均株価」は、投資家の経済見通しを反映する重要な先行指標です。たとえば、2024年4月の日経平均株価が10%上昇した場合、投資家が経済の改善を期待していることを示します。
. マネーサプライ
実例: 日本のマネーストック(M2)
日本銀行が発表する「マネーストック統計」は、通貨供給量の変化を示す指標です。たとえば、2024年5月のM2が前年同月比で3%増加した場合、金融機関の融資や政府の支出が増加しており、将来の経済活動が活発化する可能性があることを示しています。
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip
} from 'chart.js';
import { Dispatch, SetStateAction, 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 CustomInputForm = ({
income,
expense,
updateData
}: {
income: number[];
expense: number[];
updateData: (newIncome: number[], newExpense: number[]) => void;
}) => {
const [customIncome, setCustomIncome] = useState<number[]>(income);
const [customExpense, setCustomExpense] = useState<number[]>(expense);
const [selectedTab, setSelectedTab] = useState<'収入' | '支出'>('収入');
useEffect(() => {
setCustomIncome(income);
setCustomExpense(expense);
}, [income, expense]);
const handleIncomeChange = (index: number, value: string) => {
const newIncome = [...customIncome];
// 値が空の場合はそのまま空にする
if (value === '') {
newIncome[index] = '' as unknown as number;
} else if (!isNaN(Number(value)) || value === '-' || value === '.') {
// マイナスや小数点も許可しつつ、数値に変換
newIncome[index] = value as unknown as number;
}
setCustomIncome(newIncome);
};
const handleExpenseChange = (index: number, value: string) => {
const newExpense = [...customExpense];
// 値が空の場合はそのまま空にする
if (value === '') {
newExpense[index] = '' as unknown as number;
} else if (!isNaN(Number(value)) || value === '-' || value === '.') {
// マイナスや小数点も許可しつつ、数値に変換
newExpense[index] = value as unknown as number;
}
setCustomExpense(newExpense);
};
const handleSave = () => {
// 両方のデータをまとめて更新
updateData(customIncome, customExpense);
};
return (
<div className="custom-input-form">
<div className="tab-switch">
<button
className={`tab-button ${selectedTab === '収入' ? 'active' : ''}`}
onClick={() => setSelectedTab('収入')}
>
収入
</button>
<button
className={`tab-button ${selectedTab === '支出' ? 'active' : ''}`}
onClick={() => setSelectedTab('支出')}
>
支出
</button>
</div>
{/* 選択されたタブに応じて表示を切り替える */}
{selectedTab === '収入' ? (
<div className="form-container">
{/* 左側: 4月~9月 */}
<div className="column">
{customIncome.slice(0, 6).map((value, index) => {
const month = `${index + 4}月`; // 4月から9月
return (
<div key={index} className="input-item">
<div className='form-month'>{month}</div>
<input
type="text"
value={value !== undefined && value !== null ? value : ''}
onChange={(e) => handleIncomeChange(index, e.target.value)}
disabled={index < 3}
/>
<div className='form-unit'>百万円</div>
</div>
);
})}
</div>
{/* 右側: 10月~3月 */}
<div className="column">
{customIncome.slice(6, 12).map((value, index) => {
// 0〜2は10月〜12月、3〜5は1月〜3月
const month = index < 3 ? `${index + 10}月` : `${index - 2}月`; // 10月〜12月はindex + 10, 1月〜3月はindex - 2
return (
<div key={index + 6} className="input-item">
<div className='form-month'>{month}</div>
<input
type="text"
value={value !== undefined && value !== null ? value : ''}
onChange={(e) => handleIncomeChange(index + 6, e.target.value)}
/>
<div className='form-unit'>百万円</div>
</div>
);
})}
</div>
</div>
) : (
<div className="form-container">
{/* 左側: 4月~9月 */}
<div className="column">
{customExpense.slice(0, 6).map((value, index) => {
const month = `${index + 4}月`; // 4月から9月
return (
<div key={index} className="input-item">
<div className='form-month'>{month}</div>
<input
type="text"
value={value !== undefined && value !== null ? value : ''}
onChange={(e) => handleExpenseChange(index, e.target.value)}
disabled={index < 3}
/>
<div className='form-unit'>百万円</div>
</div>
);
})}
</div>
{/* 右側: 10月~3月 */}
<div className="column">
{customExpense.slice(6, 12).map((value, index) => {
// 0〜2は10月〜12月、3〜5は1月〜3月
const month = index < 3 ? `${index + 10}月` : `${index - 2}月`; // 10月〜12月はindex + 10, 1月〜3月はindex - 2
return (
<div key={index + 6} className="input-item">
<div className='form-month'>{month}</div>
<input
type="text"
value={value !== undefined && value !== null ? value : ''}
onChange={(e) => handleExpenseChange(index + 6, e.target.value)}
/>
<div className='form-unit'>百万円</div>
</div>
);
})}
</div>
</div>
)}
<button className="save-button" onClick={handleSave}>
設定
</button>
</div>
);
};
const CustomExpenseForecast = ({
updateChartAndAction,
chartRef,
isCustomForecastOpen,
setIsCustomForecastOpen,
income,
expense
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<SetStateAction<boolean>>;
income: number[];
expense: number[];
}) => {
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState);
};
const handleUpdateData = (newIncome: number[], newExpense: number[]) => {
updateChartAndAction(newIncome, newExpense, true);
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>
カスタム入力
</button>
{isCustomForecastOpen && (
<div className="form-container-out">
<CustomInputForm
income={income}
expense={expense}
updateData={handleUpdateData}
/>
</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 [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 [income, setIncome] = useState<number[]>([11.5, 13.5, 13.0, 7.0, 10.5, 9.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0]);
const [expense, setExpense] = useState<number[]>([-9.5, -14.0, -19.0, -2.8, -15.0, -10.5, -8.0, -13.2, -10.5, -1.5, -10.5, -7.5]);
const updateChartAndAction = (newIncome: number[], newExpense: number[], isCustom: boolean) => {
const incomeExpenseDiff = newIncome.map((incomeValue, i) => incomeValue + newExpense[i]);
const balance = newIncome.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : 10) + incomeValue + newExpense[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: newIncome,
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: newExpense,
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]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
setIncome(selectedData.income);
setExpense(selectedData.expense);
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
};
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 || { labels: [], datasets: [] }} options={{ responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, title: { display: true, text: '[百万円]' } } } }} />
</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}
income={income}
expense={expense}
/>
</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 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: '-' },
];
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, -2.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -13.2, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、5月と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: "昨年の残高推移は非常に安定していました。今年の前半では、5月と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: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、5月と6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
export default EmptyPage;
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip
} from 'chart.js';
import { Dispatch, SetStateAction, 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 CustomInputForm = ({
income,
expense,
updateData
}: {
income: number[];
expense: number[];
updateData: (newIncome: number[], newExpense: number[]) => void;
}) => {
const [customIncome, setCustomIncome] = useState<number[]>(income);
const [customExpense, setCustomExpense] = useState<number[]>(expense);
const [selectedTab, setSelectedTab] = useState<'収入' | '支出'>('収入');
useEffect(() => {
setCustomIncome(income);
setCustomExpense(expense);
}, [income, expense]);
const handleIncomeChange = (index: number, value: string) => {
const newIncome = [...customIncome];
if (value === '') {
newIncome[index] = '' as unknown as number;
} else if (!isNaN(Number(value)) || value === '-' || value === '.') {
newIncome[index] = value as unknown as number;
}
setCustomIncome(newIncome);
};
const handleExpenseChange = (index: number, value: string) => {
const newExpense = [...customExpense];
if (value === '') {
newExpense[index] = '' as unknown as number;
} else if (!isNaN(Number(value)) || value === '-' || value === '.') {
newExpense[index] = value as unknown as number;
}
setCustomExpense(newExpense);
};
const handleSave = () => {
updateData(customIncome, customExpense);
};
return (
<div className="custom-input-form">
<div className="tab-switch">
<button
className={`tab-button ${selectedTab === '収入' ? 'active' : ''}`}
onClick={() => setSelectedTab('収入')}
>
収入
</button>
<button
className={`tab-button ${selectedTab === '支出' ? 'active' : ''}`}
onClick={() => setSelectedTab('支出')}
>
支出
</button>
</div>
{selectedTab === '収入' ? (
<div className="form-container">
<div className="column">
{customIncome.slice(0, 6).map((value, index) => {
const month = `${index + 4}月`;
return (
<div key={index} className="input-item">
<div className='form-month'>{month}</div>
<input
type="text"
value={value !== undefined && value !== null ? value : ''}
onChange={(e) => handleIncomeChange(index, e.target.value)}
disabled={index < 3}
/>
<div className='form-unit'>百万円</div>
</div>
);
})}
</div>
<div className="column">
{customIncome.slice(6, 12).map((value, index) => {
const month = index < 3 ? `${index + 10}月` : `${index - 2}月`;
return (
<div key={index + 6} className="input-item">
<div className='form-month'>{month}</div>
<input
type="text"
value={value !== undefined && value !== null ? value : ''}
onChange={(e) => handleIncomeChange(index + 6, e.target.value)}
/>
<div className='form-unit'>百万円</div>
</div>
);
})}
</div>
</div>
) : (
<div className="form-container">
<div className="column">
{customExpense.slice(0, 6).map((value, index) => {
const month = `${index + 4}月`;
return (
<div key={index} className="input-item">
<div className='form-month'>{month}</div>
<input
type="text"
value={value !== undefined && value !== null ? Math.abs(value) : ''}
onChange={(e) => handleExpenseChange(index, e.target.value)}
disabled={index < 3}
/>
<div className='form-unit'>百万円</div>
</div>
);
})}
</div>
<div className="column">
{customExpense.slice(6, 12).map((value, index) => {
const month = index < 3 ? `${index + 10}月` : `${index - 2}月`;
return (
<div key={index + 6} className="input-item">
<div className='form-month'>{month}</div>
<input
type="text"
value={value !== undefined && value !== null ? Math.abs(value) : ''}
onChange={(e) => handleExpenseChange(index + 6, e.target.value)}
/>
<div className='form-unit'>百万円</div>
</div>
);
})}
</div>
</div>
)}
<button className="save-button" onClick={handleSave}>
設定
</button>
</div>
);
};
const CustomExpenseForecast = ({
updateChartAndAction,
chartRef,
isCustomForecastOpen,
setIsCustomForecastOpen,
income,
expense
}: {
updateChartAndAction: (income: number[], expenses: number[], isCustom: boolean) => void;
chartRef: React.RefObject<ChartJS>;
isCustomForecastOpen: boolean;
setIsCustomForecastOpen: Dispatch<SetStateAction<boolean>>;
income: number[];
expense: number[];
}) => {
const toggleForm = () => {
const newState = !isCustomForecastOpen;
setIsCustomForecastOpen(newState);
};
const handleUpdateData = (newIncome: number[], newExpense: number[]) => {
updateChartAndAction(newIncome, newExpense, true);
};
return (
<div className="custom-expense-forecast">
<button className="custom-expense-btn" onClick={toggleForm}>
カスタム入力
</button>
{isCustomForecastOpen && (
<div className="form-container-out">
<CustomInputForm
income={income}
expense={expense}
updateData={handleUpdateData}
/>
</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 InfoPanel = ({ onClose }: { onClose: () => void }) => {
const panelRef = useRef<HTMLDivElement | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [initialMousePosition, setInitialMousePosition] = useState({ x: 0, y: 0 });
const [initialPanelPosition, setInitialPanelPosition] = useState({ x: 0, y: 0 });
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const panel = panelRef.current;
if (panel) {
setInitialMousePosition({ x: e.clientX, y: e.clientY });
const rect = panel.getBoundingClientRect();
setInitialPanelPosition({ x: rect.left, y: rect.top });
setIsDragging(true);
}
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const panel = panelRef.current;
if (panel) {
const dx = e.clientX - initialMousePosition.x;
const dy = e.clientY - initialMousePosition.y;
panel.style.left = `${initialPanelPosition.x + dx}px`;
panel.style.top = `${initialPanelPosition.y + dy}px`;
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
} else {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging]);
const handleCollapseExpand = () => {
const panel = panelRef.current;
if (panel) {
const rect = panel.getBoundingClientRect();
panel.style.left = `${rect.left}px`;
panel.style.top = `${rect.top}px`;
}
};
const toggleCollapse = () => {
handleCollapseExpand();
};
return (
<div className="info-panel" ref={panelRef} onMouseDown={handleMouseDown}>
<div className="info-panel-header">
<span>関連情報</span>
<button className="close-info-panel" onClick={onClose}>ー</button>
</div>
<div className="info-content">
<p>日本経済は、過去数十年間にわたってさまざまな変動を経験してきました。1980年代のバブル経済から始まり、1990年代の失われた10年、そして最近の新型コロナウイルスのパンデミックによる影響など、多くの要因が経済に影響を与えました。経済成長の主な要因には、輸出の増加、技術革新、および内需の拡大が挙げられます。</p>
<p>現時点では、国際貿易摩擦やデジタル経済への移行などの課題に直面しています。これらの課題に対応するため、日本政府および民間企業は、持続可能な成長を目指して戦略を再評価し、多くの取り組みを進めています。</p>
<p>今後の日本経済の発展においては、労働力の流動性向上、AIおよびロボット技術の活用、さらにはエネルギー政策の見直しなど、さまざまな分野での改革が期待されています。これらの変化が企業の戦略にどのような影響を与えるのか、注視する必要があります。</p>
<p>さらに、最近の金融政策とその影響についても考慮する必要があります。日本銀行は長期にわたる超低金利政策を実施しており、この政策が企業投資と消費者行動に与える影響について、さらに分析が必要です。</p>
</div>
</div>
);
};
const InfoButton = ({ onClick }: { onClick: () => void }) => {
return (
<button className="info-button" onClick={onClick} title='teset'>
<i className="fa fa-info-circle" aria-hidden="true"></i>
</button>
);
};
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 [income, setIncome] = useState<number[]>([11.5, 13.5, 13.0, 7.0, 10.5, 9.0, 10.0, 9.5, 11.0, 5.5, 11.0, 8.0]);
const [expense, setExpense] = useState<number[]>([-9.5, -14.0, -19.0, -2.8, -15.0, -10.5, -8.0, -13.2, -10.5, -1.5, -10.5, -7.5]);
const [isInfoPanelOpen, setIsInfoPanelOpen] = useState(false);
const updateChartAndAction = (newIncome: number[], newExpense: number[], isCustom: boolean) => {
const incomeExpenseDiff = newIncome.map((incomeValue, i) => incomeValue + newExpense[i]);
const balance = newIncome.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : 10) + incomeValue + newExpense[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: newIncome,
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: newExpense,
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]);
const handleExpenseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newExpenseLevel = e.target.value;
setExpenseLevel(newExpenseLevel);
const selectedData = dataSets[newExpenseLevel];
if (selectedData) {
setIncome(selectedData.income);
setExpense(selectedData.expense);
updateChartAndAction(selectedData.income, selectedData.expense, false);
}
};
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 || { labels: [], datasets: [] }} options={{ responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, title: { display: true, text: '[百万円]' } } } }} />
</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}
income={income}
expense={expense}
/>
</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>
{isInfoPanelOpen && <InfoPanel onClose={() => setIsInfoPanelOpen(false)} />}
{!isInfoPanelOpen && <InfoButton onClick={() => setIsInfoPanelOpen(true)} />}
</div>
);
};
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: '-' },
];
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, -2.8, -15.0 * 0.8, -2.0 * 0.8, -10.5 * 0.8, -8.0 * 0.8, -13.2, -1.0 * 0.8, -10.5 * 0.8, -7.5 * 0.8],
action: {
cashFlow: "昨年は全体的に安定した残高推移が見られました。今年の前半では収入が予想を下回る一方で、5月と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: "昨年の残高推移は非常に安定していました。今年の前半では、5月と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: "昨年は支出が増加し、残高推移が後半にかけて悪化しました。今年の前半でも同様の傾向が見られ、5月と6月の当月収支がマイナスになっています。収入は安定していますが、支出の増加により残高推移が悪化しています。",
countermeasure: "残高改善の対策として以下の検討をおすすめいたします。\n現金回収を早めるため、請求の迅速化\n売掛金の管理を徹底し、未回収の債権に対する早期対応\n短期的な追加融資\nリスク管理を強化するため、定期的かつ頻度を上げて残高状況のチェックとアクションを実行"
}
},
};
export default EmptyPage;
const updateChartAndAction = (newIncome: number[], newExpense: number[], isCustom: boolean) => {
// 将支出数据转换为负数进行计算
const negativeExpense = newExpense.map(exp => -Math.abs(exp));
// 计算当月收支差和余额
const incomeExpenseDiff = newIncome.map((incomeValue, i) => incomeValue + negativeExpense[i]);
const balance = newIncome.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : 10) + incomeValue + negativeExpense[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: newIncome,
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: newExpense, // 显示正数
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);
}
}
};
const updateChartAndAction = (newIncome: number[], newExpense: number[], isCustom: boolean) => {
// 在计算时,将支出转换为负数
const negativeExpense = newExpense.map(exp => -Math.abs(exp));
// 计算当月收支差和余额
const incomeExpenseDiff = newIncome.map((incomeValue, i) => incomeValue + negativeExpense[i]);
const balance = newIncome.reduce((acc: number[], incomeValue, i) => {
const newBalance = (acc.length > 0 ? acc[acc.length - 1] : 10) + incomeValue + negativeExpense[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: newIncome,
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: newExpense, // 显示为正数
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);
}
}
};
支出の選択欄の内容を調整し、 選択後に表示する文言は楽観:0.8倍、中立:相当、悲観:1.2倍にしたい。プルダウンの内容は既存のまま、もしくは調整してほしい。