diff --git a/api/generated/openapi_gen.go b/api/generated/openapi_gen.go index 8cbbf2dac..27ce8c809 100644 --- a/api/generated/openapi_gen.go +++ b/api/generated/openapi_gen.go @@ -292,6 +292,18 @@ type SponsorStyle struct { Style string `json:"style"` } +// Teacher defines model for teacher. +type Teacher struct { + Building string `json:"building"` + DepartmentId int `json:"department_id"` + Id int `json:"id"` + IsBlack bool `json:"is_black"` + Name string `json:"name"` + Position string `json:"position"` + Remark string `json:"remark"` + Room *string `json:"room,omitempty"` +} + // Total defines model for total. type Total struct { Balance *int `json:"balance,omitempty"` @@ -582,6 +594,9 @@ type PostTeachersParams struct { // DepartmentId 学科ID DepartmentId *int `form:"department_id,omitempty" json:"department_id,omitempty"` + // Building 棟 + Building *string `form:"building,omitempty" json:"building,omitempty"` + // Room 部屋番号 Room *string `form:"room,omitempty" json:"room,omitempty"` @@ -603,6 +618,9 @@ type PutTeachersIdParams struct { // DepartmentId 学科ID DepartmentId *int `form:"department_id,omitempty" json:"department_id,omitempty"` + // Building 棟 + Building *string `form:"building,omitempty" json:"building,omitempty"` + // Room 部屋番号 Room *string `form:"room,omitempty" json:"room,omitempty"` @@ -3063,6 +3081,13 @@ func (w *ServerInterfaceWrapper) PostTeachers(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter department_id: %s", err)) } + // ------------- Optional query parameter "building" ------------- + + err = runtime.BindQueryParameter("form", true, false, "building", ctx.QueryParams(), ¶ms.Building) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter building: %s", err)) + } + // ------------- Optional query parameter "room" ------------- err = runtime.BindQueryParameter("form", true, false, "room", ctx.QueryParams(), ¶ms.Room) @@ -3180,6 +3205,13 @@ func (w *ServerInterfaceWrapper) PutTeachersId(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter department_id: %s", err)) } + // ------------- Optional query parameter "building" ------------- + + err = runtime.BindQueryParameter("form", true, false, "building", ctx.QueryParams(), ¶ms.Building) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter building: %s", err)) + } + // ------------- Optional query parameter "room" ------------- err = runtime.BindQueryParameter("form", true, false, "room", ctx.QueryParams(), ¶ms.Room) diff --git a/view/next-project/package-lock.json b/view/next-project/package-lock.json index ec9aa58b0..558db818c 100644 --- a/view/next-project/package-lock.json +++ b/view/next-project/package-lock.json @@ -28,6 +28,7 @@ "nuqs": "^2.2.3", "pdf-lib": "^1.17.1", "react": "^18.3.1", + "react-datepicker": "^8.3.0", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hook-form": "^7.31.1", @@ -3716,6 +3717,40 @@ "@floating-ui/core": "^1.2.6" } }, + "node_modules/@floating-ui/react": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.8.tgz", + "integrity": "sha512-EQJ4Th328y2wyHR3KzOUOoTW2UKjFk53fmyahfwExnFQ8vnsMYqKc+fFPOkeYtj5tcp1DUMiNJ7BFhed7e9ONw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.9", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@fontsource/noto-sans-jp": { "version": "4.5.12", "resolved": "https://registry.npmjs.org/@fontsource/noto-sans-jp/-/noto-sans-jp-4.5.12.tgz", @@ -14750,6 +14785,40 @@ "react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-datepicker": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.3.0.tgz", + "integrity": "sha512-DhfrIJnTPJTUVRtXU7c7zooug40rD6q+Fc8UTCt19dYEotLpDQgTN98MfocY6Rc4S99oOFFEoxyanOM/TKauuw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/react-datepicker/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-datepicker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/react-docgen": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.1.tgz", @@ -16567,6 +16636,12 @@ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", diff --git a/view/next-project/package.json b/view/next-project/package.json index e690f1bef..2e337ce26 100644 --- a/view/next-project/package.json +++ b/view/next-project/package.json @@ -36,6 +36,7 @@ "nuqs": "^2.2.3", "pdf-lib": "^1.17.1", "react": "^18.3.1", + "react-datepicker": "^8.3.0", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hook-form": "^7.31.1", diff --git a/view/next-project/src/components/campus_fund/OpenEditModalButton.tsx b/view/next-project/src/components/campus_fund/OpenEditModalButton.tsx new file mode 100644 index 000000000..d0221642c --- /dev/null +++ b/view/next-project/src/components/campus_fund/OpenEditModalButton.tsx @@ -0,0 +1,29 @@ +import clsx from 'clsx'; +import * as React from 'react'; +import { useState } from 'react'; + +import { AddButton } from '../common'; + +interface Props { + className?: string; + children?: React.ReactNode; +} + +const OpenEditModalButton = (props: Props) => { + const [showModal, setShowModal] = useState(false); + + return ( + <> + { + setShowModal(true); + }} + > + {props.children} + + + ); +}; + +export default OpenEditModalButton; diff --git a/view/next-project/src/components/campus_fund/OtherBuildingsCard.tsx b/view/next-project/src/components/campus_fund/OtherBuildingsCard.tsx new file mode 100644 index 000000000..caf0db944 --- /dev/null +++ b/view/next-project/src/components/campus_fund/OtherBuildingsCard.tsx @@ -0,0 +1,53 @@ +import { Box, Table, Thead, Tbody, Tr, Th, Td, Text } from '@chakra-ui/react'; + +interface OtherTeacher { + building: string; + room: string; + name: string; + amount?: number; +} + +interface Props { + teachers: OtherTeacher[]; +} + +const OtherBuildingsCard = ({ teachers }: Props) => ( + + + その他 + + + + + + + + + + + + {teachers.map((t, i) => ( + + + + + + + ))} + +
棟名居室教員名金額
{t.building}{t.room}{t.name} + {t.amount ? `¥${t.amount.toLocaleString()}` : ''} +
+
+); + +export default OtherBuildingsCard; diff --git a/view/next-project/src/components/campus_fund/ReportModal.tsx b/view/next-project/src/components/campus_fund/ReportModal.tsx new file mode 100644 index 000000000..a35f73f92 --- /dev/null +++ b/view/next-project/src/components/campus_fund/ReportModal.tsx @@ -0,0 +1,140 @@ +import { + VStack, + FormLabel, + HStack, + Text, + Button, + InputGroup, + InputRightElement, + Icon, +} from '@chakra-ui/react'; +import { useState } from 'react'; +import DatePicker from 'react-datepicker'; +import { FaRegCalendarAlt } from 'react-icons/fa'; +import { useRecoilValue } from 'recoil'; +import { userAtom } from '@/store/atoms'; +import { PrimaryButton, Modal, Title, CloseButton, Input } from '@components/common'; +import 'react-datepicker/dist/react-datepicker.css'; +import formatNumber from '@components/common/Formatter'; + +interface Props { + isOpen: boolean; + onClose: () => void; + building: string | null; + teacher: { name: string; room: string } | null; + onBack?: () => void; +} + +const ReportModal = ({ isOpen, onClose, building, teacher, onBack }: Props) => { + const [selectedDate, setSelectedDate] = useState(null); + const [amount, setAmount] = useState(''); + const user = useRecoilValue(userAtom); + + if (!isOpen) return null; + + return ( + +
+
+ +
+ {building} + + {teacher ? `${teacher.room} ${teacher.name}` : ''} + + + {/* 日時 */} + + + 日時 + + + setSelectedDate(date)} + dateFormat='yyyy/MM/dd' + placeholderText='日付を選択' + className='border-gray-400 focus:border-teal-400 w-full border-b pr-10 text-sm focus:outline-none md:text-base' + popperPlacement='bottom' + popperClassName='z-datepicker-gal' + /> + + + + + + + {/* 記入担当者 */} + + + 記入担当者 + + + + + {/* 金額 */} + + + 金額 + + { + const value = e.target.value.replace(/,/g, ''); + if (!isNaN(Number(value))) { + setAmount(formatNumber(Number(value))); + } + }} + className='border-gray-400 focus:border-teal-400 border-b text-sm focus:outline-none md:text-base' + /> + + + {/* 戻る&追加ボタン */} + + + 追加する + + +
+
+ ); +}; + +export default ReportModal; diff --git a/view/next-project/src/components/campus_fund/SelectTeacherModal.tsx b/view/next-project/src/components/campus_fund/SelectTeacherModal.tsx new file mode 100644 index 000000000..4f5b83cd6 --- /dev/null +++ b/view/next-project/src/components/campus_fund/SelectTeacherModal.tsx @@ -0,0 +1,76 @@ +import { Table, Thead, Tbody, Tr, Th, Td, Select } from '@chakra-ui/react'; +import { Title, Modal, CloseButton, EditButton } from '../common'; +import formatNumber from '../common/Formatter'; + +interface Props { + isOpen: boolean; + onClose: () => void; + onSelect: (teacher: string) => void; + building: string | null; +} + +const SelectTeacherModal = ({ isOpen, onClose, onSelect, building }: Props) => { + const teachers = [ + { building: '1号棟', room: '102号室', name: '○○教授', amount: 5000 }, + { building: '1号棟', room: '103号室', name: '△△教授', amount: null }, + { building: '1号棟', room: '104号室', name: '□□教授', amount: 12000 }, + { building: '1号棟', room: '105号室', name: '▲▲教授', amount: null }, + { building: '1号棟', room: '106号室', name: '■■教授', amount: 8000 }, + { building: '1号棟', room: '107号室', name: '☆☆教授', amount: null }, + ]; + + if (!isOpen) return null; + + return ( + +
+
+ +
+ + {building || '建物名未設定'} + <Select + ml={4} + width='auto' + display='inline-block' + variant='unstyled' + borderBottom='1px solid #ccc' + fontSize={{ base: 'xs', md: 'sm' }} + > + <option value='1F'>1F</option> + <option value='2F'>2F</option> + <option value='3F'>3F</option> + </Select> + + + + + + + + + + + + {teachers.map((teacher) => ( + + + + + + + + ))} + +
何号棟居室教員名金額 +
{teacher.building}{teacher.room}{teacher.name} + {teacher.amount ? `¥${formatNumber(teacher.amount)}` : '-'} + + onSelect(teacher.name)} /> +
+
+
+ ); +}; + +export default SelectTeacherModal; diff --git a/view/next-project/src/constants/linkItem.tsx b/view/next-project/src/constants/linkItem.tsx index 44f668933..5dcd4a9c6 100644 --- a/view/next-project/src/constants/linkItem.tsx +++ b/view/next-project/src/constants/linkItem.tsx @@ -1,4 +1,5 @@ import { ReactNode } from 'react'; +import { AiOutlineGift } from 'react-icons/ai'; import { BsBuilding, BsVectorPen } from 'react-icons/bs'; import { FaChalkboardTeacher } from 'react-icons/fa'; import { HiOutlineDocumentText, HiCurrencyDollar } from 'react-icons/hi'; @@ -34,12 +35,11 @@ export const FinanceLinkItems: LinkItemProps[] = [ icon: , href: '/purchase_report_list', }, - //TODO:募金実装時に戻す - // { - // name: '学内募金', - // icon: , - // href: '', - // }, + { + name: '学内募金', + icon: , + href: '/campus_fund', + }, { name: '教員一覧', icon: , diff --git a/view/next-project/src/generated/hooks.ts b/view/next-project/src/generated/hooks.ts index 53f88d1b5..62e8772d7 100644 --- a/view/next-project/src/generated/hooks.ts +++ b/view/next-project/src/generated/hooks.ts @@ -104,7 +104,6 @@ import type { GetSponsorstyles200, GetSponsorstylesId200, GetTeachersFundRegisteredYear200, - GetTeachersId200, GetUsersId200, Income, IncomeCategory, @@ -175,6 +174,7 @@ import type { Receipt, Sponsor, SponsorStyle, + Teacher, YearPeriods, } from './model'; @@ -7585,7 +7585,7 @@ export const useDeleteSponsorstylesId = ( * teacherの一覧を取得 */ export type getTeachersResponse200 = { - data: void; + data: Teacher; status: 200; }; @@ -7781,7 +7781,7 @@ export const useDeleteTeachersDelete = (options?: { * IDで指定されたteacherの取得 */ export type getTeachersIdResponse200 = { - data: GetTeachersId200; + data: Teacher; status: 200; }; diff --git a/view/next-project/src/generated/model/index.ts b/view/next-project/src/generated/model/index.ts index c5d721c24..1076c95a9 100644 --- a/view/next-project/src/generated/model/index.ts +++ b/view/next-project/src/generated/model/index.ts @@ -182,5 +182,6 @@ export * from './putYearsPeriodsId200'; export * from './receipt'; export * from './sponsor'; export * from './sponsorStyle'; +export * from './teacher'; export * from './total'; export * from './yearPeriods'; diff --git a/view/next-project/src/generated/model/postTeachersParams.ts b/view/next-project/src/generated/model/postTeachersParams.ts index 6721f1edf..4c6a69dd9 100644 --- a/view/next-project/src/generated/model/postTeachersParams.ts +++ b/view/next-project/src/generated/model/postTeachersParams.ts @@ -19,6 +19,10 @@ export type PostTeachersParams = { * 学科ID */ department_id?: number; + /** + * 棟 + */ + building?: string; /** * 部屋番号 */ diff --git a/view/next-project/src/generated/model/putTeachersIdParams.ts b/view/next-project/src/generated/model/putTeachersIdParams.ts index 0b98142d3..22d2c1fc3 100644 --- a/view/next-project/src/generated/model/putTeachersIdParams.ts +++ b/view/next-project/src/generated/model/putTeachersIdParams.ts @@ -19,6 +19,10 @@ export type PutTeachersIdParams = { * 学科ID */ department_id?: number; + /** + * 棟 + */ + building?: string; /** * 部屋番号 */ diff --git a/view/next-project/src/generated/model/teacher.ts b/view/next-project/src/generated/model/teacher.ts new file mode 100644 index 000000000..ed47351df --- /dev/null +++ b/view/next-project/src/generated/model/teacher.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v7.6.0 🍺 + * Do not edit manually. + * NUTFes FinanSu API + * FinanSu APIドキュメント + * OpenAPI spec version: 2.0.0 + */ + +export interface Teacher { + id: number; + name: string; + position: string; + department_id: number; + building: string; + room?: string; + is_black: boolean; + remark: string; +} diff --git a/view/next-project/src/pages/campus_fund/index.tsx b/view/next-project/src/pages/campus_fund/index.tsx new file mode 100644 index 000000000..18f676991 --- /dev/null +++ b/view/next-project/src/pages/campus_fund/index.tsx @@ -0,0 +1,107 @@ +import { Box, Grid, GridItem, Text } from '@chakra-ui/react'; +import { useState } from 'react'; +import ReportModal from '@/components/campus_fund/ReportModal'; +import SelectTeacherModal from '@/components/campus_fund/SelectTeacherModal'; +import formatNumber from '@/components/common/Formatter'; +import MainLayout from '@/components/layout/MainLayout'; + +const CampusFund = () => { + const [selectedBuilding, setSelectedBuilding] = useState(null); + const [isSelectTeacherOpen, setIsSelectTeacherOpen] = useState(false); + const [isReportModalOpen, setIsReportModalOpen] = useState(false); + const [selectedTeacher, setSelectedTeacher] = useState(null); + + const buildings = [ + { name: '機械・建設棟', amount: 24000 }, + { name: '電気棟', amount: 5000 }, + { name: '生物棟', amount: 6500 }, + { name: '環境・システム棟', amount: 21000 }, + { name: '物質・材料経営情報棟', amount: 34000 }, + { name: '総合研究棟', amount: 41000 }, + { name: '原子力・システム安全棟', amount: 8500 }, + { name: '事務局棟', amount: 64000 }, + { name: 'センター', amount: 121000 }, + ]; + + // 総募金額を計算 + const totalAmount = buildings.reduce((sum, building) => sum + building.amount, 0); + + const handleBuildingClick = (building: string) => { + setSelectedBuilding(building); + setIsSelectTeacherOpen(true); + }; + + const handleTeacherSelect = (teacher: string) => { + setSelectedTeacher(teacher); + setIsSelectTeacherOpen(false); + setIsReportModalOpen(true); + }; + + return ( + + + + 総募金額 + + + ¥{formatNumber(totalAmount)} + + + {buildings.map((building) => ( + handleBuildingClick(building.name)} + bg='white' + _hover={{ boxShadow: 'md', bg: '#f0f9fa' }} + minW={0} + > + + {building.name} + + + ¥{formatNumber(building.amount)} + + + ))} + + + {/* 教員選択モーダル */} + setIsSelectTeacherOpen(false)} + onSelect={handleTeacherSelect} + building={selectedBuilding} + /> + + {/* 報告モーダル */} + setIsReportModalOpen(false)} + building={selectedBuilding} + teacher={ + selectedTeacher + ? { + name: selectedTeacher, + room: buildings.find((b) => b.name === selectedBuilding)?.name || '', + } + : null + } + onBack={() => { + setIsReportModalOpen(false); + setIsSelectTeacherOpen(true); + }} + /> + + + ); +}; + +export default CampusFund;