ホーム > 作ってみた > 資産管理 > 資産記録アプリ開発④~フロントエンド実装・資産残高管理画面~

資産記録アプリ開発④~フロントエンド実装・資産残高管理画面~

作ってみた資産管理frontendreact

環境構築

前回バックエンドの実装が出来たので、フロントエンドの実装を行っていきます。
まずはサクッと環境構築していきます。今回はtypescriptを使用したNext.js環境で開発を行います。
合わせて必要なライブラリもインストールしておきます。
今回はMaterial UIとreact-chartjs-2を使用します。

npx create-next-app@latest --ts
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material
npm install chart.js react-chartjs-2

また、個人の好みではありますがソースコードは「src」以下に入っていた方がスッキリするので今回は少し構成を変更します。
20221123131500
「src」フォルダを作成し、その配下に「components」「layout」「pages」「services」「styles」「types」を作成しました。 ※API呼び出し処理を共通化する際に「services」にソースコードを格納しようと思いましたが、今回は使いませんでした。今後使用予定なのでとりあえず作っておきます。



ページの骨組み作成

Header作成

「Components」配下にHeaderフォルダとHeader.tsxを作成し
Material UI - App Bar
を参考にヘッダーを作成します。
スマホなどから使う予定はないですが、とりあえず「Responsive App bar with Drawer」のコードをコピペし、リンクの設定や表示項目を変更します。

import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import MenuIcon from '@mui/icons-material/Menu';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Link from 'next/link';
interface Props {
/**
* Injected by the documentation to work in an iframe.
* You won't need it on your project.
*/
window?: () => Window;
}
const drawerWidth = 240;
const navItems = ['資産残高管理', '配当管理'];
const navItemsUrl = ['assets', 'dividend'];
const Header = (props: Props) => {
const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const drawer = (
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ my: 2 }}>
<Link href={`/`}>
資産管理アプリ
</Link>
</Typography>
<Divider />
<List>
{navItems.map((item, i) => (
<ListItem key={item} disablePadding>
<Link href={`/${navItemsUrl[i]}`}>
<ListItemButton sx={{ textAlign: 'center' }}>
<ListItemText primary={item} />
</ListItemButton>
</Link>
</ListItem>
))}
</List>
</Box>
);
const container = window !== undefined ? () => window().document.body : undefined;
return (
<div style={{position: 'fixed'}}>
<Box sx={{ display: 'flex' }}>
<AppBar component="nav">
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
component="div"
sx={{ flexGrow: 1, display: { xs: 'none', sm: 'block' } }}
>
<Link href={`/`}>
資産管理アプリ
</Link>
</Typography>
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
{navItems.map((item, i) => (
<Link href={`/${navItemsUrl[i]}`}>
<Button key={item} sx={{ color: '#fff' }}>
{item}
</Button>
</Link>
))}
</Box>
</Toolbar>
</AppBar>
<Box component="nav">
<Drawer
container={container}
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawer}
</Drawer>
</Box>
</Box>
</div>
);
}
export default Header;


Layout作成

全ページでHeaderを固定表示するためのLayoutを作成します。
先ほど作成した「layout」フォルダにLayout.tsxを作成し、Headerを表示するよう記述します。
また、今回はcssを使いたいので、「styles」フォルダにLayout.module.cssも作っておきます。

import { ReactElement } from 'react';
import Header from '../components/Header/Header';
import styles from '../styles/Layout.module.css'
type LayoutProps = Required<{
readonly children: ReactElement
}>
export const Layout = ({ children }: LayoutProps) => (
<div>
<Header />
<div className={styles.main}>
<main>{children}</main>
</div>
</div>
)
.main {
margin: 100px 0px 0px 100px;
}

デフォルトの状態だとHeaderでコンテンツが隠れてしまうので、cssでmarginを設定しておきました。

「pages」フォルダ配下にある「_app.tsx」にLyoutを追加します。

import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { Layout } from '../layout/Layout'
export default function App({ Component, pageProps }: AppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}

これで前ページにHeaderが表示されるようになりました。 ※下記画像ではわかりやすいよう、「index.tsx」に「home」と表示する処理を追加して確認しています。
20221123133100



資産残高管理ページ作成

「pages」フォルダ配下に「assets.tsx」を作成し、本題の資産残高管理ページを作っていきます。

グラフの表示

Chart.js、react-chartjs-2では共に公式ページに色々なサンプルを載せてくれています。

Chart.js:Chart.js - samples
react-chartjs-2:react-chartjs-2 - samples

今回は「Line Charts」を使用します。
まずはサンプルをコピペして、動作することを確認します。「faker」が入っていない場合は、 Math.random()などを使うと動作するようになります。

20221123134400

動作することが確認できたので、APIからデータを取得してそれを表示するように修正していきます。

事前に型の定義だけしておきます。「types」フォルダ配下に「assets.tsx」を作成し、APIで受け取るデータの型を定義しておきます。

export interface IApp{
id: number;
name: string;
}
export interface IChartData{
label: string;
data: Array<Number>;
borderColor: string;
backgroundColor: string;
}

「pages/assets.tsx」を修正していきます。
useState()を使って、データを格納しておくための変数を用意しておきます。
そして、useEffect()を使ってまずはアプリ一覧取得APIを呼び出し、「apps」に格納します。

import React, { useEffect, useState } from 'react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import axios from "axios";
import { IApp, IChartData } from '../types/assets';
import styles from '../styles/assets.module.css';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
export const Assets = () => {
// アプリ一覧
const [apps, setApps] = useState<Array<IApp>>();
// グラフ用データ
const [chartData, setChartData] = useState<Array<IChartData>>([]);
// グラフ用ラベルデータ
const [labels, setLabels] = useState<Array<IChartData>>([]);
// 更新用
const [update, setUpdate] = useState(false);
useEffect(() => {
// アプリ一覧取得
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/appmaster/`)
.then((response) => {
setApps(response.data);
createChartData(response.data);
});
}, [update]);
return (
<div className={styles.content_wrapper}>
<div className={styles.chart}>
<Line options={options} data={data} />
</div>
</div>
);
}
export default Assets;

「NEXT_PUBLIC_BACKENDURL」は「.env」に

NEXT_PUBLIC_BACKENDURL=http://localhost:8888

等のように定義しておきます。

次に、グラフ用データを作成します。
今回はアプリ毎に分けて表示するので、アプリの個数分APIを呼び出し、配列にデータを格納していきます。

const createChartData = (appNames:Array<IApp>) => {
console.log(appNames);
let temp: IChartData[] = [];
appNames.map((e, i) => {
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/asset-amount/?target=${e.name}`)
.then((response) => {
const value: IChartData = {
label: e.name,
data: response.data.data,
borderColor: borderColorList[i],
backgroundColor: backgroundColorList[i],
}
temp.push(value);
if (temp.length === appNames.length){
setChartData(temp);
}
});
});
}

最後にラベルを取得します。今回ラベルには登録日を使用します。
こちらは事前に作成しておいたAPIを呼び出し「labels」に格納するだけです。

useEffect(() => {
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/asset-date/`)
.then((response) => {
setLabels(response.data.data)
});
}, [update]);

これまでのものをすべてまとめ、取得したデータをChart.jsの関数に渡すと下記のようになります。

import React, { useEffect, useState } from 'react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import axios from "axios";
import { IApp, IChartData } from '../types/assets';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const borderColorList: string[] = ['rgb(255, 99, 132)', 'rgb(53, 162, 235)']
const backgroundColorList: string[] = ['rgba(255, 99, 132, 0.5)', 'rgba(53, 162, 235, 0.5)']
export const options = {
responsive: true,
plugins: {
legend: {
position: 'top' as const,
},
title: {
display: true,
text: '資産残高',
},
},
};
export const Assets = () => {
// アプリ一覧
const [apps, setApps] = useState<Array<IApp>>();
// グラフ用データ
const [chartData, setChartData] = useState<Array<IChartData>>([]);
// グラフ用ラベルデータ
const [labels, setLabels] = useState<Array<IChartData>>([]);
// 更新用
const [update, setUpdate] = useState(false);
const createChartData = (appNames:Array<IApp>) => {
console.log(appNames);
let temp: IChartData[] = [];
appNames.map((e, i) => {
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/asset-amount/?target=${e.name}`)
.then((response) => {
const value: IChartData = {
label: e.name,
data: response.data.data,
borderColor: borderColorList[i],
backgroundColor: backgroundColorList[i],
}
temp.push(value);
if (temp.length === appNames.length){
setChartData(temp);
}
});
});
}
useEffect(() => {
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/appmaster/`)
.then((response) => {
setApps(response.data);
createChartData(response.data);
});
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/asset-date/`)
.then((response) => {
setLabels(response.data.data)
});
}, [update]);
const data = {
labels,
datasets: chartData,
};
return (
<div className={styles.content_wrapper}>
<div className={styles.chart}>
<Line options={options} data={data} />
</div>
</div>
);
}
export default Assets;

無事にグラフが表示されました。
20221123141600



グラフデータ一覧の表示

続いて、データ一覧を表示するためのテーブルを追加していきます。
見やすいように
アプリ毎にタブを分け、選択されたタブに対応するアプリのデータをテーブルで表示
というようにしていきます。
「components」配下に「Table」「Tabs」を作成します。
TabはMaterial UIを、テーブルはこちらを参考に作成しました。

■Table.tsx
コードのほとんどは流用させていただいていますが、データ更新、削除などの処理を独自に追加しています。

import React, { useState } from "react";
import { styled } from '@mui/material/styles';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from "@mui/material/TableHead";
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import Input from "@mui/material/Input";
import IconButton from "@mui/material/IconButton";
import EditIcon from "@mui//icons-material/EditOutlined";
import DoneIcon from "@mui//icons-material/DoneAllTwoTone";
import RevertIcon from "@mui//icons-material/NotInterestedOutlined";
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import { IAssetData } from '../../types/assets';
import axios from "axios";
// 型定義
export interface ICreateData{
[key: string]: string|number|boolean|null;
}
export interface ITableCell{
row:ICreateData;
name:string;
onChange:Function;
}
export interface IPrevious{
[id: string]: ICreateData;
}
export interface ITableProps{
appData: IAssetData[];
app: string;
handleUpdate: Function;
}
const SimpleTableCell = styled(TableCell)(({ theme }) => ({
width: 150,
height: 40
}));
// セルコンポーネント。「isEditMode」がtrueの場合は「Input」、falseの場合はデータを表示する
const CustomTableCell = ({ row, name, onChange }:ITableCell) => {
const { isEditMode } = row;
return (
<SimpleTableCell align="left">
{isEditMode ? (
<Input
value={row[name]}
name={name}
onChange={e => onChange(e, row)}
/>
) : (
row[name]
)}
</SimpleTableCell>
);
};
// データ変換用関数。テーブル表示に合わせてデータを変換
const createData = (
id: number,
asset_amount: number,
diff: number,
diff_rate: number,
created_at: Date,
managed_app: string,
memo: string|null
) => {
return ({
id: String(id),
asset_amount: asset_amount,
diff: diff,
diff_rate: diff_rate,
created_at: String(created_at),
managed_app,
memo,
isEditMode: false
})
};
// データ作成用関数
const createTableData = (appData:IAssetData[]|undefined, app:string) => {
let data: ICreateData[] = [];
if (appData !== undefined){
appData.map((d) => {
data.push(createData(d.id, d.asset_amount, d.diff, d.diff_rate, d.created_at, app, d.memo))
})
}
return data;
}
const CustomTable = ({appData, app, handleUpdate}:ITableProps) => {
// テーブル表示用データ
const [rows, setRows] = useState<Array<ICreateData>>(createTableData(appData, app));
const [previous, setPrevious] = useState<IPrevious>({});
const onToggleEditMode = (id: string) => {
setRows(state => {
return rows.map(row => {
if (row.id === id) {
return { ...row, isEditMode: !row.isEditMode };
}
return row;
});
});
};
const onChange = (e:any, row:ICreateData) => {
if (!previous[Number(row.id)]) {
setPrevious(state => ({ ...state, [String(row.id)]: row }));
}
const value = e.target.value;
const name = e.target.name;
const { id } = row;
const newRows = rows.map(row => {
if (row.id === id) {
return { ...row, [name]: value };
}
return row;
});
setRows(newRows);
};
// データ更新用関数
const onUpdate = (id:string) => {
rows.map(row => {
if (row.id === id) {
const tempRow = appData.filter(data => data.id.toString() === id)[0];
tempRow.asset_amount = Number(row.asset_amount);
tempRow.diff = Number(row.diff);
tempRow.diff_rate = Number(row.diff_rate);
tempRow.memo = !row.memo ? null : row.memo.toString();
axios
.put(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/assetlist/`, tempRow)
.then((response) => {
handleUpdate()
});
}
})
onToggleEditMode(id);
};
// データ削除用関数
const onDelete = (id:string) => {
rows.map(row => {
if (row.id === id) {
axios
.delete(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/assetlist/?target=${id}`)
.then((response) => {
handleUpdate()
});
}
})
onToggleEditMode(id);
};
return (
<Paper>
<Table aria-label="caption table">
<TableHead>
<TableRow>
<TableCell align="left" />
<TableCell align="left">資産額</TableCell>
<TableCell align="left">前回比(円)</TableCell>
<TableCell align="left">前回比(%)</TableCell>
<TableCell align="left">日付</TableCell>
<TableCell align="left">メモ</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => (
<TableRow key={String(row.id)}>
<TableCell>
{row.isEditMode ? (
<>
<IconButton
aria-label="done"
onClick={() => onUpdate(String(row.id))}
>
<DoneIcon />
</IconButton>
<IconButton
aria-label="revert"
onClick={() => onDelete(String(row.id))}
>
<DeleteIcon />
</IconButton>
<IconButton
aria-label="revert"
onClick={() => onToggleEditMode(String(row.id))}
>
<RevertIcon />
</IconButton>
</>
) : (
<IconButton
aria-label="delete"
onClick={() => onToggleEditMode(String(row.id))}
>
<EditIcon />
</IconButton>
)}
</TableCell>
<CustomTableCell {...{ row, name: "asset_amount", onChange }} />
<CustomTableCell {...{ row, name: "diff", onChange }} />
<CustomTableCell {...{ row, name: "diff_rate", onChange }} />
<CustomTableCell {...{ row, name: "created_at", onChange }} />
<CustomTableCell {...{ row, name: "memo", onChange }} />
</TableRow>
))}
</TableBody>
</Table>
</Paper>
);
}
export default CustomTable;

■Tabs.tsx
親コンポーネントから「アプリ一覧」「テーブルに表示するデータ」「更新用関数」を受け取ります。
こちらも基本的には公式サンプルを流用していますが、
・アプリの数に応じてタブを自動生成(map()を使用してアプリの数だけ実行)
・タブが選択された際に渡す値をアプリの「id」とし、選択された値に応じて表示データを変える といったような変更を加えています。

import React, { useState } from "react";
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { IApp, IAssetData } from '../../types/assets';
import CustomTable from '../../components/Table/Table';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 2 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
interface BasicTabsProps {
apps: Array<IApp>;
allData: IAssetData[];
handleUpdate: Function;
}
export default function BasicTabs({apps, allData, handleUpdate}: BasicTabsProps) {
const [value, setValue] = useState(1);
const [appData, setAppData] = useState<Array<IAssetData>>(allData.filter(data => data.managed_app === 1));
// 選択されたタブの値に応じて、表示データを変更する
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
setAppData(allData.filter(data => data.managed_app === newValue))
};
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
{apps.map((m, index) => (
<Tab label={m.name} {...a11yProps(m.id)} value={m.id}/>
))}
</Tabs>
</Box>
{apps.map((m, index) => (
<TabPanel value={value} index={m.id}> {(appData !== undefined) && <CustomTable appData={appData} app={m.name} handleUpdate={handleUpdate} />}</TabPanel>
))}
</Box>
);
}

「pages/assets.tsx」を修正して、テーブル表示に使用するデータの取得、タブとテーブルを表示するようにします。

useEffect(() => {
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/appmaster/`)
.then((response) => {
setApps(response.data);
createChartData(response.data);
});
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/asset-date/`)
.then((response) => {
setLabels(response.data.data)
});
// 追加。テーブル表示用データ取得
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/assetlist/`)
.then((response) => {
setAllData(response.data);
});
}, [update]);
return (
<div className={styles.content_wrapper}>
<div className={styles.chart}>
<Line options={options} data={data} />
</div>
{/* 追加 */}
<div className={styles.table}>
{(apps !== undefined) && (allData !== undefined) && <BasicTabs apps={apps} allData={allData} handleUpdate={handleUpdate} /> }
</div>
</div>
);

レイアウト調整のためにcssファイルも作成しておきます。
```javascript .content_wrapper { display: flex; flex-wrap: wrap; }

.chart { width: 800px; }

.table { width: 920; } ```

想定通り表示されました!
20221123144500



新規データ登録ボタン

せっかくなので新規データ登録ボタンも作成しておきます。
ボタンをクリック→ダイアログが開き、データ入力、データ登録
というようにしたいので、これまで同様Components配下にDialogコンポーネントを作っていきます。
こちらもMaterial UIのサンプルを流用する形で作りました。
今回入力項目とするのは「資産額」「メモ」「アプリ」の3つのみでそれ以外の項目はバックエンド側で自動入力という形にしています。
また、「アプリ」に関しては、毎回入力するのが面倒なのでSelectボックスを使用しました。
各項目の入力部分とエラーチェック、POSTメソッド(データ登録API呼び出し)を記述していきます。

import React, { useState } from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Box from '@mui/material/Box';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { IApp } from '../../types/assets';
import axios from "axios";
interface RegisterAssetDialogProps {
open: boolean;
setOpen: Function;
apps: Array<IApp>;
handleUpdate: Function;
}
export default function RegisterAssetDialog({open, setOpen, apps, handleUpdate}: RegisterAssetDialogProps) {
// 資産額
const [asset, setAsset] = useState('');
// メモ
const [memo, setMemo] = useState('');
// アプリ
const [appId, setAppId] = useState('');
// 各項目エラー用
const [assetError, setAssetError] = useState(false);
const [assetErrorMsg, setAssetErrorMsg] = useState('');
const [appIdError, setAppIdError] = useState(false);
const [appIdErrorMsg, setAppIdErrorMsg] = useState('');
// 資産額入力のたびに呼び出され、値を変数に格納
const handleChangeAsset = (event: React.ChangeEvent<HTMLInputElement>) => {
setAsset(event.target.value);
setAssetError(false);
setAssetErrorMsg('');
};
// メモ入力のたびに呼び出され、値を変数に格納
const handleChangeMemo = (event: React.ChangeEvent<HTMLInputElement>) => {
setMemo(event.target.value);
};
// アプリ選択のたびに呼び出され、値を変数に格納
const handleChangeAppId = (event: SelectChangeEvent) => {
setAppId(event.target.value);
setAppIdError(false);
setAppIdErrorMsg('');
};
// Registerボタン押下時に呼び出され、データ登録APIを呼び出す
const handleRegister = () => {
if (asset === ''){
setAssetError(true);
setAssetErrorMsg('入力してください');
return -1;
}
if (appId === ''){
setAppIdError(true);
setAppIdErrorMsg('選択してください');
return -1;
}
const data = {'asset': asset, 'memo': memo, 'app': appId};
axios
.post(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/assetlist/`, data)
.then((response) => {
handleUpdate();
handleClose();
});
};
// ダイアログを閉じる
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>新規データ登録</DialogTitle>
<DialogContent>
<TextField
margin="dense"
id="name"
label="資産額"
fullWidth
variant="standard"
onChange={handleChangeAsset}
error={assetError}
helperText={assetErrorMsg}
required
/>
<TextField
margin="dense"
id="name"
label="メモ"
fullWidth
variant="standard"
onChange={handleChangeMemo}
/>
<Box sx={{ minWidth: 120 }}>
<FormControl fullWidth required error={appIdError}>
<InputLabel id="demo-simple-select-label">アプリ</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={appId}
label="Managed App"
onChange={handleChangeAppId}
>
{apps.map((app) => {
return <MenuItem value={app.id}>{app.name}</MenuItem>
})}
</Select>
<FormHelperText>{appIdErrorMsg}</FormHelperText>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleRegister}>Register</Button>
<Button onClick={handleClose}>Cancel</Button>
</DialogActions>
</Dialog>
</div>
);
}

これまで同様「pages/assets.tsx」に追加していきます。最終的なソースコード全文は以下になります。

import React, { useEffect, useState } from 'react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import axios from "axios";
import { IApp, IChartData, IAssetData } from '../types/assets';
import styles from '../styles/assets.module.css';
import BasicTabs from '../components/Tabs/Tabs';
import Button from '@mui/material/Button';
import RegisterAssetDialog from '../components/Dialog/RegisterAssetDialog';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const borderColorList: string[] = ['rgb(255, 99, 132)', 'rgb(53, 162, 235)']
const backgroundColorList: string[] = ['rgba(255, 99, 132, 0.5)', 'rgba(53, 162, 235, 0.5)']
export const options = {
responsive: true,
plugins: {
legend: {
position: 'top' as const,
},
title: {
display: true,
text: '資産残高',
},
},
};
export const Assets = () => {
// アプリ一覧
const [apps, setApps] = useState<Array<IApp>>();
// グラフ用データ
const [chartData, setChartData] = useState<Array<IChartData>>([]);
// グラフ用ラベルデータ
const [labels, setLabels] = useState<Array<IChartData>>([]);
// テーブル表示様データ
const [allData, setAllData] = useState<Array<IAssetData>>();
// 更新用
const [update, setUpdate] = useState(false);
// ダイアログのOpen/Close制御用
const [open, setOpen] = useState(false);
// グラフ表示用データ作成関数
const createChartData = (appNames:Array<IApp>) => {
console.log(appNames);
let temp: IChartData[] = [];
appNames.map((e, i) => {
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/asset-amount/?target=${e.name}`)
.then((response) => {
const value: IChartData = {
label: e.name,
data: response.data.data,
borderColor: borderColorList[i],
backgroundColor: backgroundColorList[i],
}
temp.push(value);
if (temp.length === appNames.length){
setChartData(temp);
}
});
});
}
useEffect(() => {
// アプリ一覧取得
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/appmaster/`)
.then((response) => {
setApps(response.data);
createChartData(response.data);
});
// ラベルデータ取得
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/asset-date/`)
.then((response) => {
setLabels(response.data.data)
});
// テーブル表示用データ取得
axios
.get(`${process.env.NEXT_PUBLIC_BACKENDURL}/api/asset-management/v1/assetlist/`)
.then((response) => {
setAllData(response.data);
});
}, [update]);
// 更新用関数。データが更新された際にAPIの再呼び出しを行う
const handleUpdate = () => {
setUpdate(!update);
}
// ダイアログのオープン用関数
const handleClickOpen = () => {
setOpen(true);
};
const data = {
labels,
datasets: chartData,
};
return (
<div className={styles.content_wrapper}>
<div className={styles.chart}>
<Line options={options} data={data} />
</div>
<div className={styles.table}>
<Button variant="outlined" onClick={handleClickOpen}>
新規データ登録
</Button>
{(apps !== undefined) && (allData !== undefined) && <BasicTabs apps={apps} allData={allData} handleUpdate={handleUpdate} /> }
</div>
{(apps !== undefined) && <RegisterAssetDialog open={open} setOpen={setOpen} apps={apps} handleUpdate={handleUpdate} /> }
</div>
);
}
export default Assets;

下記のように表示されていればOKです。
20221123150300
20221123150500

全ファイルは下記に格納してあります。
https://github.com/hachi0088/AssetManagement/tree/v1.0.0