diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7141e87..d4d6caf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import Layout from './components/Layout'; import LoginPage from './pages/LoginPage'; import RegisterPage from './pages/RegisterPage'; import TaskListPage from './pages/TaskListPage'; +import StockPage from './pages/StockPage'; import './App.css'; /** @@ -93,6 +94,30 @@ const App: React.FC = () => { } /> + }> + {/* ルートパスへのアクセスはタスク一覧にリダイレクト */} + } /> + + {/* タスク一覧は認証が必要なため、PrivateRouteでラップ */} + + + + } + /> + + {/* テストページへのルートを追加 */} + + + + } + /> + diff --git a/frontend/src/components/CategoryDropDown.tsx b/frontend/src/components/CategoryDropDown.tsx new file mode 100644 index 0000000..fce3e55 --- /dev/null +++ b/frontend/src/components/CategoryDropDown.tsx @@ -0,0 +1,28 @@ +import React, { useState } from "react"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; + +const CategoryDropDown = () => { + const [selectedValue, setSelectedValue] = useState(""); // 選択された値の状態管理 + + return ( + + カテゴリ + + + ); +}; + +export default CategoryDropDown; \ No newline at end of file diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index e8696a9..eb3612f 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -3,13 +3,13 @@ * ヘッダー(AppBar)とメインコンテンツ領域を含む基本的なページ構造を定義 */ import React, { useState } from 'react'; -import { - AppBar, - Toolbar, - Typography, - Container, - Box, - Button, +import { + AppBar, + Toolbar, + Typography, + Container, + Box, + Button, Drawer, List, ListItemText, @@ -18,9 +18,10 @@ import { Divider, IconButton } from '@mui/material'; -import { +import { Menu as MenuIcon, ListAlt as ListAltIcon, + Inventory as InventoryIcon, // テストページ用のアイコン } from '@mui/icons-material'; import { useNavigate, Outlet, useLocation } from 'react-router-dom'; @@ -91,13 +92,21 @@ const Layout: React.FC = () => { role="presentation" > - handleNavigate('/tasks')} + handleNavigate('/tasks')} selected={isSelected('/tasks')} > + {/* テストページへのリンクを追加 */} + handleNavigate('/stock')} + selected={isSelected('/stock')} + > + + + diff --git a/frontend/src/pages/StockPage.tsx b/frontend/src/pages/StockPage.tsx new file mode 100644 index 0000000..8de8bd9 --- /dev/null +++ b/frontend/src/pages/StockPage.tsx @@ -0,0 +1,16 @@ +/** + * テストページコンポーネント + * 白紙の状態で表示されるテスト用のページ + */ +import React from 'react'; +import { Box } from '@mui/material'; + +const StockPage: React.FC = () => { + return ( + + {/* 白紙のページ - 何も表示しない */} + + ); +}; + +export default StockPage; \ No newline at end of file diff --git a/frontend/src/pages/TaskListPage.tsx b/frontend/src/pages/TaskListPage.tsx index c537f37..e44bc7b 100644 --- a/frontend/src/pages/TaskListPage.tsx +++ b/frontend/src/pages/TaskListPage.tsx @@ -3,10 +3,11 @@ * タスクの表示、作成、完了状態の切り替え、削除などの機能を提供 */ import React, { useState, useEffect } from 'react'; -import { taskApi } from '../services/api'; +import { toBuyApi, stuffApi } from '../services/api'; import { Container, Typography, + Tooltip, List, ListItem, ListItemText, @@ -21,21 +22,51 @@ import { TextField, Button, Box, + FormControlLabel, + FormGroup, + FormControl, + InputLabel, + Select, + MenuItem } from '@mui/material'; -import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material'; -import { Task } from '../types/types'; +import { + Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon, + SoupKitchen as SoupKitchenIcon +} from '@mui/icons-material'; +import { Task, ToBuy, Stuff } from '../types/types'; import { TASK_ERRORS } from '../constants/errorMessages'; +//import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留 +import CategoryDropDown from "../components/CategoryDropDown"; + + + // 新規タスクの初期状態 -const EMPTY_TASK = { title: '', description: '', completed: false }; +const EMPTY_TASK: Omit & { stuff_id: number | null, category: string } & { newAddition: boolean } = { + stuff_id: null, + stuff_name: '', + amount: 0, + shop: '', + category: '', + newAddition: false, +} const TaskListPage: React.FC = () => { // タスク一覧の状態管理 - const [tasks, setTasks] = useState([]); + const [tobuys, setToBuys] = useState([]); // 新規タスク作成ダイアログの表示状態 const [openDialog, setOpenDialog] = useState(false); - // 新規タスクの入力内容 - const [newTask, setNewTask] = useState(EMPTY_TASK); + + //在庫登録ダイアログの表示状態 + const [openInfoDialog, setOpenInfoDialog] = useState(false); + + const [selectedTask, setSelectedTask] = useState(); + + + const [newToBuy, setNewToBuy] = useState(EMPTY_TASK); + + const [stuffs, setStuffs] = useState([]); + // コンポーネントマウント時にタスク一覧を取得 useEffect(() => { @@ -48,36 +79,42 @@ const TaskListPage: React.FC = () => { */ const fetchTasks = async () => { try { - const tasks = await taskApi.getTasks(); - setTasks(tasks); + const tobuys = await toBuyApi.getToBuys(); + setToBuys(tobuys.tobuy_array); } catch (error) { console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error); } }; - /** - * タスクの完了状態を切り替えるハンドラー - * 対象タスクの完了状態を反転させてAPIに更新を要求 - */ - const handleToggleComplete = async (taskId: number) => { - try { - const task = tasks.find(t => t.id === taskId); - if (!task) return; + const onChangeCategory = async (category: string) => { + setNewToBuy({ ...newToBuy, category }) + const result = await stuffApi.getStuffs(category) + setStuffs(result.stuff_array) + } - await taskApi.updateTask(taskId, { ...task, completed: !task.completed }); - fetchTasks(); // 更新後のタスク一覧を再取得 - } catch (error) { - console.error(`${TASK_ERRORS.UPDATE_FAILED}:`, error); - } - }; + // /** + // * タスクの完了状態を切り替えるハンドラー + // * 対象タスクの完了状態を反転させてAPIに更新を要求 + // */ + // const handleToggleComplete = async (taskId: number) => { + // try { + // const task = tasks.find(t => t.id === taskId); + // if (!task) return; + + // await toBuyApi.updateTask(taskId, { ...task, completed: !task.completed }); + // fetchTasks(); // 更新後のタスク一覧を再取得 + // } catch (error) { + // console.error(`${TASK_ERRORS.UPDATE_FAILED}:`, error); + // } + // }; /** * タスクを削除するハンドラー * 指定されたIDのタスクをAPIを通じて削除 */ - const handleDeleteTask = async (taskId: number) => { + const handleDeleteTask = async (toBuyId: number) => { try { - await taskApi.deleteTask(taskId); + await toBuyApi.deleteToBuy(toBuyId); fetchTasks(); // 削除後のタスク一覧を再取得 } catch (error) { console.error(`${TASK_ERRORS.DELETE_FAILED}:`, error); @@ -91,9 +128,9 @@ const TaskListPage: React.FC = () => { */ const handleCreateTask = async () => { try { - await taskApi.createTask(newTask); + await toBuyApi.addToBuy(newToBuy); setOpenDialog(false); // ダイアログを閉じる - setNewTask(EMPTY_TASK); // 入力内容をリセット + setNewToBuy(EMPTY_TASK); // 入力内容をリセット fetchTasks(); // 作成後のタスク一覧を再取得 } catch (error) { console.error(`${TASK_ERRORS.CREATE_FAILED}:`, error); @@ -106,12 +143,12 @@ const TaskListPage: React.FC = () => { タスク一覧 {/* タスク一覧表示エリア - 青い背景のコンテナ */} -
+
{/* タスク一覧をマップして各タスクをリストアイテムとして表示 */} - {tasks.map((task) => ( + {tobuys.map((tobuy) => ( { }} > {/* タスク完了状態を切り替えるチェックボックス */} + {/*} handleToggleComplete(task.id)} /> + */} {/* タスクのタイトルと説明 - 完了状態に応じて取り消し線を表示 */} - {/* タスク削除ボタン */} + {/* 買い物リスト:食材情報記入ボタン */} - handleDeleteTask(task.id)} + + setOpenInfoDialog(true)} + //onClick={() => handleDeleteTask(task.id)} + > + + + + {/* 買い物リスト:食材削除ボタン */} + - - + + handleDeleteTask(tobuy.tobuy_id)} + > + + + + ))}
- {/* 新規タスク作成ボタン - 画面下部に固定表示 */} - setOpenDialog(true)} - > - - + {/* 新規材料作成ボタン - 画面下部に固定表示 */} + + setOpenDialog(true)} + > + + + + {/*新規料理追加ボタン - 画面下部に固定表示 */} + + setOpenDialog(true)} + > + + + {/* 新規タスク作成ダイアログ */} setOpenDialog(false)} disableScrollLock={true}> - 新規タスク + + 材料の追加 + + } + label="食材を新規追加" + checked={newToBuy.newAddition} + onChange={(e) => setNewToBuy({ ...newToBuy, newAddition: (e.target as HTMLInputElement).checked })} + /> + + + {/*材料カテゴリ選択 */} + + + カテゴリ + + + + {!newToBuy.newAddition && + 材料名(選択) + + } + {/* タスクタイトル入力フィールド */} + {newToBuy.newAddition && setNewToBuy({ ...newToBuy, stuff_name: e.target.value })} + sx={{ marginBottom: 2 }} + />} + {/* 数量入力フィールド */} + { + const value = e.target.value; + const parsedValue = parseInt(value, 10); // 数値に変換 + if (!isNaN(parsedValue)) { + setNewToBuy({ ...newToBuy, amount: parsedValue }); // number型で保存 + } + }} + sx={{ width: "20%" }} + type="number" + inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} // ここで整数のみ許可 + + /> + + + + + + + + + {/*在庫登録のための数値入力ダイアログ */} + setOpenInfoDialog(false)} disableScrollLock={true}> + 在庫登録 + + + {/* 価格入力フィールド */} + {/* 消費・賞味期限入力フィールド */} + setNewTask({ ...newTask, title: e.target.value })} + multiline /> - {/* タスク説明入力フィールド - 複数行入力可能 */} + {/* 購入日入力フィールド */} setNewTask({ ...newTask, description: e.target.value })} /> - - + + ); }; diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index d963bbe..a77a601 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -3,7 +3,7 @@ * バックエンドAPIとの通信を担当するモジュール * 認証、タスク管理などの機能を提供 */ -import { LoginCredentials, RegisterCredentials, AuthResponse, Task } from '../types/types'; +import { LoginCredentials, RegisterCredentials, AuthResponse, Task, ToBuy, Stuff } from '../types/types'; import { AUTH_ERRORS, TASK_ERRORS } from '../constants/errorMessages'; // APIのベースURL - 環境変数から取得するか、デフォルト値を使用 @@ -82,7 +82,140 @@ export const authApi = { }, }; + /** + * 買うものリスト管理関連のAPI機能を提供するオブジェクト + * 買うものリストの取得、作成、更新、削除などの機能を含む + */ +export const toBuyApi = { + /** + * 全タスクを取得 + * @returns タスク一覧 + */ + getToBuys: async (): Promise<{ "tobuy_array": ToBuy[] }> => { + // 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 { + "tobuy_array": [ + { + "tobuy_id": 1, + "stuff_id": 2, + "stuff_name": "じゃがいも", + "amount": 3, + "shop": "shopXXX" + }, + { + "tobuy_id": 2, + "stuff_id": 5, + "stuff_name": "にんじん", + "amount": 1 + } + ] + } + + }, + + /** + * 買うものリストへの材料追加 + * @param tobuy 作成する材料情報 + * @returns 作成された材料情報 + */ + addToBuy: async (tobuy: Omit & { stuff_id: number | null, category: string }): Promise => { + // const response = await fetch(`${API_BASE_URL}/api/tasks`, { + // method: 'POST', + // headers: getHeaders(), + // body: JSON.stringify(tobuy), + // }); + + // if (!response.ok) { + // throw new Error(TASK_ERRORS.CREATE_FAILED); + // } + + // return response.json(); + + return { + "result": true, + "tobuy_id": 1, + "stuff_id": 6, + "message": "追加に成功しました", + } + + }, + + /** + * 買うものリストを削除 + * @param id 削除対象の買うものリストID + */ + deleteToBuy: async (tobuy_id: number): Promise<{ result: boolean }> => { + // const response = await fetch(`${API_BASE_URL}/api/tobuy/delete`, { + // method: 'DELETE', + // headers: getHeaders(), + // body: JSON.stringify({tobuy_id}), + // }); + + // if (!response.ok) { + // throw new Error(TASK_ERRORS.DELETE_FAILED); + // } + + return { + "result": true + } + }, +} + +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 + } + } +} + + +/** + * (サンプル,実際には不要) * タスク管理関連のAPI機能を提供するオブジェクト * タスクの取得、作成、更新、削除などの機能を含む */ @@ -121,12 +254,12 @@ export const taskApi = { }, /** - * 新規タスクを作成 - * @param task 作成するタスク情報(ID、ユーザーID、作成日時、更新日時は除外) + * 新規材料を作成 + * @param task 作成するタスク情報(価格,作成日時、更新日時は除外) * @returns 作成されたタスク情報 */ - createTask: async (task: Omit): Promise => { - const response = await fetch(`${API_BASE_URL}/api/tasks`, { + addStuff: async (task: Omit): Promise => { + const response = await fetch(`${API_BASE_URL}/api/tubuy/add`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(task), diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index d3b1186..8d6a10c 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -4,12 +4,35 @@ */ export interface Task { id: number; // タスクの一意識別子 - title: string; // タスクのタイトル - description?: string; // タスクの詳細説明(任意) + stuff_name: string; // タスクのタイトル + amount: number; //材料の数量 + price: number; //材料の値段 + buyDate:Date; //購入日時 + expirationDate: Date; //賞味・消費期限 + //description?: string; // タスクの詳細説明(任意) completed: boolean; // タスクの完了状態 userId: number; // タスクの所有者ID createdAt: string; // タスク作成日時 - updatedAt: string; // タスク更新日時 + newAddition: boolean //材料を新規追加するかどうか + //updatedAt: string; // タスク更新日時 +} + +/** + * タスク情報を表す型定義 + * タスクの基本情報と状態を管理 + */ +export interface ToBuy { + tobuy_id: number, + stuff_id: number, + stuff_name: string, + amount: number, + shop?: string, +} + +export interface Stuff { + stuff_id: number, + stuff_name: string, + category: string, } /** diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dbdf2f4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "joint_exc", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "peerDependencies": { + "react": "*" + } + } + } +}