diff --git a/view/next-project/package-lock.json b/view/next-project/package-lock.json index 37f3e3017..cdd36986a 100644 --- a/view/next-project/package-lock.json +++ b/view/next-project/package-lock.json @@ -25,6 +25,7 @@ "minio": "^7.1.3", "next": "^14.2.4", "node-fetch": "^3.1.0", + "nuqs": "^2.2.3", "pdf-lib": "^1.17.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -17329,6 +17330,12 @@ "node": ">=8" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -17765,6 +17772,32 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nuqs": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.2.3.tgz", + "integrity": "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA==", + "license": "MIT", + "dependencies": { + "mitt": "^3.0.1" + }, + "peerDependencies": { + "@remix-run/react": ">=2", + "next": ">=14.2.0", + "react": ">=18.2.0 || ^19.0.0-0", + "react-router-dom": ">=6" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "next": { + "optional": true + }, + "react-router-dom": { + "optional": true + } + } + }, "node_modules/nypm": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.8.tgz", @@ -35629,6 +35662,11 @@ } } }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -35945,6 +35983,14 @@ "boolbase": "^1.0.0" } }, + "nuqs": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.2.3.tgz", + "integrity": "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA==", + "requires": { + "mitt": "^3.0.1" + } + }, "nypm": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.8.tgz", diff --git a/view/next-project/package.json b/view/next-project/package.json index 9c9633146..c585607ee 100644 --- a/view/next-project/package.json +++ b/view/next-project/package.json @@ -33,6 +33,7 @@ "minio": "^7.1.3", "next": "^14.2.4", "node-fetch": "^3.1.0", + "nuqs": "^2.2.3", "pdf-lib": "^1.17.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/view/next-project/src/components/budget_managements/BudgetManagement.tsx b/view/next-project/src/components/budget_managements/BudgetManagement.tsx new file mode 100644 index 000000000..229b70bbf --- /dev/null +++ b/view/next-project/src/components/budget_managements/BudgetManagement.tsx @@ -0,0 +1,192 @@ +import { useQueryStates, parseAsInteger } from 'nuqs'; +import { useState, useEffect } from 'react'; +import { + Department, + Division, + Item, + fetchDepartments, + fetchDivisions, + fetchItems, +} from './mockApi'; +import { Card, EditButton, AddButton, Title } from '@/components/common'; +import PrimaryButton from '@/components/common/OutlinePrimaryButton/OutlinePrimaryButton'; + +export default function BudgetManagement() { + const [departments, setDepartments] = useState([]); + const [divisions, setDivisions] = useState([]); + const [items, setItems] = useState([]); + + const [{ departmentId, divisionId }, setQueryState] = useQueryStates({ + departmentId: parseAsInteger.withOptions({ history: 'push', shallow: true }), + divisionId: parseAsInteger.withOptions({ history: 'push', shallow: true }), + }); + + useEffect(() => { + fetchDepartments().then(setDepartments); + }, []); + + // FIXME: APIが実装されたら、修正する。 + useEffect(() => { + if (departmentId !== null) { + fetchDivisions(departmentId).then(setDivisions); + setItems([]); + } else { + setDivisions([]); + setQueryState({ divisionId: null }); + setItems([]); + } + }, [departmentId]); + + useEffect(() => { + if (divisionId !== null) { + fetchItems(divisionId).then(setItems); + } else { + setItems([]); + } + }, [divisionId]); + + // FIXME: any型はAPIのレスポンスに合わせて変更する。 + let displayItems: any[] = []; + let title = '購入報告'; + const showBudgetColumns = true; + + if (divisionId !== null) { + displayItems = items; + title = '申請物品'; + } else if (departmentId !== null) { + displayItems = divisions; + title = '申請部門'; + } else { + displayItems = departments; + title = '申請局'; + } + + const totalBudget = displayItems.reduce((sum, item) => sum + (item.budget || 0), 0); + const totalUsed = displayItems.reduce((sum, item) => sum + (item.used || 0), 0); + const totalRemaining = displayItems.reduce((sum, item) => sum + (item.remaining || 0), 0); + + const handleDepartmentChange = (e: React.ChangeEvent) => { + const deptId = e.target.value ? parseInt(e.target.value, 10) : null; + setQueryState({ departmentId: deptId, divisionId: null }); + }; + + const handleDivisionChange = (e: React.ChangeEvent) => { + const divId = e.target.value ? parseInt(e.target.value, 10) : null; + setQueryState({ divisionId: divId }); + }; + + // FIXME: any型はAPIのレスポンスに合わせて変更する。 + const handleRowClick = (item: any) => { + if (departmentId === null) { + setQueryState({ departmentId: item.id, divisionId: null }); + } else if (divisionId === null) { + setQueryState({ divisionId: item.id }); + } + }; + + return ( + +
+
+ 予算管理ページ +
+
+
+
+ 申請する局 + +
+
+ 申請する部門 + +
+
+
+ CSVダウンロード + {title}登録 +
+
+
+ + + + + {showBudgetColumns && ( + <> + + + + + )} + + + + {displayItems.map((item, index) => ( + handleRowClick(item)} + > + + + {showBudgetColumns && ( + <> + + + + + )} + + ))} + {showBudgetColumns && displayItems.length > 0 && ( + + + + + + + )} + {displayItems.length === 0 && ( + + + + )} + +
{title}予算使用額残高
+
{item.name}
+ +
{item.budget}{item.used}{item.remaining}
合計{totalBudget}{totalUsed}{totalRemaining}
+ データがありません +
+
+
+
+ ); +} diff --git a/view/next-project/src/components/budget_managements/mockApi.ts b/view/next-project/src/components/budget_managements/mockApi.ts new file mode 100644 index 000000000..99dcce63f --- /dev/null +++ b/view/next-project/src/components/budget_managements/mockApi.ts @@ -0,0 +1,100 @@ +export interface Department { + id: number; + name: string; + budget: number; + used: number; + remaining: number; +} + +export interface Division { + id: number; + name: string; + departmentId: number; + budget: number; + used: number; + remaining: number; +} + +export interface Item { + id: number; + name: string; + divisionId: number; + budget: number; + used: number; + remaining: number; +} + +const departments: Department[] = [ + { id: 1, name: '制作局', budget: 20000, used: 5000, remaining: 15000 }, + { id: 2, name: '渉外局', budget: 18000, used: 4000, remaining: 14000 }, + { id: 3, name: '企画局', budget: 22000, used: 6000, remaining: 16000 }, + { id: 4, name: '財務局', budget: 25000, used: 5500, remaining: 19500 }, + { id: 5, name: '情報局', budget: 21000, used: 7000, remaining: 14000 }, + { id: 6, name: '総務局', budget: 23000, used: 4500, remaining: 18500 }, +]; + +const divisions: Division[] = [ + { id: 1, name: '制作部門A', departmentId: 1, budget: 10000, used: 3000, remaining: 7000 }, + { id: 2, name: '制作部門B', departmentId: 1, budget: 10000, used: 2000, remaining: 8000 }, + { id: 3, name: '渉外部門A', departmentId: 2, budget: 9000, used: 4000, remaining: 5000 }, + { id: 4, name: '渉外部門B', departmentId: 2, budget: 9000, used: 0, remaining: 9000 }, + { id: 5, name: '企画部門A', departmentId: 3, budget: 11000, used: 5000, remaining: 6000 }, + { id: 6, name: '企画部門B', departmentId: 3, budget: 11000, used: 1000, remaining: 10000 }, + { id: 7, name: '財務部門A', departmentId: 4, budget: 12500, used: 3000, remaining: 9500 }, + { id: 8, name: '財務部門B', departmentId: 4, budget: 12500, used: 2500, remaining: 10000 }, + { id: 9, name: '情報部門A', departmentId: 5, budget: 10500, used: 4000, remaining: 6500 }, + { id: 10, name: '情報部門B', departmentId: 5, budget: 10500, used: 3000, remaining: 7500 }, + { id: 11, name: '総務部門A', departmentId: 6, budget: 11500, used: 2000, remaining: 9500 }, + { id: 12, name: '総務部門B', departmentId: 6, budget: 11500, used: 2500, remaining: 9000 }, +]; + +const items: Item[] = [ + { id: 1, name: '物品A', divisionId: 1, budget: 5000, used: 1000, remaining: 4000 }, + { id: 2, name: '物品B', divisionId: 1, budget: 5000, used: 500, remaining: 4500 }, + { id: 3, name: '物品C', divisionId: 2, budget: 5000, used: 2000, remaining: 3000 }, + { id: 4, name: '物品D', divisionId: 2, budget: 5000, used: 0, remaining: 5000 }, + { id: 5, name: '物品E', divisionId: 3, budget: 5000, used: 3000, remaining: 2000 }, + { id: 6, name: '物品F', divisionId: 3, budget: 5000, used: 500, remaining: 4500 }, + { id: 7, name: '物品G', divisionId: 4, budget: 5000, used: 2000, remaining: 3000 }, + { id: 8, name: '物品H', divisionId: 4, budget: 5000, used: 1500, remaining: 3500 }, + { id: 9, name: '物品I', divisionId: 5, budget: 5000, used: 4000, remaining: 1000 }, + { id: 10, name: '物品J', divisionId: 5, budget: 5000, used: 3000, remaining: 2000 }, + { id: 11, name: '物品K', divisionId: 6, budget: 5000, used: 1000, remaining: 4000 }, + { id: 12, name: '物品L', divisionId: 6, budget: 5000, used: 1500, remaining: 3500 }, + { id: 13, name: '物品M', divisionId: 7, budget: 5000, used: 3000, remaining: 2000 }, + { id: 14, name: '物品N', divisionId: 7, budget: 5000, used: 2000, remaining: 3000 }, + { id: 15, name: '物品O', divisionId: 8, budget: 5000, used: 1000, remaining: 4000 }, + { id: 16, name: '物品P', divisionId: 8, budget: 5000, used: 2500, remaining: 2500 }, + { id: 17, name: '物品Q', divisionId: 9, budget: 5000, used: 4000, remaining: 1000 }, + { id: 18, name: '物品R', divisionId: 9, budget: 5000, used: 3000, remaining: 2000 }, + { id: 19, name: '物品S', divisionId: 10, budget: 5000, used: 1000, remaining: 4000 }, + { id: 20, name: '物品T', divisionId: 10, budget: 5000, used: 2500, remaining: 2500 }, + { id: 21, name: '物品U', divisionId: 11, budget: 5000, used: 4000, remaining: 1000 }, + { id: 22, name: '物品V', divisionId: 11, budget: 5000, used: 3000, remaining: 2000 }, + { id: 23, name: '物品W', divisionId: 12, budget: 5000, used: 1000, remaining: 4000 }, + { id: 24, name: '物品X', divisionId: 12, budget: 5000, used: 2500, remaining: 2500 }, +]; + +export const fetchDepartments = async (): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(departments); + }); + }); +}; + +export const fetchDivisions = async (departmentId: number): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(divisions.filter((division) => division.departmentId === departmentId)); + }); + }); +}; + +export const fetchItems = async (divisionId: number): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(items.filter((item) => item.divisionId === divisionId)); + }); + }); +}; diff --git a/view/next-project/src/components/common/OutlinePrimaryButton/OutlinePrimaryButton.tsx b/view/next-project/src/components/common/OutlinePrimaryButton/OutlinePrimaryButton.tsx index d689032b1..c52294bb7 100644 --- a/view/next-project/src/components/common/OutlinePrimaryButton/OutlinePrimaryButton.tsx +++ b/view/next-project/src/components/common/OutlinePrimaryButton/OutlinePrimaryButton.tsx @@ -9,7 +9,7 @@ interface Props { function PrimaryButton(props: Props): JSX.Element { const className = - 'px-4 py-2 text-primary-1 font-bold text-md rounded-lg bg-white-0 border border-primary-1 hover:bg-white-100 hover:text-primary-2 hover:border-primary-2' + + 'flex justify-center px-4 py-2 text-primary-1 font-bold text-md rounded-lg bg-white-0 border border-primary-1 hover:bg-white-100 hover:text-primary-2 hover:border-primary-2' + (props.className ? ` ${props.className}` : ''); return (