From 41ed84a6528cb9c8360cba02c0df94745179e870 Mon Sep 17 00:00:00 2001 From: leejeongho Date: Tue, 5 Aug 2025 22:16:32 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20=EC=A0=95=EC=82=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/app/(auth)/(home)/page.tsx | 5 +- .../(auth)/bills/_components/bill-filter.tsx | 23 ++ .../(auth)/bills/_components/bill-item.tsx | 197 ++++++++++++++++++ .../(auth)/bills/_components/bill-list.tsx | 112 ++++++++++ .../bills/_components/bill-type-filter.tsx | 70 +++++++ .../_hooks/use-bill-status-search-params.ts | 22 ++ apps/admin/app/(auth)/bills/layout.tsx | 7 + apps/admin/app/(auth)/bills/page.tsx | 11 + apps/admin/components/app-header.tsx | 6 +- apps/admin/remotes/queries/bill.ts | 8 +- 10 files changed, 454 insertions(+), 7 deletions(-) create mode 100644 apps/admin/app/(auth)/bills/_components/bill-filter.tsx create mode 100644 apps/admin/app/(auth)/bills/_components/bill-item.tsx create mode 100644 apps/admin/app/(auth)/bills/_components/bill-list.tsx create mode 100644 apps/admin/app/(auth)/bills/_components/bill-type-filter.tsx create mode 100644 apps/admin/app/(auth)/bills/_hooks/use-bill-status-search-params.ts create mode 100644 apps/admin/app/(auth)/bills/layout.tsx create mode 100644 apps/admin/app/(auth)/bills/page.tsx diff --git a/apps/admin/app/(auth)/(home)/page.tsx b/apps/admin/app/(auth)/(home)/page.tsx index d7620b4c..571a59b2 100644 --- a/apps/admin/app/(auth)/(home)/page.tsx +++ b/apps/admin/app/(auth)/(home)/page.tsx @@ -7,7 +7,6 @@ import IconSession from '@/assets/icons/icon_session.png'; import IconSettlement from '@/assets/icons/icon_settlement.png'; import { NavigationBar } from '@/components/navigation-bar'; import { SESSION_ID } from '../(attendance)/attendance/search/@tabs/const/const'; -import { FeatureComingSoon } from './_components/coming-soon'; import { CurrentWeekSession } from './_components/current-week-session'; import { SessionBanner } from './_components/session-banner'; import { SessionCurrentWeekBanner } from './_components/session-current-week-banner'; @@ -39,9 +38,7 @@ const UserPage = () => { href={`/attendance/search/session?week=${SESSION_ID}`} /> - - - +
diff --git a/apps/admin/app/(auth)/bills/_components/bill-filter.tsx b/apps/admin/app/(auth)/bills/_components/bill-filter.tsx new file mode 100644 index 00000000..30868c4e --- /dev/null +++ b/apps/admin/app/(auth)/bills/_components/bill-filter.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { useBillStatusSearchParams } from '../_hooks/use-bill-status-search-params'; +import { BillTypeFilter, BillTypeFilterItem } from './bill-type-filter'; + +const BillFilter = () => { + const { billStatus, handleChange } = useBillStatusSearchParams(); + + return ( + handleChange(filterValue)} + className="flex gap-x-2 py-2.5 px-4" + value={billStatus} + > + + + + + + ); +}; + +export { BillFilter }; diff --git a/apps/admin/app/(auth)/bills/_components/bill-item.tsx b/apps/admin/app/(auth)/bills/_components/bill-item.tsx new file mode 100644 index 00000000..f29067cb --- /dev/null +++ b/apps/admin/app/(auth)/bills/_components/bill-item.tsx @@ -0,0 +1,197 @@ +import type { Bill, BillStatus } from '@dpm-core/api'; +import Link from 'next/link'; + +function formatInvitationCount( + invitationConfirmedCount: number, + invitedMemberCount: number, + billStatus: BillStatus, +) { + switch (billStatus) { + case 'OPEN': + return `${invitationConfirmedCount}/${invitedMemberCount}명 제출`; + case 'IN_PROGRESS': + case 'COMPLETED': + return `대상자 ${invitedMemberCount}명`; + } +} + +function BillItem({ bill }: { bill: Bill }) { + return ( +
  • + + +
    +

    {bill.title}

    +
    +
    + + calendar-01 + + + + {bill.title} + +
    +
    + + bill-01 + + + + + ~{bill.gatherings.length}차 + +
    + + user-01 + + + + + {formatInvitationCount( + bill.invitedMemberCount, + bill.invitedMemberCount, + bill.billStatus, + )} + +
    +
    +
    + +
  • + ); +} + +function BillStatusBadge({ status }: { status: BillStatus }) { + return ( +
    + {(() => { + switch (status) { + case 'OPEN': + return ( + <> + + 멤버 확정 전 + + + + + + 멤버 확정 전 + + + ); + case 'IN_PROGRESS': + return ( + <> + + 정산 중 + + + + + + 정산 중 + + ); + case 'COMPLETED': + return ( + <> + + 정산 끝 + + + + 정산 끝 + + ); + default: + status satisfies never; + return null; + } + })()} +
    + ); +} + +export { BillItem }; diff --git a/apps/admin/app/(auth)/bills/_components/bill-list.tsx b/apps/admin/app/(auth)/bills/_components/bill-list.tsx new file mode 100644 index 00000000..5c90ce78 --- /dev/null +++ b/apps/admin/app/(auth)/bills/_components/bill-list.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { Button, useAppShell } from '@dpm-core/shared'; +import { ErrorBoundary } from '@suspensive/react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { Suspense } from 'react'; +import { createPortal } from 'react-dom'; +import { ErrorBox } from '@/components/error-box'; +import { LoadingBox } from '@/components/loading-box'; +import { getBillsQueryOptions } from '@/remotes/queries/bill'; +import { useBillStatusSearchParams } from '../_hooks/use-bill-status-search-params'; +import { BillFilter } from './bill-filter'; +import { BillItem } from './bill-item'; + +const BillListContainer = () => { + const { billStatus } = useBillStatusSearchParams(); + const { + data: { + data: { bills }, + }, + } = useSuspenseQuery(getBillsQueryOptions); + + const filteredBillsByStatus = + billStatus === 'ALL' ? bills : bills.filter((bill) => bill.billStatus === billStatus); + + return ( + <> + + {filteredBillsByStatus.length > 0 ? ( +
      + {filteredBillsByStatus.map((bill) => { + return ; + })} +
    + ) : ( +
    + + bill-01 + + + +

    아직 정산서가 없어요.

    +
    + )} + + + ); +}; + +const BillLsit = ErrorBoundary.with( + { + fallback: (props) => props.reset()} />, + }, + () => ( + }> + + + ), +); + +export { BillLsit }; + +function AddButton() { + const { ref } = useAppShell(); + + const router = useRouter(); + const handleCreateSettle = () => { + router.push('/bills/create'); + }; + return createPortal( + , + ref.current, + ); +} diff --git a/apps/admin/app/(auth)/bills/_components/bill-type-filter.tsx b/apps/admin/app/(auth)/bills/_components/bill-type-filter.tsx new file mode 100644 index 00000000..55ba7b8f --- /dev/null +++ b/apps/admin/app/(auth)/bills/_components/bill-type-filter.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { cn, createContext } from '@dpm-core/shared'; +import { type ComponentPropsWithoutRef, type PropsWithChildren, useState } from 'react'; + +interface BillTypeFilterProps { + value?: T; + defaultValue?: T; + onChange?: (value: T) => void; +} + +interface BillTypeFilterItemProps { + value: T; + label?: string; +} + +const [BillTypeFilterProvider, useBillTypeFilter] = createContext( + 'Bill-type-filter', + { + value: undefined, + defaultValue: undefined, + onChange: undefined, + }, +); + +const BillTypeFilter = ({ + value, + defaultValue, + onChange, + children, + ...rest +}: PropsWithChildren> & + Omit, 'onChange'>) => { + const [internalValue, setInternalValue] = useState(defaultValue as T); + + const isControlled = value !== undefined; + const currentValue = isControlled ? value : internalValue; + + const handleChange = (newValue: T) => { + if (!isControlled) { + setInternalValue(newValue); + } + onChange?.(newValue); + }; + return ( + void}> +
    {children}
    +
    + ); +}; + +const BillTypeFilterItem = ({ value, label }: BillTypeFilterItemProps) => { + const { value: currentValue, onChange } = useBillTypeFilter(); + const isSelected = currentValue === value; + return ( + + ); +}; + +export { BillTypeFilter, BillTypeFilterItem }; diff --git a/apps/admin/app/(auth)/bills/_hooks/use-bill-status-search-params.ts b/apps/admin/app/(auth)/bills/_hooks/use-bill-status-search-params.ts new file mode 100644 index 00000000..08f4e91e --- /dev/null +++ b/apps/admin/app/(auth)/bills/_hooks/use-bill-status-search-params.ts @@ -0,0 +1,22 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import z from 'zod'; + +const billStatusSchema = z.enum(['ALL', 'OPEN', 'IN_PROGRESS', 'COMPLETED']).catch('ALL'); + +const useBillStatusSearchParams = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const billStatus = billStatusSchema.parse(searchParams.get('billStatus')); + return { + billStatus, + handleChange: (value: string) => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set('billStatus', value); + router.push(`/bills?${newSearchParams.toString()}`); + }, + }; +}; + +export { useBillStatusSearchParams }; 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..2e3b06c7 --- /dev/null +++ b/apps/admin/app/(auth)/bills/page.tsx @@ -0,0 +1,11 @@ +import { AppHeader } from '@/components/app-header'; +import { BillLsit } 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/remotes/queries/bill.ts b/apps/admin/remotes/queries/bill.ts index c0244cf3..052557f6 100644 --- a/apps/admin/remotes/queries/bill.ts +++ b/apps/admin/remotes/queries/bill.ts @@ -1,7 +1,13 @@ 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), + }); From 6dccc1146f240994e646fc3775b4327f5fbd4b9c Mon Sep 17 00:00:00 2001 From: leejeongho Date: Fri, 8 Aug 2025 02:48:24 +0900 Subject: [PATCH 02/18] =?UTF-8?q?chore:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/app/(auth)/bills/_components/bill-list.tsx | 4 ++-- apps/admin/app/(auth)/bills/page.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/admin/app/(auth)/bills/_components/bill-list.tsx b/apps/admin/app/(auth)/bills/_components/bill-list.tsx index 5c90ce78..6287e1eb 100644 --- a/apps/admin/app/(auth)/bills/_components/bill-list.tsx +++ b/apps/admin/app/(auth)/bills/_components/bill-list.tsx @@ -57,7 +57,7 @@ const BillListContainer = () => { ); }; -const BillLsit = ErrorBoundary.with( +const BillList = ErrorBoundary.with( { fallback: (props) => props.reset()} />, }, @@ -68,7 +68,7 @@ const BillLsit = ErrorBoundary.with( ), ); -export { BillLsit }; +export { BillList }; function AddButton() { const { ref } = useAppShell(); diff --git a/apps/admin/app/(auth)/bills/page.tsx b/apps/admin/app/(auth)/bills/page.tsx index 2e3b06c7..3da02970 100644 --- a/apps/admin/app/(auth)/bills/page.tsx +++ b/apps/admin/app/(auth)/bills/page.tsx @@ -1,11 +1,11 @@ import { AppHeader } from '@/components/app-header'; -import { BillLsit } from './_components/bill-list'; +import { BillList } from './_components/bill-list'; export default function BillsMainPage() { return ( <> - + ); } From 1901da508850c643cc5139376944e1cd41d6bf09 Mon Sep 17 00:00:00 2001 From: leejeongho Date: Fri, 8 Aug 2025 02:58:35 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=EC=A0=95=EC=82=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/copy-to-clipboard.tsx | 31 +++ .../[billId]/_components/done-container.tsx | 81 ++++++++ .../_components/floating-button-container.tsx | 16 ++ .../app/(auth)/bills/create/[billId]/page.tsx | 19 ++ .../create/_components/bill-create-button.tsx | 69 +++++++ .../bills/create/_components/bill-form.tsx | 99 ++++++++++ .../create/_components/form/form-date.tsx | 63 +++++++ .../_components/form/form-description.tsx | 36 ++++ .../create/_components/form/form-range.tsx | 20 ++ .../_components/form/form-step-item.tsx | 106 +++++++++++ .../_components/form/form-step-list.tsx | 17 ++ .../create/_components/form/form-title.tsx | 42 +++++ .../bills/create/_components/form/index.ts | 5 + .../_components/information-form-item.tsx | 17 ++ .../create/_components/step-form-item.tsx | 55 ++++++ apps/admin/app/(auth)/bills/create/page.tsx | 11 ++ apps/admin/remotes/mutations/bill.ts | 13 ++ packages/api/src/bill/remote.ts | 7 +- packages/api/src/bill/types.ts | 14 ++ packages/shared/package.json | 2 + .../shared/src/components/ui/calendar.tsx | 176 ++++++++++++++++++ packages/shared/src/components/ui/form.tsx | 2 +- packages/shared/src/components/ui/input.tsx | 8 +- packages/shared/src/index.ts | 1 + yarn.lock | 36 ++++ 25 files changed, 940 insertions(+), 6 deletions(-) create mode 100644 apps/admin/app/(auth)/bills/create/[billId]/_components/copy-to-clipboard.tsx create mode 100644 apps/admin/app/(auth)/bills/create/[billId]/_components/done-container.tsx create mode 100644 apps/admin/app/(auth)/bills/create/[billId]/_components/floating-button-container.tsx create mode 100644 apps/admin/app/(auth)/bills/create/[billId]/page.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/bill-create-button.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/bill-form.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/form/form-date.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/form/form-description.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/form/form-range.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/form/form-step-item.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/form/form-step-list.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/form/form-title.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/form/index.ts create mode 100644 apps/admin/app/(auth)/bills/create/_components/information-form-item.tsx create mode 100644 apps/admin/app/(auth)/bills/create/_components/step-form-item.tsx create mode 100644 apps/admin/app/(auth)/bills/create/page.tsx create mode 100644 apps/admin/remotes/mutations/bill.ts create mode 100644 packages/shared/src/components/ui/calendar.tsx diff --git a/apps/admin/app/(auth)/bills/create/[billId]/_components/copy-to-clipboard.tsx b/apps/admin/app/(auth)/bills/create/[billId]/_components/copy-to-clipboard.tsx new file mode 100644 index 00000000..2a3b0b46 --- /dev/null +++ b/apps/admin/app/(auth)/bills/create/[billId]/_components/copy-to-clipboard.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { cloneElement, isValidElement, type MouseEventHandler, type ReactElement } from 'react'; + +interface CopyToClipboardProps { + text: string; + children: ReactElement<{ onClick?: MouseEventHandler }>; + onCopy?: () => void; +} + +const copyToClipboard = async (text: string) => { + navigator.clipboard.writeText(text); +}; + +export const CopyToClipBoard = ({ text, children, onCopy }: CopyToClipboardProps) => { + const handleCopyClipBoard = async () => { + await copyToClipboard(text); + onCopy?.(); + }; + + if (!isValidElement(children)) { + console.warn( + 'CopyToClipboard 컴포넌트에는 유효한 React 엘리먼트만 children으로 전달할 수 있습니다.', + ); + return null; + } + + return cloneElement(children, { + onClick: handleCopyClipBoard, + }); +}; diff --git a/apps/admin/app/(auth)/bills/create/[billId]/_components/done-container.tsx b/apps/admin/app/(auth)/bills/create/[billId]/_components/done-container.tsx new file mode 100644 index 00000000..20215665 --- /dev/null +++ b/apps/admin/app/(auth)/bills/create/[billId]/_components/done-container.tsx @@ -0,0 +1,81 @@ +'use client'; + +import { Button, toast } from '@dpm-core/shared'; +import { CopyToClipBoard } from './copy-to-clipboard'; + +interface DoneContainerProps { + billId: number; +} + +export const CLIENT_BASE_URL = process.env.NEXT_PUBLIC_CLIENT_BASE_URL; + +export const DoneContainer = (props: DoneContainerProps) => { + const { billId } = props; + + // TODO env로 환경 변수 관리 + const copyLink = `${CLIENT_BASE_URL}/bills/${billId}`; + + return ( +
    +
    + + icon + + +
    +
    +

    참여 여부 제출 링크 생성됨

    +

    + 멤버들이 참석 여부를 제출할 수 있도록
    링크를 복사해 공유해 주세요. +

    +
    + + toast.success('정산 링크를 복사했습니다.')} + > + + +
    +
    +
    + ); +}; diff --git a/apps/admin/app/(auth)/bills/create/[billId]/_components/floating-button-container.tsx b/apps/admin/app/(auth)/bills/create/[billId]/_components/floating-button-container.tsx new file mode 100644 index 00000000..036b45aa --- /dev/null +++ b/apps/admin/app/(auth)/bills/create/[billId]/_components/floating-button-container.tsx @@ -0,0 +1,16 @@ +import { Button } from '@dpm-core/shared'; +import Link from 'next/link'; + +interface FloatingButtonContainer { + billId: number; +} + +export const FloatingButtonContainer = (props: FloatingButtonContainer) => { + const { billId } = props; + + return ( + + ); +}; diff --git a/apps/admin/app/(auth)/bills/create/[billId]/page.tsx b/apps/admin/app/(auth)/bills/create/[billId]/page.tsx new file mode 100644 index 00000000..b2f25d91 --- /dev/null +++ b/apps/admin/app/(auth)/bills/create/[billId]/page.tsx @@ -0,0 +1,19 @@ +import { AppHeader } from '@/components/app-header'; +import { DoneContainer } from './_components/done-container'; +import { FloatingButtonContainer } from './_components/floating-button-container'; + +interface CreateBillsDonePageProps { + params: Promise<{ billId: string }>; +} + +export default async function CreateBillsDonePage({ params }: CreateBillsDonePageProps) { + const { billId } = await params; + + return ( + <> + + + + + ); +} diff --git a/apps/admin/app/(auth)/bills/create/_components/bill-create-button.tsx b/apps/admin/app/(auth)/bills/create/_components/bill-create-button.tsx new file mode 100644 index 00000000..562d5ece --- /dev/null +++ b/apps/admin/app/(auth)/bills/create/_components/bill-create-button.tsx @@ -0,0 +1,69 @@ +import { + Button, + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, + useAppShell, +} from '@dpm-core/shared'; +import { createPortal } from 'react-dom'; + +export const BillCreateButton = ({ + disabled, + isLoading, + ...props +}: React.ComponentProps<'button'> & { isLoading: boolean }) => { + const { ref } = useAppShell(); + + return createPortal( + + + + + + + 생성 전 확인 + + + 정산서를 생성하시겠어요? MVP 단계에서는 생성 후 수정이
    어려우니, 한 번 더 확인해 + 주세요. +
    + +
    + + + + +
    +
    +
    +
    , + ref.current, + ); +}; diff --git a/apps/admin/app/(auth)/bills/create/_components/bill-form.tsx b/apps/admin/app/(auth)/bills/create/_components/bill-form.tsx new file mode 100644 index 00000000..dd8547e6 --- /dev/null +++ b/apps/admin/app/(auth)/bills/create/_components/bill-form.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { Form } from '@dpm-core/shared'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import z from 'zod'; +import Devider from '@/components/settle/layout/Devider'; +import { createBillMutationOptions } from '@/remotes/mutations/bill'; +import { BillCreateButton } from './bill-create-button'; +import { InformationFormItem } from './information-form-item'; +import { StepFormItem } from './step-form-item'; + +const createSettleSchema = z.object({ + title: z.string().min(1, '필수 입력 값입니다.'), + description: z.string().optional(), + heldAt: z.date(), + billAccountId: z.number(), + invitedAuthorityIds: z.array(z.number()), + gatherings: z + .array( + z.object({ + title: z.string().min(1, '필수 입력 값입니다.'), + receipt: z.object({ + amount: z + .string() + .min(1, '필수 입력 값입니다.') + .regex(/^0$|^[1-9]\d{0,2}(,\d{3})*$/, '올바른 금액 형식이어야 합니다.'), + }), + }), + ) + .min(1, '최소 하나의 차수 정보가 필요합니다.'), +}); + +const parseFormattedAmount = (amount: string) => { + return Number(amount.replace(/,/g, '')); +}; + +type CreateSettleSchema = z.infer; + +const FORM_ID = 'create-settle-form'; + +export const BillForm = () => { + const router = useRouter(); + const form = useForm({ + resolver: zodResolver(createSettleSchema), + defaultValues: { + title: '', + heldAt: new Date(), + description: '', + billAccountId: 1, + invitedAuthorityIds: [1, 2], + gatherings: [{ title: '', receipt: { amount: '' } }], + }, + }); + + const { mutate: createBillMutate, isPending } = useMutation( + createBillMutationOptions({ + onSuccess: (response) => { + router.replace(`settle/create/${response.data.billId}`); + }, + onError: () => { + console.log('에러'); + }, + }), + ); + + const handleSubmitSettle = (formData: CreateSettleSchema) => { + const { title, description, heldAt, gatherings, billAccountId, invitedAuthorityIds } = formData; + const params = { + title, + description, + billAccountId, + invitedAuthorityIds, + gatherings: gatherings.map((gathering, index) => ({ + ...gathering, + heldAt: heldAt.toISOString(), + roundNumber: index + 1, + receipt: { amount: parseFormattedAmount(gathering.receipt.amount) }, + })), + }; + + createBillMutate(params); + }; + + const isDisabled = !form.formState.isValid || form.formState.isSubmitting || isPending; + + return ( +
    + + + + + + + + ); +}; diff --git a/apps/admin/app/(auth)/bills/create/_components/form/form-date.tsx b/apps/admin/app/(auth)/bills/create/_components/form/form-date.tsx new file mode 100644 index 00000000..ad67409b --- /dev/null +++ b/apps/admin/app/(auth)/bills/create/_components/form/form-date.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { Calendar, FormControl, FormField, FormItem, FormLabel } from '@dpm-core/shared'; +import { ko } from 'date-fns/locale'; +import { useFormContext } from 'react-hook-form'; + +export const FormDate = () => { + const form = useFormContext(); + + return ( +
    +
    + ( + + + 회식 날짜 + + + date.toLocaleDateString('ko-KR', { month: 'long' }), + formatWeekdayName: (date) => + date.toLocaleDateString('en-US', { weekday: 'short' }).slice(0, 2), + }} + className="px-5 py-4.5 border border-line-subtle rounded-md" + mode="single" + selected={field.value} + onSelect={field.onChange} + disabled={(date) => date < new Date('1900-01-01')} + classNames={{ + root: 'w-full h-full', + months: 'w-full h-full relative', + month: 'flex flex-col w-full gap-3', + month_caption: 'flex items-center justify-center w-full h-7', + caption_label: 'text-body1 text-label-normal font-semibold', + nav: 'absolute top-0 inset-x-0 flex items-center justify-between', + weekdays: + 'grid grid-cols-7 gap-x-[calc((100%-32px*7)/6)] mb-2 place-items-center', + weekday: + 'w-8 h-8 flex items-center justify-center text-body2 font-medium text-label-assistive', + week: 'grid grid-cols-7 gap-x-[calc((100%-32px*7)/6)] mb-2 place-items-center', + day: 'w-8 h-8 flex items-center justify-center text-body2 font-medium text-label-subtle ', + today: 'bg-background-strong text-label-assistive rounded-md', + selected: '!bg-background-inverse rounded-md !text-white', + button_previous: + 'w-7 h-7 border border-line-subtle rounded-md flex items-center justify-center text-label-normal cursor-pointer', + button_next: + 'w-7 h-7 border border-line-subtle rounded-md flex items-center justify-center text-label-normal cursor-pointer', + outside: '!text-label-disable', + }} + /> + + + )} + /> +
    +
    + ); +}; diff --git a/apps/admin/app/(auth)/bills/create/_components/form/form-description.tsx b/apps/admin/app/(auth)/bills/create/_components/form/form-description.tsx new file mode 100644 index 00000000..0c35ce71 --- /dev/null +++ b/apps/admin/app/(auth)/bills/create/_components/form/form-description.tsx @@ -0,0 +1,36 @@ +import { FormControl, FormField, FormItem, FormLabel } from '@dpm-core/shared'; +import { useFormContext, useWatch } from 'react-hook-form'; + +const MAX_LENGTH = 50; +export const FormDescription = () => { + const form = useFormContext(); + const description = useWatch({ control: form.control, name: 'description' }); + + return ( +
    + ( + + + 정산서 설명 + (선택) + + +