@
17기 운영진
@@ -17,5 +18,3 @@ const FormRange = () => {
);
};
-
-export default FormRange;
diff --git a/apps/admin/app/(auth)/bills/create/_components/form/form-step-item.tsx b/apps/admin/app/(auth)/bills/create/_components/form/form-step-item.tsx
new file mode 100644
index 00000000..3c16aaf2
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/create/_components/form/form-step-item.tsx
@@ -0,0 +1,106 @@
+'use client';
+
+import {
+ Button,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ Input,
+} from '@dpm-core/shared';
+import { useFormContext, useWatch } from 'react-hook-form';
+
+const MAX_LENGTH = 20;
+
+interface FormStepItemProps {
+ index: number;
+ onRemove: () => void;
+}
+
+export const FormStepItem = ({ index, onRemove }: FormStepItemProps) => {
+ const { control } = useFormContext();
+ const title = useWatch({ name: `gatherings.${index}.title`, control });
+
+ return (
+
+ {/* 닫기 버튼 */}
+ {index > 0 && (
+
+ )}
+
+ {/* 차수 이름 */}
+ (
+
+ 회식 차수 이름
+
+
+
+ {/* TODO 차수 추가 시 자동 스크롤 구현 */}
+
+
+
+ {title.length}/{MAX_LENGTH}자
+
+
+
+ )}
+ />
+
+ {/* 금액 입력 */}
+ (
+
+ 금액
+
+ {
+ const raw = e.target.value.replace(/[^0-9]/g, '');
+ const formatted = raw.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+ field.onChange(formatted);
+ }}
+ />
+
+
+
+ )}
+ />
+
+ );
+};
diff --git a/apps/admin/app/(auth)/bills/create/_components/form/form-step-list.tsx b/apps/admin/app/(auth)/bills/create/_components/form/form-step-list.tsx
new file mode 100644
index 00000000..b785e262
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/create/_components/form/form-step-list.tsx
@@ -0,0 +1,17 @@
+import type { UseFieldArrayRemove } from 'react-hook-form';
+import { FormStepItem } from './form-step-item';
+
+interface FormStepListPros {
+ fields: Record<'id', string>[];
+ remove: UseFieldArrayRemove;
+}
+
+export const FormStepList = ({ fields, remove }: FormStepListPros) => {
+ return (
+
+ {fields.map((field, index) => (
+ remove(index)} />
+ ))}
+
+ );
+};
diff --git a/apps/admin/app/(auth)/bills/create/_components/form/form-title.tsx b/apps/admin/app/(auth)/bills/create/_components/form/form-title.tsx
new file mode 100644
index 00000000..cb28a82c
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/create/_components/form/form-title.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import { FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from '@dpm-core/shared';
+import { useFormContext, useWatch } from 'react-hook-form';
+
+const MAX_LENGTH = 20;
+
+export const FormTitle = () => {
+ const form = useFormContext();
+ const title = useWatch({ control: form.control, name: 'title' });
+
+ return (
+
+ );
+};
diff --git a/apps/admin/app/(auth)/bills/create/_components/form/index.ts b/apps/admin/app/(auth)/bills/create/_components/form/index.ts
new file mode 100644
index 00000000..91f69b89
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/create/_components/form/index.ts
@@ -0,0 +1,5 @@
+export * from './form-date';
+export * from './form-description';
+export * from './form-range';
+export * from './form-step-list';
+export * from './form-title';
diff --git a/apps/admin/app/(auth)/bills/create/_components/information-form-item.tsx b/apps/admin/app/(auth)/bills/create/_components/information-form-item.tsx
new file mode 100644
index 00000000..a7687c19
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/create/_components/information-form-item.tsx
@@ -0,0 +1,17 @@
+import { FormDate, FormDescription, FormRange, FormTitle } from './form';
+
+export const InformationFormItem = () => {
+ return (
+ <>
+
+
기본 정보
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/apps/admin/components/settle/create/StepWrapper.tsx b/apps/admin/app/(auth)/bills/create/_components/step-form-item.tsx
similarity index 66%
rename from apps/admin/components/settle/create/StepWrapper.tsx
rename to apps/admin/app/(auth)/bills/create/_components/step-form-item.tsx
index 62d14115..179170c9 100644
--- a/apps/admin/components/settle/create/StepWrapper.tsx
+++ b/apps/admin/app/(auth)/bills/create/_components/step-form-item.tsx
@@ -1,7 +1,18 @@
-import React from 'react';
-import FormStepList from './FormStepList';
+import { Button } from '@dpm-core/shared';
+import { useFieldArray, useFormContext } from 'react-hook-form';
+import { FormStepList } from './form';
+
+export const StepFormItem = () => {
+ const form = useFormContext();
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name: 'gatherings',
+ });
+
+ const handleAddStep = () => {
+ append({ title: '', receipt: { amount: '' } });
+ };
-const StepWrapper = () => {
return (
<>
@@ -11,10 +22,13 @@ const StepWrapper = () => {
-
-
+
+
>
);
};
-
-export default StepWrapper;
diff --git a/apps/admin/app/(auth)/bills/create/page.tsx b/apps/admin/app/(auth)/bills/create/page.tsx
new file mode 100644
index 00000000..74287758
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/create/page.tsx
@@ -0,0 +1,11 @@
+import { AppHeader } from '@/components/app-header';
+import { BillForm } from './_components/bill-form';
+
+export default function CreateBillsPage() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-container.tsx b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-container.tsx
new file mode 100644
index 00000000..aaa4a1ee
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-container.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import { ErrorBoundary } from '@suspensive/react';
+import { Suspense } from 'react';
+import { AppHeader } from '@/components/app-header';
+import { LoadingBox } from '@/components/loading-box';
+import { BillGatheringList } from './bill-gathering-list';
+
+interface BillGatheringContainerProps {
+ gatheringId: number;
+}
+
+export const _BillGatheringContainer = ({ gatheringId }: BillGatheringContainerProps) => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export const BillGatheringContainer = ErrorBoundary.with(
+ { fallback: <>> },
+ ({ gatheringId }: BillGatheringContainerProps) => (
+
}>
+ <_BillGatheringContainer gatheringId={gatheringId} />
+
+ ),
+);
diff --git a/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-filter.tsx b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-filter.tsx
new file mode 100644
index 00000000..2e729ad4
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-filter.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { Tabs, TabsList, TabsTrigger } from '@dpm-core/shared';
+import { useGatheringStatusSearchParams } from '../_hooks/use-gathering-status-search-params';
+import { ATTEND_STATUS } from '../const/attend-status';
+
+const TABS_FILTER = [
+ {
+ label: '제출 전체',
+ value: ATTEND_STATUS.ALL,
+ },
+ {
+ label: '참석함',
+ value: ATTEND_STATUS.ATTEND,
+ },
+ {
+ label: '참석 안함',
+ value: ATTEND_STATUS.ABSENT,
+ },
+];
+
+export const BillGatheringFilter = () => {
+ const { attendStatus, handleChange } = useGatheringStatusSearchParams();
+
+ return (
+
+
+ {TABS_FILTER.map(({ value, label }) => {
+ return (
+
+ {label}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-item.tsx b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-item.tsx
new file mode 100644
index 00000000..438ba2ec
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-item.tsx
@@ -0,0 +1,44 @@
+import type { Part } from '@dpm-core/api';
+import { Badge, CircleCheck, CircleX } from '@dpm-core/shared';
+import { Profile } from '@/components/attendance/profile';
+
+interface BillGatheringItemProps {
+ member: {
+ name: string;
+ authority: string;
+ part: Exclude
;
+ teamNumber: number;
+ isJoined: boolean;
+ };
+}
+
+export const BillGatheringItem = ({ member }: BillGatheringItemProps) => {
+ return (
+
+ );
+};
+
+interface GatheringStatusBadgeProps {
+ isJoined: boolean;
+}
+
+const GatheringStatusBadge = ({ isJoined }: GatheringStatusBadgeProps) => {
+ return (
+
+ {isJoined ? (
+ <>
+
+ 참석함
+ >
+ ) : (
+ <>
+
+ 참석 안함
+ >
+ )}
+
+ );
+};
diff --git a/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-list.tsx b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-list.tsx
new file mode 100644
index 00000000..20a40ba8
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_components/bill-gathering-list.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import { useSuspenseQuery } from '@tanstack/react-query';
+import { getGatheringMembersQueryOptions } from '@/remotes/queries/gathering';
+import { useGatheringStatusSearchParams } from '../_hooks/use-gathering-status-search-params';
+import { BillGatheringFilter } from './bill-gathering-filter';
+import { BillGatheringItem } from './bill-gathering-item';
+
+interface BillGatheringListProps {
+ gatheringId: number;
+}
+export const BillGatheringList = ({ gatheringId }: BillGatheringListProps) => {
+ const {
+ data: { data: gatheringMemberList },
+ } = useSuspenseQuery(getGatheringMembersQueryOptions({ gatheringId }));
+
+ const { attendStatus } = useGatheringStatusSearchParams();
+
+ const filteredMember = gatheringMemberList.members.filter((member) => {
+ if (attendStatus === 'ATTEND') {
+ return member.isJoined;
+ }
+ if (attendStatus === 'ABSENT') {
+ return !member.isJoined;
+ }
+ if (attendStatus === 'ALL') return true;
+ });
+
+ return (
+
+
+
+ {filteredMember.map((member) => (
+ -
+
+
+ ))}
+
+
+ );
+};
diff --git a/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_hooks/use-gathering-status-search-params.ts b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_hooks/use-gathering-status-search-params.ts
new file mode 100644
index 00000000..37ad4868
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/_hooks/use-gathering-status-search-params.ts
@@ -0,0 +1,20 @@
+import { useRouter, useSearchParams } from 'next/navigation';
+import z from 'zod';
+import { ATTEND_STATUS } from './../const/attend-status';
+
+const attendStatusSchema = z.enum(ATTEND_STATUS).catch(ATTEND_STATUS.ALL);
+
+export const useGatheringStatusSearchParams = () => {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+
+ const attendStatus = attendStatusSchema.parse(searchParams.get('attendStatus'));
+
+ const handleFilterStatus = (value: string) => {
+ const newSearchParams = new URLSearchParams(searchParams);
+ newSearchParams.set('attendStatus', value);
+ router.replace(`?${newSearchParams.toString()}`);
+ };
+
+ return { attendStatus, handleChange: handleFilterStatus };
+};
diff --git a/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/const/attend-status.ts b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/const/attend-status.ts
new file mode 100644
index 00000000..2405ec9a
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/const/attend-status.ts
@@ -0,0 +1,5 @@
+export const ATTEND_STATUS = {
+ ALL: 'ALL',
+ ATTEND: 'ATTEND',
+ ABSENT: 'ABSENT',
+} as const;
diff --git a/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/page.tsx b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/page.tsx
new file mode 100644
index 00000000..0cf94c02
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/gatherings/[gatheringId]/page.tsx
@@ -0,0 +1,10 @@
+import { BillGatheringContainer } from './_components/bill-gathering-container';
+
+interface Props {
+ params: Promise<{ gatheringId: string }>;
+}
+export default async function BillsDetailPage({ params }: Props) {
+ const { gatheringId } = await params;
+
+ return ;
+}
diff --git a/apps/admin/app/(auth)/bills/layout.tsx b/apps/admin/app/(auth)/bills/layout.tsx
new file mode 100644
index 00000000..2188fab4
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/layout.tsx
@@ -0,0 +1,7 @@
+import { AppLayout } from '@dpm-core/shared';
+
+const BillsLayout = async ({ children }: { children: React.ReactNode }) => {
+ return {children};
+};
+
+export default BillsLayout;
diff --git a/apps/admin/app/(auth)/bills/page.tsx b/apps/admin/app/(auth)/bills/page.tsx
new file mode 100644
index 00000000..3da02970
--- /dev/null
+++ b/apps/admin/app/(auth)/bills/page.tsx
@@ -0,0 +1,11 @@
+import { AppHeader } from '@/components/app-header';
+import { BillList } from './_components/bill-list';
+
+export default function BillsMainPage() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/apps/admin/components/app-header.tsx b/apps/admin/components/app-header.tsx
index 6ba35fd5..b306cd62 100644
--- a/apps/admin/components/app-header.tsx
+++ b/apps/admin/components/app-header.tsx
@@ -13,7 +13,9 @@ interface AppHeaderProps {
const AppHeader = ({ title, backHref, className }: AppHeaderProps) => {
const router = useRouter();
return (
-
+
{
@@ -26,7 +28,7 @@ const AppHeader = ({ title, backHref, className }: AppHeaderProps) => {
{title}
-
+
);
};
diff --git a/apps/admin/components/settle/_mock/image.svg b/apps/admin/components/settle/_mock/image.svg
deleted file mode 100644
index 72c166ac..00000000
--- a/apps/admin/components/settle/_mock/image.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/apps/admin/components/settle/_mock/index.ts b/apps/admin/components/settle/_mock/index.ts
deleted file mode 100644
index 65f708de..00000000
--- a/apps/admin/components/settle/_mock/index.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export type SettleStatus = 'before' | 'progress' | 'done';
-
-export interface SettleItem {
- id: string;
- title: string;
- date: string;
- round?: string;
- count?: string;
- status: SettleStatus;
-}
-
-export const settleMockData: SettleItem[] = [
- {
- id: '1',
- title: '코어 1기 UT 후 회식',
- date: '25년 8월 12일 (토)',
- round: '~3차',
- count: '27/48명 제출',
- status: 'before',
- },
- {
- id: '2',
- title: '코어 1기 UT 후 회식',
- date: '25년 8월 12일 (토)',
- round: '~3차',
- count: '48명',
- status: 'progress',
- },
- {
- id: '3',
- title: '코어 1기 UT 후 회식',
- date: '25년 8월 12일 (토)',
- round: '~3차',
- count: '대상자 48명',
- status: 'done',
- },
-];
diff --git a/apps/admin/components/settle/create/CreateBottomSheet.tsx b/apps/admin/components/settle/create/CreateBottomSheet.tsx
deleted file mode 100644
index 1a8da359..00000000
--- a/apps/admin/components/settle/create/CreateBottomSheet.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-const CreateBottomSheet = () => {
- return (
-
-
-
-
생성 전 확인
-
- 정산서를 생성하시겠어요? MVP 단계에서는 생성 후 수정이
어려우니, 한 번 더 확인해
- 주세요.
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default CreateBottomSheet;
diff --git a/apps/admin/components/settle/create/CreateFormWrapper.tsx b/apps/admin/components/settle/create/CreateFormWrapper.tsx
deleted file mode 100644
index 0a344add..00000000
--- a/apps/admin/components/settle/create/CreateFormWrapper.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-import FormDate from './FormDate';
-import FormDesc from './FormDesc';
-import FormRange from './FormRange';
-import FormTitle from './FormTitle';
-
-const CreateFormWrapper = () => {
- return (
- <>
-
-
기본 정보
-
-
-
-
-
-
-
- >
- );
-};
-
-export default CreateFormWrapper;
diff --git a/apps/admin/components/settle/create/FormDate.tsx b/apps/admin/components/settle/create/FormDate.tsx
deleted file mode 100644
index b5fd933b..00000000
--- a/apps/admin/components/settle/create/FormDate.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-const FormDate = () => {
- return (
-
- );
-};
-
-export default FormDate;
diff --git a/apps/admin/components/settle/create/FormDesc.tsx b/apps/admin/components/settle/create/FormDesc.tsx
deleted file mode 100644
index cd39f256..00000000
--- a/apps/admin/components/settle/create/FormDesc.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-const FormDesc = () => {
- return (
-
-
- 정산서 이름
- (선택)
-
-
-
0/50자
-
- );
-};
-
-export default FormDesc;
diff --git a/apps/admin/components/settle/create/FormStepItem.tsx b/apps/admin/components/settle/create/FormStepItem.tsx
deleted file mode 100644
index 7dc581b6..00000000
--- a/apps/admin/components/settle/create/FormStepItem.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-const FormStepItem = () => {
- return (
-
-
- {/* 닫기 버튼 */}
-
-
- {/* 차수 정보 */}
-
-
-
- {/* 금액 */}
-
-
- );
-};
-
-export default FormStepItem;
diff --git a/apps/admin/components/settle/create/FormStepList.tsx b/apps/admin/components/settle/create/FormStepList.tsx
deleted file mode 100644
index daf51d10..00000000
--- a/apps/admin/components/settle/create/FormStepList.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-import FormStepItem from './FormStepItem';
-
-const FormStepList = () => {
- return (
-
- );
-};
-
-export default FormStepList;
diff --git a/apps/admin/components/settle/create/FormTitle.tsx b/apps/admin/components/settle/create/FormTitle.tsx
deleted file mode 100644
index 63a98593..00000000
--- a/apps/admin/components/settle/create/FormTitle.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client';
-
-const FormTitle = () => {
- return (
-
- );
-};
-
-export default FormTitle;
diff --git a/apps/admin/components/settle/detail/AttendList.tsx b/apps/admin/components/settle/detail/AttendList.tsx
deleted file mode 100644
index ac4861eb..00000000
--- a/apps/admin/components/settle/detail/AttendList.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from 'react';
-
-const AttendList = () => {
- return (
-
- {[1, 2, 3, 4, 5].map((item) => (
- -
-
-
-
- ))}
-
- {[1, 2, 3, 4, 5].map((item) => (
- -
-
-
-
- ))}
-
- );
-};
-
-export default AttendList;
diff --git a/apps/admin/components/settle/detail/DetailBottom.tsx b/apps/admin/components/settle/detail/DetailBottom.tsx
deleted file mode 100644
index 314d873b..00000000
--- a/apps/admin/components/settle/detail/DetailBottom.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-import DetailStep from './DetailStep';
-
-const DetailBottom = () => {
- return (
-
-
회식 참석 현황
-
- {[1, 2, 3, 4, 5].map((item) => (
-
- ))}
-
-
- );
-};
-
-export default DetailBottom;
diff --git a/apps/admin/components/settle/detail/DetailBottomSheet.tsx b/apps/admin/components/settle/detail/DetailBottomSheet.tsx
deleted file mode 100644
index 8bf09688..00000000
--- a/apps/admin/components/settle/detail/DetailBottomSheet.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React from 'react';
-
-const DetailBottomSheet = () => {
- return (
-
-
-
-
제출 전 확인
-
- -
- - 멤버 확정 후에는 참석 여부를 수정할 수
- 없습니다.
-
- - - 수정이 필요한 경우, 정산서를 삭제하고 새로 만들어야 합니다.
- - - 확정 전에는 꼭 한 번 더 확인해 주세요.
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default DetailBottomSheet;
diff --git a/apps/admin/components/settle/detail/DetailStateHeader.tsx b/apps/admin/components/settle/detail/DetailStateHeader.tsx
deleted file mode 100644
index 8d53f238..00000000
--- a/apps/admin/components/settle/detail/DetailStateHeader.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import { usePathname, useRouter } from 'next/navigation';
-
-const DetailStateHeader = () => {
- const router = useRouter();
- const pathname = usePathname();
-
- const headerTitle = pathname?.includes('/submit') ? '제출 현황' : '참석 현황';
- return (
-
-
-
- #일이삼사오육칠팔구십 {headerTitle}
-
-
-
-
- );
-};
-
-export default DetailStateHeader;
diff --git a/apps/admin/components/settle/detail/DetailStep.tsx b/apps/admin/components/settle/detail/DetailStep.tsx
deleted file mode 100644
index 07f604cb..00000000
--- a/apps/admin/components/settle/detail/DetailStep.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-import { useParams } from 'next/navigation';
-
-const DetailStep = () => {
- const { settleId } = useParams();
- return (
-
- #회식 차수 이름
-
-
- {/* 금액 */}
-
-
금액
-
-
총 800,000원
-
/인당 금액 미정
-
-
-
- {/* 인원 */}
-
-
-
- {/* 참석자 리스트 확인 */}
-
-
-
-
참석자 리스트 확인
-
-
-
-
-
- );
-};
-
-export default DetailStep;
diff --git a/apps/admin/components/settle/detail/DetailTop.tsx b/apps/admin/components/settle/detail/DetailTop.tsx
deleted file mode 100644
index d8c007ea..00000000
--- a/apps/admin/components/settle/detail/DetailTop.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-import { useParams } from 'next/navigation';
-
-const DetailTop = () => {
- const { settleId } = useParams();
- return (
-
-
-
-
-
- 초대 멤버 전원이 참석 여부를 제출하면
- 멤버를 확정할 수 있어요.
-
-
-
-
- {/* 회식 이름 */}
-
17기 OT세션 공식 회식
-
- {/* 회식 설명 */}
-
- 회식설명 어쩌고 저쩌고 회식설명 어쩌고 저쩌고 회식설명 어쩌고 저쩌고 회식설명 어쩌고
- 저쩌고 회식설명 어쩌고 저쩌고 회식설명 어쩌고 저쩌고 회식설명 어쩌고 저쩌고 회식설명
- 어쩌고 저쩌고 회식설명 어쩌고 저쩌고
-
-
- {/* 회식 날짜, 범위 */}
-
- -
-
회식 날짜
- 25년 00월 00일 (토)
-
- -
-
초대 범위
-
-
-
-
- {/* 회식 인원 */}
-
-
-
-
-
-
- );
-};
-
-export default DetailTop;
diff --git a/apps/admin/components/settle/detail/SubmitList.tsx b/apps/admin/components/settle/detail/SubmitList.tsx
deleted file mode 100644
index 70022c96..00000000
--- a/apps/admin/components/settle/detail/SubmitList.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react';
-
-const SubmitList = () => {
- return (
-
-
- {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item, index) => (
- -
-
-
-
-
- ))}
-
-
- );
-};
-
-export default SubmitList;
diff --git a/apps/admin/components/settle/layout/CopyComplete.tsx b/apps/admin/components/settle/layout/CopyComplete.tsx
deleted file mode 100644
index 947c7e7e..00000000
--- a/apps/admin/components/settle/layout/CopyComplete.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-
-const CopyComplete = () => {
- return (
-
-
-
-
최종 정산 링크를 복사했습니다.
-
-
- );
-};
-
-export default CopyComplete;
diff --git a/apps/admin/components/settle/layout/CreateSettleBtn.tsx b/apps/admin/components/settle/layout/CreateSettleBtn.tsx
deleted file mode 100644
index 3a497909..00000000
--- a/apps/admin/components/settle/layout/CreateSettleBtn.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-const CreateSettleBtn = () => {
- const router = useRouter();
- const handleCreateSettle = () => {
- router.push('/settle/create');
- };
- return (
-
- );
-};
-
-export default CreateSettleBtn;
diff --git a/apps/admin/components/settle/layout/Devider.tsx b/apps/admin/components/settle/layout/Devider.tsx
deleted file mode 100644
index 2db9aa3c..00000000
--- a/apps/admin/components/settle/layout/Devider.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-const Devider = () => {
- return ;
-};
-
-export default Devider;
diff --git a/apps/admin/components/settle/layout/FixedBottomBtn.tsx b/apps/admin/components/settle/layout/FixedBottomBtn.tsx
deleted file mode 100644
index 9e951978..00000000
--- a/apps/admin/components/settle/layout/FixedBottomBtn.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-
-interface FixedBottomBtnProps {
- title: string;
- onClick?: () => void;
-}
-
-const FixedBottomBtn = ({ title, onClick }: FixedBottomBtnProps) => {
- return (
-
- );
-};
-
-export default FixedBottomBtn;
diff --git a/apps/admin/components/settle/layout/SettleHeader.tsx b/apps/admin/components/settle/layout/SettleHeader.tsx
deleted file mode 100644
index d7aec3b1..00000000
--- a/apps/admin/components/settle/layout/SettleHeader.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-'use client';
-
-import { usePathname, useRouter } from 'next/navigation';
-import React from 'react';
-import SettleHeaderFilter from './SettleHeaderFilter';
-
-interface HeaderConfig {
- title: string;
- withFilter: boolean;
-}
-
-const settleHeaderMap: Record = {
- '/settle': {
- title: '정산',
- withFilter: true,
- },
- '/settle/create': {
- title: '정산서 만들기',
- withFilter: false,
- },
- '/settle/create/done': {
- title: '최종 정산서',
- withFilter: false,
- },
-};
-
-const SettleHeader = () => {
- const pathname = usePathname();
- const router = useRouter();
-
- const config = settleHeaderMap[pathname] ?? {
- title: '정산',
- withToggle: true,
- };
-
- return (
-
- {/* header content */}
-
-
-
{config.title}
-
-
- {/* toggle group */}
- {config.withFilter && }
-
- );
-};
-
-export default SettleHeader;
diff --git a/apps/admin/components/settle/layout/SettleHeaderFilter.tsx b/apps/admin/components/settle/layout/SettleHeaderFilter.tsx
deleted file mode 100644
index 322d2434..00000000
--- a/apps/admin/components/settle/layout/SettleHeaderFilter.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-'use client';
-
-import { useSettleFilterStore } from '@/store/useSettleFilterStore';
-import * as ToggleGroup from '@radix-ui/react-toggle-group';
-import clsx from 'clsx';
-
-const FILTERS = [
- { value: 'all', label: '전체' },
- { value: 'before', label: '멤버 확정 전' },
- { value: 'progress', label: '정산 중' },
- { value: 'done', label: '정산 끝' },
-] as const;
-
-type FilterValue = (typeof FILTERS)[number]['value'];
-
-export default function SettleHeaderFilter() {
- const { filter, setFilter } = useSettleFilterStore();
-
- return (
-
- v && setFilter(v as FilterValue)}
- className="flex gap-2"
- >
- {FILTERS.map(({ value: v, label }) => (
-
- {label}
-
- ))}
-
-
- );
-}
diff --git a/apps/admin/components/settle/main/NoSettle.tsx b/apps/admin/components/settle/main/NoSettle.tsx
deleted file mode 100644
index 40f3e89d..00000000
--- a/apps/admin/components/settle/main/NoSettle.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-
-const NoSettle = () => {
- return (
-
- );
-};
-
-export default NoSettle;
diff --git a/apps/admin/components/settle/main/SettleList.tsx b/apps/admin/components/settle/main/SettleList.tsx
deleted file mode 100644
index e3ad36c5..00000000
--- a/apps/admin/components/settle/main/SettleList.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ErrorBoundary, Suspense } from '@suspensive/react';
-import { useSuspenseQuery } from '@tanstack/react-query';
-import { ErrorBox } from '@/components/error-box';
-import { LoadingBox } from '@/components/loading-box';
-import { getBiilsQueryOptions } from '@/remotes/queries/bill';
-import SettleListItem1 from './SettleListItem1';
-import SettleListItem2 from './SettleListItem2';
-import SettleListItem3 from './SettleListItem3';
-
-const SettlementListContainer = () => {
- const { data } = useSuspenseQuery(getBiilsQueryOptions);
-
- return (
-
- {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
-
- ))}
-
-
-
- );
-};
-
-const SettleList = ErrorBoundary.with(
- {
- fallback: (props) => props.reset()} />,
- },
- () => (
- }>
-
-
- ),
-);
-
-export default SettleList;
diff --git a/apps/admin/components/settle/main/SettleListItem1.tsx b/apps/admin/components/settle/main/SettleListItem1.tsx
deleted file mode 100644
index cd1b96a3..00000000
--- a/apps/admin/components/settle/main/SettleListItem1.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-const SettleListItem1 = () => {
- return (
-
- {/* 왼쪽 */}
-
-
- {/* 오른쪽 */}
-
-
코어 1기 UT 후 회식
-
- {/* 날짜 */}
-
-
-
25년 8월 12일 (토)
-
-
- {/* 차수, 인원 */}
-
- {/* 차수 */}
-
-
- {/* Devider */}
-
-
- {/* 인원 */}
-
-
-
27/48명 제출
-
-
-
-
-
- );
-};
-
-export default SettleListItem1;
diff --git a/apps/admin/components/settle/main/SettleListItem2.tsx b/apps/admin/components/settle/main/SettleListItem2.tsx
deleted file mode 100644
index 48907d0e..00000000
--- a/apps/admin/components/settle/main/SettleListItem2.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-const SettleListItem2 = () => {
- return (
-
- {/* 왼쪽 */}
-
-
- {[1, 2, 3].map((item, index) => (
-
- ))}
-
-
정산 중
-
-
- {/* 오른쪽 */}
-
-
코어 1기 UT 후 회식
-
- {/* 날짜 */}
-
-
-
25년 8월 12일 (토)
-
-
- {/* 차수, 인원 */}
-
- {/* 차수 */}
-
-
- {/* Devider */}
-
-
- {/* 인원 */}
-
-
-
-
-
- );
-};
-
-export default SettleListItem2;
diff --git a/apps/admin/components/settle/main/SettleListItem3.tsx b/apps/admin/components/settle/main/SettleListItem3.tsx
deleted file mode 100644
index 3f4eed0c..00000000
--- a/apps/admin/components/settle/main/SettleListItem3.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-const SettleListItem3 = () => {
- return (
-
- {/* 왼쪽 */}
-
-
- {/* 오른쪽 */}
-
-
코어 1기 UT 후 회식
-
- {/* 날짜 */}
-
-
-
25년 8월 12일 (토)
-
-
- {/* 차수, 인원 */}
-
- {/* 차수 */}
-
-
- {/* Devider */}
-
-
- {/* 인원 */}
-
-
-
대상자 48명
-
-
-
-
-
- );
-};
-
-export default SettleListItem3;
diff --git a/apps/admin/components/settle/templates/AttendTemplate.tsx b/apps/admin/components/settle/templates/AttendTemplate.tsx
deleted file mode 100644
index 8fb6dd06..00000000
--- a/apps/admin/components/settle/templates/AttendTemplate.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import AttendList from '../detail/AttendList';
-
-export default function AttendTemplete() {
- type FilterType = 'all' | 'attend' | 'absent';
-
- const [currentFilter, setCurrentFilter] = useState('all');
- return (
- <>
-
-
- {(
- [
- { label: '제출 전체', value: 'all' },
- { label: '참석함', value: 'attend' },
- { label: '참석 안함', value: 'absent' },
- ] as { label: string; value: FilterType }[]
- ).map((filter) => (
-
- ))}
-
-
-
-
- >
- );
-}
diff --git a/apps/admin/components/settle/templates/CreateTemplate.tsx b/apps/admin/components/settle/templates/CreateTemplate.tsx
deleted file mode 100644
index 2b9a0bd6..00000000
--- a/apps/admin/components/settle/templates/CreateTemplate.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-import CreateFormWrapper from '../create/CreateFormWrapper';
-import StepWrapper from '../create/StepWrapper';
-import Devider from '../layout/Devider';
-import FixedBottomBtn from '../layout/FixedBottomBtn';
-
-export default async function CreateTemplate() {
- return (
- <>
-
-
-
-
- {/* */}
- >
- );
-}
diff --git a/apps/admin/components/settle/templates/DetailTemplate.tsx b/apps/admin/components/settle/templates/DetailTemplate.tsx
deleted file mode 100644
index cd9fadca..00000000
--- a/apps/admin/components/settle/templates/DetailTemplate.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import DetailBottom from '../detail/DetailBottom';
-import DetailBottomSheet from '../detail/DetailBottomSheet';
-import DetailTop from '../detail/DetailTop';
-import Devider from '../layout/Devider';
-import FixedBottomBtn from '../layout/FixedBottomBtn';
-
-interface DetailTemplateProps {
- settleId: string;
-}
-
-export default async function DetailTemplate({ settleId }: DetailTemplateProps) {
- return (
- <>
-
-
-
-
- {/* */}
- >
- );
-}
diff --git a/apps/admin/components/settle/templates/DoneTemplate.tsx b/apps/admin/components/settle/templates/DoneTemplate.tsx
deleted file mode 100644
index 7518d755..00000000
--- a/apps/admin/components/settle/templates/DoneTemplate.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import React from 'react';
-import CopyComplete from '../layout/CopyComplete';
-import FixedBottomBtn from '../layout/FixedBottomBtn';
-
-interface DoneTemplateProps {
- title: string;
- desc: string;
- btnTitle: string;
- link: string;
-}
-
-export default async function DoneTemplate() {
- return (
-
-
-
-
-
-
참여 여부 제출 링크 생성됨
-
- 멤버들이 참석 여부를 제출할 수 있도록
링크를 복사해 공유해 주세요.
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/admin/components/settle/templates/MainTemplete.tsx b/apps/admin/components/settle/templates/MainTemplete.tsx
deleted file mode 100644
index dc206431..00000000
--- a/apps/admin/components/settle/templates/MainTemplete.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-'use client';
-
-import {
- SettlementTypeFilter,
- SettlementTypeFilterItem,
-} from '@/app/(auth)/(settlement)/settlement/_components/settlement-type-filter';
-import SettleList from '../main/SettleList';
-
-export default function MainTemplete() {
- return (
- <>
-
-
-
-
-
-
-
- {/* */}
- >
- );
-}
-
-{
- /* */
-}
diff --git a/apps/admin/components/settle/templates/SubmitTemplate.tsx b/apps/admin/components/settle/templates/SubmitTemplate.tsx
deleted file mode 100644
index b05c2a32..00000000
--- a/apps/admin/components/settle/templates/SubmitTemplate.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import SubmitList from '../detail/SubmitList';
-
-export default function SubmitTemplete() {
- const [currentFilter, setCurrentFilter] = useState<'submitted' | 'unsubmitted'>('submitted');
-
- return (
- <>
-
-
- 초대 멤버 전원이 참석 여부를 제출하면 멤버를 확정할 수 있어요.
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/admin/lib/date.ts b/apps/admin/lib/date.ts
index ff519ae1..e95981cc 100644
--- a/apps/admin/lib/date.ts
+++ b/apps/admin/lib/date.ts
@@ -35,3 +35,7 @@ export const formatISOStringToCompactDateString = (isoString: string) => {
export const formatISOStringHHMM = (isoString: string) => {
return dayjs(isoString).format('HH:mm');
};
+
+export const formatISOStringToCompactYearDate = (isoString: string) => {
+ return dayjs(isoString).format('YY년 MM월 DD일 (dd)');
+};
diff --git a/apps/admin/remotes/mutations/bill.ts b/apps/admin/remotes/mutations/bill.ts
new file mode 100644
index 00000000..4ebed497
--- /dev/null
+++ b/apps/admin/remotes/mutations/bill.ts
@@ -0,0 +1,22 @@
+import { type ApiResponse, type Bill, bill, type CreateBillParams } from '@dpm-core/api';
+import { type MutationOptions, mutationOptions } from '@tanstack/react-query';
+
+const MUTATE_KEY = 'BILL';
+
+export const createBillMutationOptions = (
+ options?: MutationOptions>, unknown, CreateBillParams, unknown>,
+) =>
+ mutationOptions({
+ mutationKey: [MUTATE_KEY],
+ mutationFn: (params: CreateBillParams) => bill.createBill(params),
+ ...options,
+ });
+
+export const closeBillParticipationMutationOptions = (
+ options?: MutationOptions,
+) =>
+ mutationOptions({
+ mutationKey: [MUTATE_KEY],
+ mutationFn: (params: { billId: number }) => bill.closeBillParticipation(params),
+ ...options,
+ });
diff --git a/apps/admin/remotes/queries/bill.ts b/apps/admin/remotes/queries/bill.ts
index c0244cf3..1eacb35f 100644
--- a/apps/admin/remotes/queries/bill.ts
+++ b/apps/admin/remotes/queries/bill.ts
@@ -1,7 +1,31 @@
import { bill } from '@dpm-core/api';
import { queryOptions } from '@tanstack/react-query';
-export const getBiilsQueryOptions = queryOptions({
+export const getBillsQueryOptions = queryOptions({
queryKey: ['bills'],
queryFn: bill.getBiils,
});
+
+export const getBillDetailByIdQueryOptions = (id: number) =>
+ queryOptions({
+ queryKey: ['bill', id],
+ queryFn: () => bill.getBillDetailById(id),
+ });
+
+export const getBillSubmittedMemberByIdQueryOptions = (id: number) =>
+ queryOptions({
+ queryKey: ['bills', id, 'submitted-member'],
+ queryFn: () => bill.getBillSubmittedMembersById(id),
+ });
+
+export const getBillFinalAmountMemberByIdQueryOptions = (id: number) =>
+ queryOptions({
+ queryKey: ['bills', id, 'members'],
+ queryFn: () => bill.getBillFinalAmountByMember(id),
+ });
+
+export const getBillAccountbyId = (accountId: number) =>
+ queryOptions({
+ queryKey: ['bills', 'account', accountId],
+ queryFn: () => bill.getBillAccountById(accountId),
+ });
diff --git a/apps/admin/remotes/queries/gathering.ts b/apps/admin/remotes/queries/gathering.ts
new file mode 100644
index 00000000..3e1f6205
--- /dev/null
+++ b/apps/admin/remotes/queries/gathering.ts
@@ -0,0 +1,8 @@
+import { gathering } from '@dpm-core/api';
+import { queryOptions } from '@tanstack/react-query';
+
+export const getGatheringMembersQueryOptions = (params: { gatheringId: number }) =>
+ queryOptions({
+ queryKey: ['gatherings', params.gatheringId],
+ queryFn: async () => gathering.getGatheringMembers(params),
+ });
diff --git a/packages/api/src/bill/remote.ts b/packages/api/src/bill/remote.ts
index 16fbd54f..bfd2d3d1 100644
--- a/packages/api/src/bill/remote.ts
+++ b/packages/api/src/bill/remote.ts
@@ -1,5 +1,12 @@
import { http } from '../http';
-import type { Bill, GatheringJoin } from './types';
+import type {
+ Bill,
+ BillAccount,
+ CreateBillParams,
+ FinalAmountByMember,
+ GatheringJoin,
+ SubmittedMember,
+} from './types';
interface GetBillsResponse {
bills: Bill[];
@@ -7,6 +14,16 @@ interface GetBillsResponse {
type GetBillDetailByIdResponse = Bill;
+type GetBillAccountReponse = BillAccount;
+
+interface BillSubmiitedMemberResponse {
+ members: SubmittedMember[];
+}
+
+interface BillFinalAmountByMemberResponse {
+ members: FinalAmountByMember[];
+}
+
export const bill = {
getBiils: async () => {
const res = await http.get('v1/bills');
@@ -18,6 +35,33 @@ export const bill = {
return res;
},
+ createBill: async (json: CreateBillParams) => {
+ const res = await http.post>('v1/bills', { json });
+ return res;
+ },
+
+ getBillSubmittedMembersById: async (id: number) => {
+ const res = await http.get(
+ `v1/bills/${id}/members/submitted-members`,
+ );
+ return res;
+ },
+
+ getBillFinalAmountByMember: async (id: number) => {
+ const res = await http.get(`v1/bills/${id}/members`);
+ return res;
+ },
+
+ closeBillParticipation: async ({ billId }: { billId: number }) => {
+ const res = await http.patch(`v1/bills/${billId}/close-participation`);
+ return res;
+ },
+
+ getBillAccountById: async (accountId: number) => {
+ const res = await http.get(`v1/bills/accounts/${accountId}`);
+ return res;
+ },
+
patchBillGatheringJoins: async (id: number, data: GatheringJoin[]) => {
const res = await http.patch(`v1/bills/${id}/join`, { json: { gatheringJoins: data } });
return res;
diff --git a/packages/api/src/bill/types.ts b/packages/api/src/bill/types.ts
index 2ec4563d..0d29bd26 100644
--- a/packages/api/src/bill/types.ts
+++ b/packages/api/src/bill/types.ts
@@ -1,3 +1,5 @@
+import type { Part } from '../member';
+
export interface InviteAuthority {
invitedAuthorityId: number;
authorityName: string;
@@ -34,12 +36,49 @@ export interface Bill {
createdAt: string;
billAccountId: number;
invitedMemberCount: number;
- invitationConfirmedCount: number;
+ invitationSubmittedCount: number;
invitationCheckedMemberCount: number;
inviteAuthorities: InviteAuthority[];
gatherings: Gathering[];
}
+export interface CreateBillParams {
+ title: string;
+ description?: string;
+ billAccountId: number;
+ invitedAuthorityIds: number[];
+ gatherings: {
+ roundNumber: number;
+ heldAt: string;
+ receipt: {
+ amount: number;
+ };
+ }[];
+}
+
+interface Member {
+ name: string;
+ teamNumber: number;
+ authority: string;
+ part: Exclude;
+}
+
+export interface SubmittedMember extends Member {
+ isInvitationSubmitted: boolean;
+}
+
+export interface FinalAmountByMember extends Member {
+ splitAmount: number;
+}
+
+export interface BillAccount {
+ accountHolderName: string;
+ accountType: string;
+ bankName: string;
+ billAccountValue: string;
+ id: number;
+}
+
export interface GatheringJoin {
gatheringId: number;
isJoined: boolean;
diff --git a/packages/api/src/gathering/index.ts b/packages/api/src/gathering/index.ts
new file mode 100644
index 00000000..a7071632
--- /dev/null
+++ b/packages/api/src/gathering/index.ts
@@ -0,0 +1,2 @@
+export * from './remote';
+export type * from './types';
diff --git a/packages/api/src/gathering/remote.ts b/packages/api/src/gathering/remote.ts
new file mode 100644
index 00000000..ff66c45c
--- /dev/null
+++ b/packages/api/src/gathering/remote.ts
@@ -0,0 +1,16 @@
+import { http } from '../http';
+import type { GatheringMember } from './types';
+
+interface GetGatheringMembersResponse {
+ members: GatheringMember[];
+}
+
+export const gathering = {
+ getGatheringMembers: async (params: { gatheringId: number }) => {
+ const { gatheringId } = params;
+ const res = await http.get(
+ `v1/gatherings/${gatheringId}/participant-members`,
+ );
+ return res;
+ },
+};
diff --git a/packages/api/src/gathering/types.ts b/packages/api/src/gathering/types.ts
new file mode 100644
index 00000000..52ded282
--- /dev/null
+++ b/packages/api/src/gathering/types.ts
@@ -0,0 +1,9 @@
+import type { Part } from '../member';
+
+export interface GatheringMember {
+ name: string;
+ authority: string;
+ isJoined: boolean;
+ teamNumber: number;
+ part: Exclude;
+}
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts
index d8b1341c..a3da0d42 100644
--- a/packages/api/src/index.ts
+++ b/packages/api/src/index.ts
@@ -2,6 +2,7 @@ export * from './attendance';
export * from './auth';
export * from './bill';
export * from './constants';
+export * from './gathering';
export * from './http';
export * from './member';
export * from './session';
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 918703d6..2b2f7199 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -26,11 +26,13 @@
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
+ "date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"input-otp": "^1.4.2",
"lucide-react": "^0.514.0",
"motion": "^12.23.0",
"next-themes": "^0.4.6",
+ "react-day-picker": "^9.8.1",
"react-hook-form": "^7.60.0",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1",
diff --git a/packages/shared/src/components/ui/calendar.tsx b/packages/shared/src/components/ui/calendar.tsx
new file mode 100644
index 00000000..d62273a1
--- /dev/null
+++ b/packages/shared/src/components/ui/calendar.tsx
@@ -0,0 +1,176 @@
+'use client';
+
+import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
+import * as React from 'react';
+import { type DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker';
+import { cn } from '../../utils/cn';
+import { Button, buttonVariants } from './button';
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = 'label',
+ buttonVariant = 'none',
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps['variant'];
+}) {
+ const defaultClassNames = getDefaultClassNames();
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className,
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn('w-fit', defaultClassNames.root),
+ months: cn('flex gap-4 flex-col md:flex-row relative', defaultClassNames.months),
+ month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
+ nav: cn(
+ 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
+ defaultClassNames.nav,
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
+ defaultClassNames.button_previous,
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
+ defaultClassNames.button_next,
+ ),
+ month_caption: cn(
+ 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
+ defaultClassNames.month_caption,
+ ),
+ dropdowns: cn(
+ 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
+ defaultClassNames.dropdowns,
+ ),
+ dropdown_root: cn(
+ 'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
+ defaultClassNames.dropdown_root,
+ ),
+ dropdown: cn('absolute bg-popover inset-0 opacity-0', defaultClassNames.dropdown),
+ caption_label: cn(
+ 'select-none font-medium',
+ captionLayout === 'label'
+ ? 'text-sm'
+ : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
+ defaultClassNames.caption_label,
+ ),
+ table: 'w-full border-collapse',
+ weekdays: cn('flex', defaultClassNames.weekdays),
+ weekday: cn(
+ 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
+ defaultClassNames.weekday,
+ ),
+ week: cn('flex w-full mt-2', defaultClassNames.week),
+ week_number_header: cn('select-none w-(--cell-size)', defaultClassNames.week_number_header),
+ week_number: cn(
+ 'text-[0.8rem] select-none text-muted-foreground',
+ defaultClassNames.week_number,
+ ),
+ day: cn(
+ 'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
+ defaultClassNames.day,
+ ),
+ range_start: cn('rounded-l-md bg-accent', defaultClassNames.range_start),
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
+ range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
+ today: cn(
+ 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
+ defaultClassNames.today,
+ ),
+ outside: cn(
+ 'text-muted-foreground aria-selected:text-muted-foreground',
+ defaultClassNames.outside,
+ ),
+ disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
+ hidden: cn('invisible', defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return ;
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === 'left') {
+ return ;
+ }
+
+ if (orientation === 'right') {
+ return ;
+ }
+
+ return ;
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ );
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ );
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames();
+
+ const ref = React.useRef(null);
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus();
+ }, [modifiers.focused]);
+
+ return (
+