レビュー

dev-frontend-stock
akito.nishiwaki 5 months ago
commit ca789ae18c
  1. 1
      frontend/src/App.tsx
  2. 3
      frontend/src/components/Layout.tsx
  3. 272
      frontend/src/pages/StockPage.tsx
  4. 87
      frontend/src/pages/TaskListPage.tsx
  5. 100
      frontend/src/services/api.ts
  6. 22
      frontend/src/types/types.ts

@ -93,6 +93,7 @@ const App: React.FC = () => {
</PrivateRoute>
}
/>
</Route>
<Route path="/" element={<Layout />}>
{/* ルートパスへのアクセスはタスク一覧にリダイレクト */}

@ -22,6 +22,7 @@ import {
Menu as MenuIcon,
ListAlt as ListAltIcon,
Inventory as InventoryIcon, // テストページ用のアイコン
Science as ScienceIcon, // 鈴木
} from '@mui/icons-material';
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
@ -100,6 +101,8 @@ const Layout: React.FC = () => {
<ListItemText primary="タスク一覧" />
</ListItemButton>
{/* テストページへのリンクを追加 */}
{/* 在庫リストへのリンクを追加 */}
<ListItemButton
onClick={() => handleNavigate('/stock')}
selected={isSelected('/stock')}

@ -2,15 +2,273 @@
*
*
*/
import React from 'react';
import { Box } from '@mui/material';
import React, { useState, useEffect } from 'react';
import { stockApi } from '../services/api';
import { Stock } from '../types/types';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from "@mui/material";
import {
Container,
Typography,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Button,
Box,
} from '@mui/material';
import { TASK_ERRORS } from '../constants/errorMessages';
import CategoryDropDown from "../components/CategoryDropDown";
const StockPage: React.FC = () => {
return (
<Box>
{/* 白紙のページ - 何も表示しない */}
</Box>
);
const [stocks, setStocks] = useState<Stock[]>([]);
// 在庫編集ダイアログの表示状態
const [openDialog, setOpenDialog] = useState(false);
// コンポーネントマウント時にタスク一覧を取得
useEffect(() => {
fetchStocks();
}, []);
/**
* APIからタスク一覧を取得する関数
* state(tasks)
*/
const fetchStocks = async () => {
try {
const stocks = await stockApi.getStocks();
setStocks(stocks.stock_array);
} catch (error) {
console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error);
}
};
/**
* (ISO 8601)yyyy/MM/ddに変換する関数
*/
const formatDate = (isoString: string): string => {
const date = new Date(isoString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0"); // 月は0始まりなので+1
const day = date.getDate().toString().padStart(2, "0");
return `${year}/${month}/${day}`;
};
/* Dateyyyy/MM/dd
const formatDate = (date: Date): string => {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0"); // 月は0始まりなので+1
const day = date.getDate().toString().padStart(2, "0");
return `${year}/${month}/${day}`;
};
*/
return (
<Container>
<Typography variant="h4" component="h1" gutterBottom>
</Typography>
{/* タスク編集ボタン */}
<Button color="success"
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2, left: '80%' }}
>
</Button>
{/* タスク削除ボタン */}
<Button color="error"
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2, left: '82%' }}
>
</Button>
<Typography variant="h4" component="h1" gutterBottom>
</Typography>
{/* 乳製品一覧表示エリア - 青い背景のコンテナ */}
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{/* 要素が無かったら表示しない */}
{stocks.filter(stock => stock.category === "乳製品").length === 0 ? null : (
<TableContainer component={Paper}>
<Table>
<TableHead sx={{ backgroundColor: "#dcdcdc", color: "#333" }}>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{stocks
.filter(stock => stock.category == "乳製品") // 乳製品だけ抽出
.map(stock => (
<TableRow key={stock.stock_id}
>
<TableCell>{stock.stuff_name}</TableCell>
<TableCell>{stock.amount}</TableCell>
<TableCell>{stock.price}</TableCell>
<TableCell>{formatDate(stock.exp_date)}</TableCell>
<TableCell>{formatDate(stock.buy_date)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
<Typography variant="h4" component="h1" gutterBottom>
</Typography>
{/* 肉・魚一覧表示エリア - 青い背景のコンテナ */}
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{stocks.filter(stock => stock.category == "肉" || stock.category == "魚").length === 0 ? null : (
<TableContainer component={Paper}>
<Table>
<TableHead sx={{ backgroundColor: "#dcdcdc", color: "#333" }}>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{stocks
.filter(stock => stock.category == "肉" || stock.category == "魚") // 肉と魚だけ抽出
.map(stock => (
<TableRow key={stock.stock_id}>
<TableCell>{stock.stuff_name}</TableCell>
<TableCell>{stock.amount}</TableCell>
<TableCell>{stock.price}</TableCell>
<TableCell>{formatDate(stock.exp_date)}</TableCell>
<TableCell>{formatDate(stock.buy_date)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
<Typography variant="h4" component="h1" gutterBottom>
</Typography>
{/* 野菜一覧表示エリア - 青い背景のコンテナ */}
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{stocks.filter(stock => stock.category === "野菜").length === 0 ? null : (
<TableContainer component={Paper}>
<Table>
<TableHead sx={{ backgroundColor: "#dcdcdc", color: "#333" }}>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{stocks
.filter(stock => stock.category == "野菜") // 野菜だけ抽出
.map(stock => (
<TableRow key={stock.stock_id}>
<TableCell>{stock.stuff_name}</TableCell>
<TableCell>{stock.amount}</TableCell>
<TableCell>{stock.price}</TableCell>
<TableCell>{formatDate(stock.exp_date)}</TableCell>
<TableCell>{formatDate(stock.buy_date)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
<Typography variant="h4" component="h1" gutterBottom>
調
</Typography>
{/* 調味料一覧表示エリア - 青い背景のコンテナ */}
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{stocks.filter(stock => stock.category === "調味料").length === 0 ? null : (
<TableContainer component={Paper}>
<Table>
<TableHead sx={{ backgroundColor: "#dcdcdc", color: "#333" }}>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{stocks
.filter(stock => stock.category == "調味料") // 調味料だけ抽出
.map(stock => (
<TableRow key={stock.stock_id}>
<TableCell>{stock.stuff_name}</TableCell>
<TableCell>{stock.amount}</TableCell>
<TableCell>{stock.price}</TableCell>
<TableCell>{formatDate(stock.exp_date)}</TableCell>
<TableCell>{formatDate(stock.buy_date)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
<Typography variant="h4" component="h1" gutterBottom>
</Typography>
{/* その他一覧表示エリア - 青い背景のコンテナ */}
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{stocks.filter(stock => stock.category === "その他").length === 0 ? null : (
<TableContainer component={Paper}>
<Table>
<TableHead sx={{ backgroundColor: "#dcdcdc", color: "#333" }}>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{stocks
.filter(stock => stock.category == "その他") // その他だけ抽出
.map(stock => (
<TableRow key={stock.stock_id}>
<TableCell>{stock.stuff_name}</TableCell>
<TableCell>{stock.amount}</TableCell>
<TableCell>{stock.price}</TableCell>
<TableCell>{formatDate(stock.exp_date)}</TableCell>
<TableCell>{formatDate(stock.buy_date)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
</Container>
);
};
export default StockPage;

@ -3,7 +3,7 @@
*
*/
import React, { useState, useEffect } from 'react';
import { toBuyApi } from '../services/api';
import { toBuyApi, stuffApi } from '../services/api';
import {
Container,
Typography,
@ -23,13 +23,17 @@ import {
Button,
Box,
FormControlLabel,
FormGroup
FormGroup,
FormControl,
InputLabel,
Select,
MenuItem
} from '@mui/material';
import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon,
SoupKitchen as SoupKitchenIcon
} from '@mui/icons-material';
import { Task, ToBuy } from '../types/types';
import { Task, ToBuy, Stuff } from '../types/types';
import { TASK_ERRORS } from '../constants/errorMessages';
//import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留
import CategoryDropDown from "../components/CategoryDropDown";
@ -38,13 +42,13 @@ import CategoryDropDown from "../components/CategoryDropDown";
// 新規タスクの初期状態
const EMPTY_TASK: Omit<ToBuy, 'tobuy_id'> & {category: string} & {newAddition: boolean} = {
stuff_id: 0,
const EMPTY_TASK: Omit<ToBuy, 'tobuy_id' | 'stuff_id'> & { stuff_id: number | null, category: string } & { newAddition: boolean } = {
stuff_id: null,
stuff_name: '',
amount: 0,
shop: '',
category: '',
newAddition: false
newAddition: false,
}
const TaskListPage: React.FC = () => {
@ -52,8 +56,7 @@ const TaskListPage: React.FC = () => {
const [tobuys, setToBuys] = useState<ToBuy[]>([]);
// 新規タスク作成ダイアログの表示状態
const [openDialog, setOpenDialog] = useState(false);
// 新規タスクの入力内容
const [newTask, setNewTask] = useState(EMPTY_TASK);
//在庫登録ダイアログの表示状態
const [openInfoDialog, setOpenInfoDialog] = useState(false);
@ -61,6 +64,9 @@ const TaskListPage: React.FC = () => {
const [newToBuy, setNewToBuy] = useState(EMPTY_TASK);
const [stuffs, setStuffs] = useState<Stuff[]>([]);
// コンポーネントマウント時にタスク一覧を取得
useEffect(() => {
fetchTasks();
@ -79,6 +85,12 @@ const TaskListPage: React.FC = () => {
}
};
const onChangeCategory = async (category: string) => {
setNewToBuy({ ...newToBuy, category })
const result = await stuffApi.getStuffs(category)
setStuffs(result.stuff_array)
}
// /**
// * タスクの完了状態を切り替えるハンドラー
// * 対象タスクの完了状態を反転させてAPIに更新を要求
@ -225,22 +237,53 @@ const TaskListPage: React.FC = () => {
{/* 新規タスク作成ダイアログ */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true}>
<Box display="flex" alignItems="center">
<DialogTitle sx={{ flexGrow: 1 }}></DialogTitle>
<FormGroup row>
<FormControlLabel
control={<Checkbox />}
label="食材を新規追加"
checked={newTask.newAddition}
/>
</FormGroup>
<Box display="flex" alignItems="center">
<DialogTitle sx={{ flexGrow: 1 }}></DialogTitle>
<FormGroup row>
<FormControlLabel
control={<Checkbox />}
label="食材を新規追加"
checked={newToBuy.newAddition}
onChange={(e) => setNewToBuy({ ...newToBuy, newAddition: (e.target as HTMLInputElement).checked })}
/>
</FormGroup>
</Box>
<DialogContent>
<Box sx={{ pt: 1 }}>
{/*材料カテゴリ選択 */}
<CategoryDropDown></CategoryDropDown>
<FormControl sx={{ width: "50%", marginBottom: 2 }}>
<InputLabel id="demo-simple-select-label"></InputLabel>
<Select
labelId="demo-simple-select-label"
value={newToBuy.category}
onChange={(e) => onChangeCategory(e.target.value) }
>
<MenuItem value="乳製品"></MenuItem>
<MenuItem value="肉・魚"></MenuItem>
<MenuItem value="野菜"></MenuItem>
<MenuItem value="調味料">調</MenuItem>
<MenuItem value="その他"></MenuItem>
</Select>
</FormControl>
{!newToBuy.newAddition && <FormControl sx={{ width: "100%", marginBottom: 2 }}>
<InputLabel id="demo-simple-select-label"></InputLabel>
<Select
labelId="demo-simple-select-label"
value={newToBuy.stuff_id}
onChange={(e) => setNewToBuy({ ...newToBuy, stuff_id: Number(e.target.value) })}
>
{stuffs.map((stuff) => (
<MenuItem key={stuff.stuff_id} value={stuff.stuff_id}>
{stuff.stuff_name}
</MenuItem>
))}
</Select>
</FormControl>}
{/* タスクタイトル入力フィールド */}
<TextField
{newToBuy.newAddition && <TextField
autoFocus
margin="dense"
label="材料名"
@ -248,7 +291,7 @@ const TaskListPage: React.FC = () => {
value={newToBuy.stuff_name}
onChange={(e) => setNewToBuy({ ...newToBuy, stuff_name: e.target.value })}
sx={{ marginBottom: 2 }}
/>
/>}
{/* 数量入力フィールド */}
<TextField
margin="dense"
@ -313,14 +356,14 @@ const TaskListPage: React.FC = () => {
setOpenInfoDialog(false)
setNewToBuy(EMPTY_TASK); // 入力内容をリセット
}
}}
}}
variant="contained">
</Button>
</DialogActions>
</Dialog>
</Container>
);
};

@ -3,7 +3,7 @@
* APIとの通信を担当するモジュール
*
*/
import { LoginCredentials, RegisterCredentials, AuthResponse, Task, ToBuy } from '../types/types';
import { LoginCredentials, RegisterCredentials, AuthResponse, Task, ToBuy, Stuff, Stock } from '../types/types';
import { AUTH_ERRORS, TASK_ERRORS } from '../constants/errorMessages';
// APIのベースURL - 環境変数から取得するか、デフォルト値を使用
@ -89,8 +89,8 @@ export const authApi = {
*/
export const toBuyApi = {
/**
*
* @returns
*
* @returns
*/
getToBuys: async (): Promise<{ "tobuy_array": ToBuy[] }> => {
// const response = await fetch(`${API_BASE_URL}/api/tobuy/get`, {
@ -128,7 +128,7 @@ export const toBuyApi = {
* @param tobuy
* @returns
*/
addToBuy: async (tobuy: Omit<ToBuy, 'tobuy_id'> & { category: string }): Promise<any> => {
addToBuy: async (tobuy: Omit<ToBuy, 'stuff_id' | 'tobuy_id'> & { stuff_id: number | null, category: string }): Promise<any> => {
// const response = await fetch(`${API_BASE_URL}/api/tasks`, {
// method: 'POST',
// headers: getHeaders(),
@ -171,6 +171,96 @@ export const toBuyApi = {
},
}
export const stuffApi = {
getStuffs: async (category: string): Promise<{ stuff_array: Stuff[] }> => {
const data = [
{ "stuff_id": 1, "stuff_name": "牛乳", "category": "乳製品" },
{ "stuff_id": 2, "stuff_name": "ヨーグルト", "category": "乳製品" },
{ "stuff_id": 3, "stuff_name": "チーズ", "category": "乳製品" },
{ "stuff_id": 4, "stuff_name": "バター", "category": "乳製品" },
{ "stuff_id": 5, "stuff_name": "生クリーム", "category": "乳製品" },
{ "stuff_id": 6, "stuff_name": "鮭", "category": "魚・肉" },
{ "stuff_id": 7, "stuff_name": "鶏むね肉", "category": "魚・肉" },
{ "stuff_id": 8, "stuff_name": "豚バラ肉", "category": "魚・肉" },
{ "stuff_id": 9, "stuff_name": "牛ひき肉", "category": "魚・肉" },
{ "stuff_id": 10, "stuff_name": "まぐろ", "category": "魚・肉" },
{ "stuff_id": 11, "stuff_name": "にんじん", "category": "野菜" },
{ "stuff_id": 12, "stuff_name": "キャベツ", "category": "野菜" },
{ "stuff_id": 13, "stuff_name": "ほうれん草", "category": "野菜" },
{ "stuff_id": 14, "stuff_name": "玉ねぎ", "category": "野菜" },
{ "stuff_id": 15, "stuff_name": "ピーマン", "category": "野菜" },
{ "stuff_id": 16, "stuff_name": "醤油", "category": "調味料" },
{ "stuff_id": 17, "stuff_name": "味噌", "category": "調味料" },
{ "stuff_id": 18, "stuff_name": "塩", "category": "調味料" },
{ "stuff_id": 19, "stuff_name": "砂糖", "category": "調味料" },
{ "stuff_id": 20, "stuff_name": "酢", "category": "調味料" },
{ "stuff_id": 21, "stuff_name": "米", "category": "その他" },
{ "stuff_id": 22, "stuff_name": "パスタ", "category": "その他" },
{ "stuff_id": 23, "stuff_name": "小麦粉", "category": "その他" },
{ "stuff_id": 24, "stuff_name": "卵", "category": "その他" },
{ "stuff_id": 25, "stuff_name": "豆腐", "category": "その他" }
]
const filtered = data.filter(stuff => stuff.category == category)
return {
"stuff_array": filtered
}
}
}
export const stockApi = {
/**
*
* @returns
*/
getStocks: async (): Promise<{ "stock_array": Stock[] }> => {
// const response = await fetch(`${API_BASE_URL}/api/tobuy/get`, {
// headers: getHeaders(), // 認証トークンを含むヘッダー
// });
// if (!response.ok) {
// throw new Error(TASK_ERRORS.FETCH_FAILED);
// }
// return response.json();
return {
"stock_array": [
{
"stock_id": 1,
"stuff_id": 10,
"stuff_name": "豚肉",
"amount": 100,
"price": 200,
"buy_date": "2025-05-18T09:00:00.000Z",
"last_update": "2025-05-18T09:00:00.000Z",
"exp_date": "2025-05-19T10:15:00.000Z",
"category": "肉"
},
{
"stock_id": 2,
"stuff_id": 1,
"stuff_name": "トマト",
"amount": 10,
"price": 200,
"buy_date": "2025-05-18T09:00:00.000Z",
"last_update": "2025-05-18T09:00:00.000Z",
"exp_date": "2025-05-19T10:15:00.000Z",
"category": "野菜"
}
]
}
},
}
/**
* ()
@ -216,7 +306,7 @@ export const taskApi = {
* @param task ,
* @returns
*/
addStuff: async (task: Omit<Task, 'userId'|'createdAt'|'price'|'buyDate'|'expirationDate'|'newAddition' >): Promise<Task> => {
addStuff: async (task: Omit<Task, 'userId' | 'createdAt' | 'price' | 'buyDate' | 'expirationDate' | 'newAddition'>): Promise<Task> => {
const response = await fetch(`${API_BASE_URL}/api/tubuy/add`, {
method: 'POST',
headers: getHeaders(),

@ -29,6 +29,28 @@ export interface ToBuy {
shop?: string,
}
export interface Stuff {
stuff_id: number,
stuff_name: string,
category: string,
}
/**
*
*
*/
export interface Stock {
stock_id: number,
stuff_id: number,
stuff_name: string,
amount: number,
price: number,
buy_date: string,
last_update: string,
exp_date: string,
category: string,
}
/**
*
*

Loading…
Cancel
Save