Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

予算管理ページの作成(フロント) #894

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
46 changes: 46 additions & 0 deletions view/next-project/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions view/next-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Department[]>([]);
const [divisions, setDivisions] = useState<Division[]>([]);
const [items, setItems] = useState<Item[]>([]);

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[] = [];
Copy link
Preview

Copilot AI Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of 'any' type for 'displayItems' can lead to potential type safety issues. Consider using a more specific type.

Suggested change
let displayItems: any[] = [];
let displayItems: (Department | Division | Item)[] = [];

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
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<HTMLSelectElement>) => {
const deptId = e.target.value ? parseInt(e.target.value, 10) : null;
setQueryState({ departmentId: deptId, divisionId: null });
};

const handleDivisionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
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 (
<Card>
<div className='px-4 py-10'>
<div className='flex-start mb-4 flex'>
<Title>予算管理ページ</Title>
</div>
<div className='mb-4 flex flex-col items-center md:flex-row md:justify-between'>
<div className='flex flex-col gap-4 text-nowrap py-2'>
<div className='flex gap-3'>
<span className='text-base font-light'>申請する局</span>
<select
value={departmentId ?? ''}
onChange={handleDepartmentChange}
className='border-b border-black-300 focus:outline-none'
>
<option value=''>ALL</option>
{departments.map((dept) => (
<option key={dept.id} value={dept.id}>
{dept.name}
</option>
))}
</select>
</div>
<div className={`flex gap-3 ${departmentId !== null ? 'visible' : 'invisible'}`}>
<span className='text-base font-light'>申請する部門</span>
<select
value={divisionId ?? ''}
onChange={handleDivisionChange}
className='border-b border-black-300 focus:outline-none'
>
<option value=''>ALL</option>
{divisions.map((div) => (
<option key={div.id} value={div.id}>
{div.name}
</option>
))}
</select>
</div>
</div>
<div className='mt-2 flex w-full flex-col gap-1 md:w-fit md:flex-row md:gap-3'>
<PrimaryButton className='w-full md:w-fit'>CSVダウンロード</PrimaryButton>
<AddButton className='w-full md:w-fit'>{title}登録</AddButton>
</div>
</div>
<div className='mt-5 overflow-x-auto'>
<table className='w-full table-auto border-collapse text-nowrap'>
<thead>
<tr className='border border-x-white-0 border-b-primary-1 border-t-white-0 py-3'>
<th className='w-1/4 pb-2 text-center font-medium text-black-600'>{title}</th>
{showBudgetColumns && (
<>
<th className='w-1/4 pb-2 text-center font-medium text-black-600'>予算</th>
<th className='w-1/4 pb-2 text-center font-medium text-black-600'>使用額</th>
<th className='w-1/4 pb-2 text-center font-medium text-black-600'>残高</th>
</>
)}
</tr>
</thead>
<tbody>
{displayItems.map((item, index) => (
<tr
key={item.id}
className={`cursor-pointer ${
index !== displayItems.length - 1 ? 'border-b' : ''
}`}
onClick={() => handleRowClick(item)}
>
<td className='flex justify-center gap-2 py-3'>
<div className='text-center text-primary-1 underline'>{item.name}</div>
<EditButton />
</td>

{showBudgetColumns && (
<>
<td className='py-3 text-center'>{item.budget}</td>
<td className='py-3 text-center'>{item.used}</td>
<td className='py-3 text-center'>{item.remaining}</td>
</>
)}
</tr>
))}
{showBudgetColumns && displayItems.length > 0 && (
<tr className='border border-x-white-0 border-b-white-0 border-t-primary-1'>
<td className='py-3 text-center font-bold'>合計</td>
<td className='py-3 text-center font-bold'>{totalBudget}</td>
<td className='py-3 text-center font-bold'>{totalUsed}</td>
<td className='py-3 text-center font-bold'>{totalRemaining}</td>
</tr>
)}
{displayItems.length === 0 && (
<tr>
<td
colSpan={showBudgetColumns ? 4 : 1}
className='text-gray-500 px-4 py-6 text-center text-sm'
>
データがありません
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</Card>
);
}
100 changes: 100 additions & 0 deletions view/next-project/src/components/budget_managements/mockApi.ts
Original file line number Diff line number Diff line change
@@ -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<Department[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(departments);
});
});
};

export const fetchDivisions = async (departmentId: number): Promise<Division[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(divisions.filter((division) => division.departmentId === departmentId));
});
});
};

export const fetchItems = async (divisionId: number): Promise<Item[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(items.filter((item) => item.divisionId === divisionId));
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<button className={clsx(className)} onClick={props.onClick}>
Expand Down
Loading
Loading