From ceff3db0d0e08f182fc65c75946694607a358bff Mon Sep 17 00:00:00 2001 From: jspark2000 Date: Thu, 14 Mar 2024 08:32:29 +0000 Subject: [PATCH] feat: implements attendance statistic page --- .../src/attendance/attendance.controller.ts | 28 +++- .../app/src/attendance/attendance.service.ts | 101 +++++++++++---- .../app/src/attendance/dto/attendance.dto.ts | 12 +- backend/app/src/roster/roster.service.ts | 9 +- backend/app/src/survey/survey.controller.ts | 25 ++++ backend/app/src/survey/survey.service.ts | 39 ++++++ backend/prisma/seed.ts | 97 +++++++++++++- frontend/package.json | 1 + .../account/_component/UserListTable.tsx | 76 +++++------ .../AthleteAttendanceListTable.tsx | 120 ++++++++++++++++++ .../[id]/_components/ScheduleCard.tsx | 56 ++++++++ .../src/app/console/schedule/[id]/page.tsx | 83 ++++++++++++ .../_components/ScheduleListTable.tsx | 89 +++++++++++++ frontend/src/app/console/schedule/page.tsx | 33 +++++ frontend/src/components/ConsoleSidebar.tsx | 19 ++- frontend/src/components/Search.tsx | 36 ++++++ frontend/src/lib/actions.ts | 26 ++++ frontend/src/lib/types/attendance.ts | 20 +++ frontend/src/lib/types/schedule.ts | 6 + pnpm-lock.yaml | 12 ++ 20 files changed, 803 insertions(+), 85 deletions(-) create mode 100644 frontend/src/app/console/schedule/[id]/_components/AthleteAttendanceListTable.tsx create mode 100644 frontend/src/app/console/schedule/[id]/_components/ScheduleCard.tsx create mode 100644 frontend/src/app/console/schedule/[id]/page.tsx create mode 100644 frontend/src/app/console/schedule/_components/ScheduleListTable.tsx create mode 100644 frontend/src/app/console/schedule/page.tsx create mode 100644 frontend/src/components/Search.tsx create mode 100644 frontend/src/lib/types/attendance.ts diff --git a/backend/app/src/attendance/attendance.controller.ts b/backend/app/src/attendance/attendance.controller.ts index 9d9c0a2..30fb714 100644 --- a/backend/app/src/attendance/attendance.controller.ts +++ b/backend/app/src/attendance/attendance.controller.ts @@ -1,7 +1,33 @@ -import { Controller } from '@nestjs/common' +import { Controller, Get, ParseIntPipe, Query } from '@nestjs/common' +import { Roles } from '@libs/decorator' +import { BusinessExceptionHandler } from '@libs/exception' +import { Role } from '@prisma/client' import { AttendanceService } from './attendance.service' +import type { AttendanceWithRoster } from './dto/attendance.dto' @Controller('attendances') export class AttendanceController { constructor(private readonly attendanceService: AttendanceService) {} + + @Get('') + @Roles(Role.Manager) + async getAttendances( + @Query('scheduleId', ParseIntPipe) scheduleId: number, + @Query('page', ParseIntPipe) page: number, + @Query('searchTerm') searchTerm: string, + @Query('limit', new ParseIntPipe({ optional: true })) limit?: number, + @Query('rosterType') rosterType?: string + ): Promise<{ attendances: AttendanceWithRoster[]; total: number }> { + try { + return await this.attendanceService.getAttendances( + scheduleId, + searchTerm, + page, + rosterType, + limit + ) + } catch (error) { + BusinessExceptionHandler(error) + } + } } diff --git a/backend/app/src/attendance/attendance.service.ts b/backend/app/src/attendance/attendance.service.ts index d1a53db..908b1b3 100644 --- a/backend/app/src/attendance/attendance.service.ts +++ b/backend/app/src/attendance/attendance.service.ts @@ -50,53 +50,94 @@ export class AttendanceService { async getAttendances( scheduleId: number, - searchTerm: string, + searchTerm = '', page: number, + rosterType?: string, limit = 10 - ): Promise<{ attendances: AttendanceWithRoster[] }> { + ): Promise<{ attendances: AttendanceWithRoster[]; total: number }> { try { const attendances = await this.prisma.attendance.findMany({ where: { scheduleId, Roster: { - OR: [ + AND: [ { - offPosition: { - contains: searchTerm - }, - defPosition: { - contains: searchTerm - }, - splPosition: { - contains: searchTerm - } + type: this.transformRosterType(rosterType) + }, + { + OR: [ + { + offPosition: { + contains: searchTerm + } + }, + { + defPosition: { + contains: searchTerm + } + }, + { + splPosition: { + contains: searchTerm + } + } + ] } ] } }, include: { - Roster: { - select: { - id: true, - name: true, - type: true, - registerYear: true, - offPosition: true, - defPosition: true, - splPosition: true - } - } + Roster: true }, take: limit, skip: calculatePaginationOffset(page, limit), - orderBy: { + orderBy: [ + { + Roster: { + admissionYear: 'asc' + } + }, + { + Roster: { + name: 'asc' + } + } + ] + }) + + const total = await this.prisma.attendance.count({ + where: { + scheduleId, Roster: { - name: 'asc' + AND: [ + { + type: this.transformRosterType(rosterType) + }, + { + OR: [ + { + offPosition: { + contains: searchTerm + } + }, + { + defPosition: { + contains: searchTerm + } + }, + { + splPosition: { + contains: searchTerm + } + } + ] + } + ] } } }) - return { attendances } + return { attendances, total } } catch (error) { throw new UnexpectedException(error) } @@ -244,4 +285,12 @@ export class AttendanceService { return positionCounts } + + private transformRosterType(rosterType: string): RosterType { + try { + return RosterType[rosterType] + } catch (error) { + return RosterType.Athlete + } + } } diff --git a/backend/app/src/attendance/dto/attendance.dto.ts b/backend/app/src/attendance/dto/attendance.dto.ts index e4c119b..8b33ce2 100644 --- a/backend/app/src/attendance/dto/attendance.dto.ts +++ b/backend/app/src/attendance/dto/attendance.dto.ts @@ -2,7 +2,7 @@ import { AttendanceLocation, AttendanceResponse, type Attendance, - type RosterType + type Roster } from '@prisma/client' import { IsEnum, @@ -50,13 +50,5 @@ export class UpdateAttendanceDTO { export interface AttendanceWithRoster extends Attendance { // eslint-disable-next-line @typescript-eslint/naming-convention - Roster: { - id: number - name: string - type: RosterType - registerYear: number - offPosition?: string - defPosition?: string - splPosition?: string - } + Roster: Roster } diff --git a/backend/app/src/roster/roster.service.ts b/backend/app/src/roster/roster.service.ts index d2efb02..5784824 100644 --- a/backend/app/src/roster/roster.service.ts +++ b/backend/app/src/roster/roster.service.ts @@ -70,9 +70,12 @@ export class RosterService { }, take: limit, skip: calculatePaginationOffset(page, limit), - orderBy: { - name: 'asc' - } + orderBy: [ + { + admissionYear: 'asc', + name: 'asc' + } + ] }) const total = await this.prisma.roster.count({ diff --git a/backend/app/src/survey/survey.controller.ts b/backend/app/src/survey/survey.controller.ts index ec15ada..cc51b7e 100644 --- a/backend/app/src/survey/survey.controller.ts +++ b/backend/app/src/survey/survey.controller.ts @@ -63,6 +63,31 @@ export class SurveyController { } } + @Public() + @Get('/schedules') + async getSchedules( + @Query('page', ParseIntPipe) page: number, + @Query('limit', new ParseIntPipe({ optional: true })) limit?: number + ): Promise<{ schedules: Schedule[]; total: number }> { + try { + return await this.surveyService.getSchedules(page, limit) + } catch (error) { + BusinessExceptionHandler(error) + } + } + + @Public() + @Get('/schedules/:scheduleId') + async getSchedule( + @Param('scheduleId', ParseIntPipe) scheduleId: number + ): Promise { + try { + return await this.surveyService.getSchedule(scheduleId) + } catch (error) { + BusinessExceptionHandler(error) + } + } + @Public() @Get('/groups') async getSurveyGroups( diff --git a/backend/app/src/survey/survey.service.ts b/backend/app/src/survey/survey.service.ts index 7f6024b..7c527c9 100644 --- a/backend/app/src/survey/survey.service.ts +++ b/backend/app/src/survey/survey.service.ts @@ -103,6 +103,45 @@ export class SurveyService { } } + async getSchedules( + page: number, + limit = 10 + ): Promise<{ schedules: Schedule[]; total: number }> { + try { + const schedules = await this.prisma.schedule.findMany({ + take: limit, + skip: calculatePaginationOffset(page, limit), + orderBy: { + startedAt: 'desc' + } + }) + + const total = await this.prisma.schedule.count() + + return { schedules, total } + } catch (error) { + throw new UnexpectedException(error) + } + } + + async getSchedule(scheduleId: number): Promise { + try { + return await this.prisma.schedule.findUniqueOrThrow({ + where: { + id: scheduleId + } + }) + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2025' + ) { + throw new EntityNotExistException('일정이 존재하지 않습니다') + } + throw new UnexpectedException(error) + } + } + async createSurveyGroup( surveyGroupDTO: CreateSurveyGroupDTO ): Promise { diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index e52b824..28eafc5 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -4,13 +4,14 @@ import { Role, AccountStatus, RosterStatus, - RosterType + RosterType, + ScheduleType } from '@prisma/client' import { hash } from 'argon2' const prisma = new PrismaClient() -const createAccounts = async () => { +const seedingDatabase = async () => { const accounts: Prisma.UserCreateManyInput[] = [ { username: 'user01', @@ -223,10 +224,100 @@ const createAccounts = async () => { await prisma.surveyGroup.createMany({ data: surveyGroups }) + + const schedules: Prisma.ScheduleCreateManyInput[] = [ + { + name: '월요일 캠퍼스별 훈련', + description: '월요일 캠퍼스별 훈련', + surveyGroupId: 1, + startedAt: new Date('2024-01-01T08:00:00.000Z'), + endedAt: new Date('2024-01-01T11:00:00.000Z'), + type: ScheduleType.SeperatedExercise + }, + { + name: '수요일 캠퍼스별 훈련', + description: '수요일 캠퍼스별 훈련', + surveyGroupId: 1, + startedAt: new Date('2024-01-03T08:00:00.000Z'), + endedAt: new Date('2024-01-03T11:00:00.000Z'), + type: ScheduleType.SeperatedExercise + }, + { + name: '금요일 통합훈련', + description: '금요일 통합훈련', + surveyGroupId: 1, + startedAt: new Date('2024-01-05T08:00:00.000Z'), + endedAt: new Date('2024-01-05T11:00:00.000Z'), + type: ScheduleType.IntegratedExercise + }, + { + name: '토요일 통합훈련', + description: '토요일 통합훈련', + surveyGroupId: 1, + startedAt: new Date('2024-01-05T23:00:00.000Z'), + endedAt: new Date('2024-01-06T02:00:00.000Z'), + type: ScheduleType.IntegratedExercise + } + ] + + await prisma.schedule.createMany({ + data: schedules + }) + + const attendances: Prisma.AttendanceCreateManyInput[] = [ + { + scheduleId: 1, + rosterId: 1, + response: 'Present' + }, + { + scheduleId: 1, + rosterId: 2, + response: 'Present' + }, + { + scheduleId: 1, + rosterId: 3, + response: 'Present' + }, + { + scheduleId: 1, + rosterId: 5, + response: 'Tardy', + reason: '수업' + }, + { + scheduleId: 1, + rosterId: 6, + response: 'Tardy', + reason: '수업' + }, + { + scheduleId: 1, + rosterId: 7, + response: 'Absence', + reason: '수업' + }, + { + scheduleId: 1, + rosterId: 9, + response: 'Present' + }, + { + scheduleId: 1, + rosterId: 9, + response: 'Absence', + reason: '수업' + } + ] + + await prisma.attendance.createMany({ + data: attendances + }) } const main = async () => { - await createAccounts() + await seedingDatabase() } main() diff --git a/frontend/package.json b/frontend/package.json index adc7818..a5e54be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,7 @@ "sonner": "^1.4.3", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", + "use-debounce": "^10.0.0", "vaul": "^0.9.0", "zod": "^3.22.4" }, diff --git a/frontend/src/app/console/account/_component/UserListTable.tsx b/frontend/src/app/console/account/_component/UserListTable.tsx index fb80fd9..6c6e0ac 100644 --- a/frontend/src/app/console/account/_component/UserListTable.tsx +++ b/frontend/src/app/console/account/_component/UserListTable.tsx @@ -5,45 +5,45 @@ import LocalTime from '@/components/Localtime' import type { UserListItem } from '@/lib/types/user' import type { ColumnDef } from '@tanstack/react-table' -const columns: ColumnDef[] = [ - { - accessorKey: 'id', - header: 'ID' - }, - { - accessorKey: 'username', - header: '아이디' - }, - { - accessorKey: 'email', - header: '이메일' - }, - { - accessorKey: 'nickname', - header: '닉네임' - }, - { - accessorKey: 'role', - header: '권한' - }, - { - accessorKey: 'status', - header: '계정상태' - }, - { - accessorKey: 'lastLogin', - header: '마지막 로그인', - cell: ({ row }) => { - return ( - - ) +export default function UserListTable({ users }: { users: UserListItem[] }) { + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID' + }, + { + accessorKey: 'username', + header: '아이디' + }, + { + accessorKey: 'email', + header: '이메일' + }, + { + accessorKey: 'nickname', + header: '닉네임' + }, + { + accessorKey: 'role', + header: '권한' + }, + { + accessorKey: 'status', + header: '계정상태' + }, + { + accessorKey: 'lastLogin', + header: '마지막 로그인', + cell: ({ row }) => { + return ( + + ) + } } - } -] + ] -export default function UserListTable({ users }: { users: UserListItem[] }) { return } diff --git a/frontend/src/app/console/schedule/[id]/_components/AthleteAttendanceListTable.tsx b/frontend/src/app/console/schedule/[id]/_components/AthleteAttendanceListTable.tsx new file mode 100644 index 0000000..cdaea06 --- /dev/null +++ b/frontend/src/app/console/schedule/[id]/_components/AthleteAttendanceListTable.tsx @@ -0,0 +1,120 @@ +'use client' + +import Badge, { BadgeColor } from '@/components/Badge' +import { DataTable } from '@/components/DataTable' +import { AttendanceStatus, RosterType } from '@/lib/enums' +import type { AttendanceListItem } from '@/lib/types/attendance' +import type { RosterListItem } from '@/lib/types/roster' +import type { ColumnDef } from '@tanstack/react-table' +import { UserIcon } from 'lucide-react' +import Image from 'next/image' + +export default function AthleteAttendanceListTable({ + attendances +}: { + attendances: AttendanceListItem[] +}) { + const renderAthletePosition = (roster: RosterListItem) => { + const positions = [] + switch (roster.type) { + case RosterType.Athlete: + roster.offPosition ? positions.push(roster.offPosition) : null + roster.defPosition ? positions.push(roster.defPosition) : null + roster.splPosition ? positions.push(roster.splPosition) : null + return

{positions.join('/')}

+ default: + return

-

+ } + } + + const renderAttendanceStatus = (attendance: AttendanceListItem) => { + switch (attendance.response) { + case AttendanceStatus.Absence: + return + case AttendanceStatus.Tardy: + return + case AttendanceStatus.Present: + default: + return + } + } + + const renderAthleteType = (roster: RosterListItem) => { + if (roster.registerYear === new Date().getFullYear()) { + return + } + return + } + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID' + }, + { + id: 'rosterProfile', + header: '이름', + cell: ({ row }) => { + const attendance = row.original + + return ( +
+ {attendance.Roster.profileImageUrl ? ( + profile + ) : ( + + )} +

{attendance.Roster.name}

+
+ ) + } + }, + { + id: 'admissionYear', + header: '학번', + cell: ({ row }) => { + const attendance = row.original + return attendance.Roster.admissionYear + } + }, + { + id: 'position', + header: '포지션', + cell: ({ row }) => { + const attendance = row.original + + return renderAthletePosition(attendance.Roster) + } + }, + { + id: 'type', + header: '구분', + cell: ({ row }) => { + const attendance = row.original + + return renderAthleteType(attendance.Roster) + } + }, + { + accessorKey: 'response', + header: '응답', + cell: ({ row }) => { + const attendance = row.original + + return renderAttendanceStatus(attendance) + } + }, + { + accessorKey: 'reason', + header: '불참사유' + } + ] + + return +} diff --git a/frontend/src/app/console/schedule/[id]/_components/ScheduleCard.tsx b/frontend/src/app/console/schedule/[id]/_components/ScheduleCard.tsx new file mode 100644 index 0000000..0b8f853 --- /dev/null +++ b/frontend/src/app/console/schedule/[id]/_components/ScheduleCard.tsx @@ -0,0 +1,56 @@ +import Badge, { BadgeColor } from '@/components/Badge' +import LocalTime from '@/components/Localtime' +import { Card, CardContent, CardHeader } from '@/components/ui/card' +import { getSchedule } from '@/lib/actions' +import { ScheduleType } from '@/lib/enums' +import type { ScheduleListItem } from '@/lib/types/schedule' +import { CalendarIcon } from '@heroicons/react/24/outline' + +export default async function ScheduleCard({ + params +}: { + params: { id: number } +}) { + const schedule = await getSchedule(params.id) + + const renderScheduleType = (schedule: ScheduleListItem) => { + switch (schedule.type) { + case ScheduleType.IntegratedExercise: + return + case ScheduleType.SeperatedExercise: + return + case ScheduleType.Game: + return + case ScheduleType.Event: + default: + return + } + } + + return ( + + +
+ +

{schedule.name}

{' '} + {renderScheduleType(schedule)} +
+
+ +
+

+ 시작:{' '} + +

+

+ 종료:{' '} + +

+
+
+
+ ) +} diff --git a/frontend/src/app/console/schedule/[id]/page.tsx b/frontend/src/app/console/schedule/[id]/page.tsx new file mode 100644 index 0000000..0a0c5fd --- /dev/null +++ b/frontend/src/app/console/schedule/[id]/page.tsx @@ -0,0 +1,83 @@ +import Pagination from '@/components/Pagination' +import Search from '@/components/Search' +import { Button } from '@/components/ui/button' +import { getAttendances } from '@/lib/actions' +import { RosterType } from '@/lib/enums' +import { calculateTotalPages } from '@/lib/utils' +import { PAGINATION_LIMIT_DEFAULT } from '@/lib/vars' +import Link from 'next/link' +import AthleteAttendanceListTable from './_components/AthleteAttendanceListTable' +import ScheduleCard from './_components/ScheduleCard' + +export default async function AttendanceStatisticPage({ + params, + searchParams +}: { + params: { + id: number + } + searchParams: { + searchTerm?: string + page?: string + } +}) { + const searchTerm = searchParams?.searchTerm || '' + const currentPage = Number(searchParams?.page) || 1 + + const attendanceList = await getAttendances( + params.id, + currentPage, + RosterType.Athlete, + searchTerm + ) + + console.log(attendanceList) + + return ( +
+
+

출결상세

+ + + +
+
+
+
+

일정

+ +
+
+

포지션별 출석 통계

+

준비중입니다

+
+
+
+
+

선수단

+
+
+ +
+
+ + +
+
+ {/*
+

스태프

+
+
+

코치진

+
*/} +
+
+ ) +} diff --git a/frontend/src/app/console/schedule/_components/ScheduleListTable.tsx b/frontend/src/app/console/schedule/_components/ScheduleListTable.tsx new file mode 100644 index 0000000..b037835 --- /dev/null +++ b/frontend/src/app/console/schedule/_components/ScheduleListTable.tsx @@ -0,0 +1,89 @@ +'use client' + +import Badge, { BadgeColor } from '@/components/Badge' +import { DataTable } from '@/components/DataTable' +import LocalTime from '@/components/Localtime' +import { Button } from '@/components/ui/button' +import { ScheduleType } from '@/lib/enums' +import type { ScheduleListItem } from '@/lib/types/schedule' +import { ArrowRightIcon } from '@heroicons/react/24/outline' +import type { ColumnDef } from '@tanstack/react-table' +import Link from 'next/link' + +export default function ScheduleListTable({ + schedules +}: { + schedules: ScheduleListItem[] +}) { + const renderScheduleType = (schedule: ScheduleListItem) => { + switch (schedule.type) { + case ScheduleType.IntegratedExercise: + return + case ScheduleType.SeperatedExercise: + return + case ScheduleType.Game: + return + case ScheduleType.Event: + default: + return + } + } + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID' + }, + { + accessorKey: 'name', + header: '일정명' + }, + { + accessorKey: 'startedAt', + header: '시작', + cell: ({ row }) => { + return ( + + ) + } + }, + { + accessorKey: 'endedAt', + header: '종료', + cell: ({ row }) => { + return ( + + ) + } + }, + { + accessorKey: 'type', + header: '구분', + cell: ({ row }) => { + const schedule = row.original + return renderScheduleType(schedule) + } + }, + { + id: 'action', + header: '상세보기', + cell: ({ row }) => { + return ( + + + + ) + } + } + ] + + return +} diff --git a/frontend/src/app/console/schedule/page.tsx b/frontend/src/app/console/schedule/page.tsx new file mode 100644 index 0000000..5c566f7 --- /dev/null +++ b/frontend/src/app/console/schedule/page.tsx @@ -0,0 +1,33 @@ +import Pagination from '@/components/Pagination' +import { getSchedules } from '@/lib/actions' +import { calculateTotalPages } from '@/lib/utils' +import { PAGINATION_LIMIT_DEFAULT } from '@/lib/vars' +import AttendanceListTable from './_components/ScheduleListTable' + +export default async function AttendancePage({ + searchParams +}: { + searchParams?: { + page?: string + } +}) { + const currentPage = Number(searchParams?.page) || 1 + const scheduleList = await getSchedules(currentPage) + + return ( +
+
+

출결관리

+
+
+ + +
+
+ ) +} diff --git a/frontend/src/components/ConsoleSidebar.tsx b/frontend/src/components/ConsoleSidebar.tsx index fa42d05..7e5a4e6 100644 --- a/frontend/src/components/ConsoleSidebar.tsx +++ b/frontend/src/components/ConsoleSidebar.tsx @@ -9,7 +9,8 @@ import { Bars3Icon, CalendarIcon, HomeIcon, - UserIcon, + LockClosedIcon, + PencilSquareIcon, UsersIcon, XMarkIcon } from '@heroicons/react/24/outline' @@ -45,16 +46,26 @@ const navigation = [ icon: UsersIcon, role: 'Manager' }, + { + name: '계정관리', + href: '/console/account', + icon: LockClosedIcon, + role: 'Admin' + }, // { name: '시합관리', href: '#', icon: FireIcon, role: 'Manager' }, { name: '출석조사', href: '/console/survey', - icon: CalendarIcon, + icon: PencilSquareIcon, role: 'Manager' }, - { name: '계정관리', href: '/console/account', icon: UserIcon, role: 'Admin' } // { name: '출석변경', href: '#', icon: PencilIcon, role: 'Admin' }, - // { name: '출석통계', href: '#', icon: ChartPieIcon, role: 'Admin' } + { + name: '출결관리', + href: '/console/schedule', + icon: CalendarIcon, + role: 'Manager' + } ] export default function ConsoleSidebar() { diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx new file mode 100644 index 0000000..714cc38 --- /dev/null +++ b/frontend/src/components/Search.tsx @@ -0,0 +1,36 @@ +'use client' + +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useDebouncedCallback } from 'use-debounce' +import { Input } from './ui/input' + +export default function Search({ placeholder }: { placeholder: string }) { + const searchParams = useSearchParams() + const pathname = usePathname() + const { replace } = useRouter() + + const handleSearch = useDebouncedCallback((searchTerm: string) => { + const params = new URLSearchParams(searchParams) + + if (searchTerm) { + params.set('searchTerm', searchTerm) + } else { + params.delete('searchTerm') + } + + replace(`${pathname}?${params.toString()}`) + }, 500) + + return ( +
+ + handleSearch(e.target.value)} + /> +
+ ) +} diff --git a/frontend/src/lib/actions.ts b/frontend/src/lib/actions.ts index d1015ef..b7f0a55 100644 --- a/frontend/src/lib/actions.ts +++ b/frontend/src/lib/actions.ts @@ -1,7 +1,10 @@ 'use server' +import type { RosterType } from './enums' import fetcher from './fetcher' +import type { AttendanceList } from './types/attendance' import type { RosterList } from './types/roster' +import type { ScheduleList, ScheduleListItem } from './types/schedule' import type { SurveyGroupList, SurveyGroupListItem, @@ -47,3 +50,26 @@ export const getSurveyGroupWithSchedules = async ( `/surveys/groups/${surveyGroupId}/schedules` ) } + +export const getSchedules = async (page: number): Promise => { + return await fetcher.get( + `/surveys/schedules?page=${page}&limit=${PAGINATION_LIMIT_DEFAULT}` + ) +} + +export const getSchedule = async ( + scheduleId: number +): Promise => { + return await fetcher.get(`/surveys/schedules/${scheduleId}`) +} + +export const getAttendances = async ( + scheduleId: number, + page: number, + rosterType: RosterType, + searchTerm: string +): Promise => { + return await fetcher.get( + `/attendances?scheduleId=${scheduleId}&page=${page}&rosterType=${rosterType}&searchTerm=${searchTerm}&limit=${PAGINATION_LIMIT_DEFAULT}` + ) +} diff --git a/frontend/src/lib/types/attendance.ts b/frontend/src/lib/types/attendance.ts new file mode 100644 index 0000000..9a24b8b --- /dev/null +++ b/frontend/src/lib/types/attendance.ts @@ -0,0 +1,20 @@ +import type { AttendanceLocation, AttendanceStatus } from '../enums' +import type { RosterListItem } from './roster' + +type AttendanceBasic = { + id: number + response: AttendanceStatus + result: AttendanceStatus + location: AttendanceLocation + reason?: string +} + +export interface AttendanceListItem extends AttendanceBasic { + // eslint-disable-next-line @typescript-eslint/naming-convention + Roster: RosterListItem +} + +export interface AttendanceList { + attendances: AttendanceListItem[] + total: number +} diff --git a/frontend/src/lib/types/schedule.ts b/frontend/src/lib/types/schedule.ts index 1136ec2..ecb2a9d 100644 --- a/frontend/src/lib/types/schedule.ts +++ b/frontend/src/lib/types/schedule.ts @@ -10,3 +10,9 @@ type ScheduleBasic = { } export interface Schedule extends ScheduleBasic {} +export interface ScheduleListItem extends ScheduleBasic {} + +export interface ScheduleList { + schedules: ScheduleListItem[] + total: number +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e63d839..1460ba2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,6 +279,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.1) + use-debounce: + specifier: ^10.0.0 + version: 10.0.0(react@18.2.0) vaul: specifier: ^0.9.0 version: 0.9.0(@types/react-dom@18.2.21)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) @@ -15508,6 +15511,15 @@ packages: react: 18.2.0 tslib: 2.6.2 + /use-debounce@10.0.0(react@18.2.0): + resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==} + engines: {node: '>= 16.0.0'} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + /use-resize-observer@9.1.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} peerDependencies: