Repository: CP-RektMart/pic-me-pls-frontend
Pull request #43: Feature/pic 143 fe integrate customer accept quotation page
Author: login: NhongSun
Created: 2025-03-11T16:04:04Z | Merged: 2025-03-12T09:38:02Z

<img width="1438" alt="image" src="https://github.com/user-attachments/assets/9e32ec8a-1a8f-4080-82c8-16f806905b06" />
<img width="884" alt="image" src="https://github.com/user-attachments/assets/a9044bf5-d9dd-461e-8f63-00d661166277" />



Diff:
diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts
index 69d03c04..406babbd 100644
--- a/src/api/schema.d.ts
+++ b/src/api/schema.d.ts
@@ -1331,7 +1331,48 @@ export interface paths {
       path?: never
       cookie?: never
     }
-    get?: never
+    /**
+     * List Photographer's packages
+     * @description List Photographer's packages
+     */
+    get: {
+      parameters: {
+        query?: never
+        header?: never
+        path?: never
+        cookie?: never
+      }
+      requestBody?: never
+      responses: {
+        /** @description OK */
+        200: {
+          headers: {
+            [name: string]: unknown
+          }
+          content: {
+            'application/json': components['schemas']['dto.HttpListResponse-dto_SmallPackageResponse']
+          }
+        }
+        /** @description Bad Request */
+        400: {
+          headers: {
+            [name: string]: unknown
+          }
+          content: {
+            'application/json': components['schemas']['dto.HttpError']
+          }
+        }
+        /** @description Internal Server Error */
+        500: {
+          headers: {
+            [name: string]: unknown
+          }
+          content: {
+            'application/json': components['schemas']['dto.HttpError']
+          }
+        }
+      }
+    }
     put?: never
     /**
      * Create Package
@@ -1842,6 +1883,9 @@ export interface components {
     'dto.HttpError': {
       error?: string
     }
+    'dto.HttpListResponse-dto_SmallPackageResponse': {
+      result?: components['schemas']['dto.SmallPackageResponse'][]
+    }
     'dto.HttpResponse-PaginationResponse[dto_CategoryResponse]': {
       result?: components['schemas']['dto.PaginationResponse-dto_CategoryResponse']
     }
@@ -1977,9 +2021,11 @@ export interface components {
       rating?: number
     }
     'dto.SmallPackageResponse': {
+      category?: components['schemas']['dto.CategoryResponse']
       description?: string
       id?: number
       name?: string
+      price?: number
     }
     'dto.TagResponse': {
       id?: number
diff --git a/src/app/quotation/[id]/page.tsx b/src/app/quotation/[id]/page.tsx
index 9fe663ec..ef9a333c 100644
--- a/src/app/quotation/[id]/page.tsx
+++ b/src/app/quotation/[id]/page.tsx
@@ -1,3 +1,5 @@
+import { client } from '@/api/client'
+import { calculateDurationFromString, formatDateToString } from '@/lib/utils'
 import { type QuotationStatus } from '@/types/quotation'
 
 import CustomerQuotation from '@/components/customer-quotation/index'
@@ -9,53 +11,54 @@ export default async function Page({
 }) {
   const quotationId = (await params).id
 
-  const mockData = {
-    status: 'Pending',
-    packageName: 'Package A',
-    photographer: {
-      name: 'Pattapol K.',
-      imageUrl: 'https://images.unsplash.com/photo-1517849845537-4d257902454a',
-    },
-    customerName: 'Chanotai K.',
-    from: '20/02/2025 14:00',
-    to: '20/02/2025 19:00',
-    description:
-      'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas augue tortor, luctus eget laoreet et, interdum fringilla urna. ',
-    duration: 5,
-    totalPrice: 2000,
-    images: [
-      {
-        url: 'https://thedaily.case.edu/wp-content/uploads/2023/06/underwater-view.jpg',
-        name: 'Ocean',
-      },
-      {
-        url: 'https://images.unsplash.com/photo-1425913397330-cf8af2ff40a1?q=80&w=3174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
-        name: 'Forrest',
-      },
-      {
-        url: 'https://www.allaboutbirds.org/news/wp-content/uploads/2024/05/549985061-Rufous_Hummingbird-Nathan_Wall.jpg',
-        name: 'Hummingbird',
-      },
-      {
-        url: 'https://media.newyorker.com/photos/660fe3deaba0b51c65382288/master/w_2240,c_limit/Galchen-Eclipse.jpg',
-        name: 'Solar Eclipse',
-      },
-      {
-        url: 'https://media.istockphoto.com/id/1224025134/th/%E0%B8%A3%E0%B8%B9%E0%B8%9B%E0%B8%96%E0%B9%88%E0%B8%B2%E0%B8%A2/%E0%B8%A0%E0%B8%B2%E0%B8%9E%E0%B8%A3%E0%B8%B0%E0%B8%A2%E0%B8%B0%E0%B9%83%E0%B8%81%E0%B8%A5%E0%B9%89%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B8%AA%E0%B9%80%E0%B8%95%E0%B9%87%E0%B8%81%E0%B8%99%E0%B8%B4%E0%B8%A7%E0%B8%A2%E0%B8%AD%E0%B8%A3%E0%B9%8C%E0%B8%81.jpg?s=1024x1024&w=is&k=20&c=r52awidhEO6DXvup515015SCZxavSK2Q-G7OExsLRMU=',
-        name: 'Steak',
-      },
-    ],
+  const { response, data } = await client.GET('/api/v1/quotations/{id}', {
+    params: { path: { id: quotationId } },
+  })
+
+  if (response.status === 404) {
+    return <p>Quotation not found</p>
+  }
+
+  if (response.status !== 200) {
+    return <p>Internal server error</p>
   }
 
+  const quotation = data?.result
+
+  if (
+    !quotation ||
+    !quotation.fromDate ||
+    !quotation.toDate ||
+    !quotation.status
+  ) {
+    return <p>Internal server error</p>
+  }
+
+  const images = quotation.package?.media?.map((media) => ({
+    url: media.pictureUrl || '',
+    name: media.description || '',
+  }))
+
+  const duration = calculateDurationFromString(
+    quotation.fromDate,
+    quotation.toDate
+  )
+
   return (
     <CustomerQuotation
-      photographerName={mockData.photographer.name}
-      quotationImages={mockData.images}
       quotationId={quotationId}
-      quotationStatus={mockData.status as QuotationStatus}
-      photographerImageUrl={mockData.photographer.imageUrl}
-      galleriesNumber={5}
-      {...mockData}
+      quotationStatus={quotation.status as QuotationStatus}
+      packageName={quotation.package?.name || 'Package'}
+      photographerName={quotation.photographer?.name || 'Photographer'}
+      customerName={quotation.customer?.name || 'Customer'}
+      from={formatDateToString(new Date(quotation.fromDate))}
+      to={formatDateToString(new Date(quotation.toDate))}
+      description={quotation.description || ''}
+      duration={duration}
+      totalPrice={quotation.price || 0}
+      photographerImageUrl={quotation.photographer?.profilePictureUrl || ''}
+      packageNumber={quotation.photographer?.packages?.length || 0}
+      quotationImages={images || []}
     />
   )
 }
diff --git a/src/components/customer-quotation/button.tsx b/src/components/customer-quotation/button.tsx
index e6657aa4..24b2ae18 100644
--- a/src/components/customer-quotation/button.tsx
+++ b/src/components/customer-quotation/button.tsx
@@ -15,7 +15,7 @@ export default function QuotationButton({
 }) {
   return (
     <div className='flex flex-row justify-between gap-2'>
-      {status === 'Pending' && (
+      {status === 'PENDING' && (
         <>
           <Button variant='destructive' className='w-full' onClick={onCancel}>
             <p>Cancel</p>
@@ -25,7 +25,7 @@ export default function QuotationButton({
           </Button>
         </>
       )}
-      {status === 'Confirm' && (
+      {status === 'CONFIRM' && (
         <Button className='w-full' onClick={onPay}>
           <p>Pay</p>
         </Button>
diff --git a/src/components/customer-quotation/carousel.tsx b/src/components/customer-quotation/carousel.tsx
index 2cdaa2ff..a8b05832 100644
--- a/src/components/customer-quotation/carousel.tsx
+++ b/src/components/customer-quotation/carousel.tsx
@@ -57,8 +57,8 @@ export default function ImageCarousel({ images }: ImageCarouselProps) {
             </CarouselItem>
           ))}
         </CarouselContent>
-        <CarouselPrevious className='-left-1' variant='ghost' />
-        <CarouselNext className='-right-1' variant='ghost' />
+        <CarouselPrevious variant='ghost' />
+        <CarouselNext variant='ghost' />
       </Carousel>
       <div className='absolute -bottom-4 left-0 right-0 flex justify-center gap-2'>
         {images.map((_, index) => (
diff --git a/src/components/customer-quotation/details.tsx b/src/components/customer-quotation/details.tsx
index 63cad972..5db78d71 100644
--- a/src/components/customer-quotation/details.tsx
+++ b/src/components/customer-quotation/details.tsx
@@ -6,10 +6,10 @@ import { type QuotationDetailsProps } from '@/types/quotation'
 import { Separator } from '@/components/ui/separator'
 
 const statusColor: { [key: string]: string } = {
-  Pending: 'text-amber-600',
-  Confirm: 'text-sky-600',
-  Paid: 'text-emerald-600',
-  Cancelled: 'text-red-600',
+  PENDING: 'text-amber-600',
+  CONFIRM: 'text-sky-600',
+  PAID: 'text-emerald-600',
+  CANCELLED: 'text-red-600',
 }
 
 export default function QuotationDetails({
@@ -37,9 +37,10 @@ export default function QuotationDetails({
     { title: 'From', info: from },
     { title: 'To', info: to },
     { title: 'Description', info: description },
-    { title: 'Duration', info: `${duration} Hour${duration > 1 ? 's' : ''}` },
+    { title: 'Duration', info: duration },
     { title: 'Total Price', info: `${totalPrice} Baht` },
   ]
+
   return (
     <div className='flex justify-center px-4'>
       <div className='flex w-full flex-col'>
diff --git a/src/components/customer-quotation/index.tsx b/src/components/customer-quotation/index.tsx
index f0ec3060..a2c1045a 100644
--- a/src/components/customer-quotation/index.tsx
+++ b/src/components/customer-quotation/index.tsx
@@ -25,13 +25,13 @@ export default function Page({
   duration,
   totalPrice,
   photographerImageUrl,
-  galleriesNumber,
+  packageNumber,
   quotationImages,
 }: CustomerQuotationProps) {
   const [status, setStatus] = useState<QuotationStatus>(quotationStatus)
 
   const handlePayment = () => {
-    setStatus('Paid')
+    setStatus('PAID')
     // TODO: handle payment
   }
 
@@ -43,7 +43,7 @@ export default function Page({
           <ProfileHeader
             imageUrl={photographerImageUrl}
             name={photographerName}
-            galleriesNumber={galleriesNumber}
+            packageNumber={packageNumber}
           />
 
           {/* package details */}
@@ -70,10 +70,10 @@ export default function Page({
           <QuotationButton
             status={status}
             onCancel={() => {
-              setStatus('Cancelled')
+              setStatus('CANCELLED')
             }}
             onConfirm={() => {
-              setStatus('Confirm')
+              setStatus('CONFIRM')
             }}
             onPay={handlePayment}
           />
diff --git a/src/components/customer-quotation/profile-header.tsx b/src/components/customer-quotation/profile-header.tsx
index 4162bcde..cda65d40 100644
--- a/src/components/customer-quotation/profile-header.tsx
+++ b/src/components/customer-quotation/profile-header.tsx
@@ -5,20 +5,20 @@ import { Badge } from '@/components/ui/badge'
 interface ProfileHeaderProps {
   imageUrl: string
   name: string
-  galleriesNumber: number
+  packageNumber: number
 }
 
 export default function ProfileHeader({
   imageUrl,
   name,
-  galleriesNumber,
+  packageNumber,
 }: ProfileHeaderProps) {
   return (
     <div className='flex flex-row items-center gap-6'>
       <Image
         src={imageUrl || '/image.png'}
         alt='image profile'
-        className='size-16 rounded-full lg:size-28'
+        className='size-16 rounded-full object-cover lg:size-28'
         width={112}
         height={112}
       />
@@ -34,7 +34,7 @@ export default function ProfileHeader({
             Photographer
           </Badge>
           <p className='text-center text-sm font-medium text-gray-500 lg:mt-1'>
-            {galleriesNumber} Galleries
+            {packageNumber} Packages
           </p>
         </div>
       </div>
diff --git a/src/components/quotation/photographer-quotation.tsx b/src/components/quotation/photographer-quotation.tsx
index 6e0a1cf3..5b5679d8 100644
--- a/src/components/quotation/photographer-quotation.tsx
+++ b/src/components/quotation/photographer-quotation.tsx
@@ -4,6 +4,7 @@ import { useState } from 'react'
 
 import { Package } from '@/actions/get-packages'
 import { Quotation } from '@/actions/get-quotations'
+import { calculateDurationFromDate } from '@/lib/utils'
 import { formatDateToString } from '@/lib/utils'
 import { QuotationStatus, WindowState } from '@/types/quotation'
 import { Icon } from '@iconify/react'
@@ -131,11 +132,10 @@ export default function PhotographerQuotation({
                       from={formatDateToString(quotation.from)}
                       to={formatDateToString(quotation.to)}
                       description={quotation.description}
-                      duration={
-                        (new Date(quotation.to).getTime() -
-                          new Date(quotation.from).getTime()) /
-                        (1000 * 60 * 60)
-                      }
+                      duration={calculateDurationFromDate(
+                        quotation.from,
+                        quotation.to
+                      )}
                       totalPrice={
                         quotation.pricePerHour *
                         ((new Date(quotation.to).getTime() -
@@ -202,11 +202,10 @@ export default function PhotographerQuotation({
                     from={formatDateToString(currentQuotation.from)}
                     to={formatDateToString(currentQuotation.to)}
                     description={currentQuotation.description}
-                    duration={
-                      (new Date(currentQuotation.to).getTime() -
-                        new Date(currentQuotation.from).getTime()) /
-                      (1000 * 60 * 60)
-                    }
+                    duration={calculateDurationFromDate(
+                      currentQuotation.from,
+                      currentQuotation.to
+                    )}
                     totalPrice={
                       currentQuotation.pricePerHour *
                       ((new Date(currentQuotation.to).getTime() -
diff --git a/src/components/quotation/quotation-status-tag.tsx b/src/components/quotation/quotation-status-tag.tsx
index 9d9b6375..a469bb51 100644
--- a/src/components/quotation/quotation-status-tag.tsx
+++ b/src/components/quotation/quotation-status-tag.tsx
@@ -11,22 +11,26 @@ const statusVariants = cva(
   {
     variants: {
       variant: {
-        Pending: 'bg-amber-100 text-amber-700',
-        Confirm: 'bg-sky-100 text-sky-700',
-        Cancelled: 'bg-red-100 text-red-700',
-        Paid: 'bg-emerald-100 text-emerald-700',
+        PENDING: 'bg-amber-100 text-amber-700',
+        CONFIRM: 'bg-sky-100 text-sky-700',
+        CANCELLED: 'bg-red-100 text-red-700',
+        PAID: 'bg-emerald-100 text-emerald-700',
       },
       size: {
         default: 'px-2 py-1',
       },
     },
     defaultVariants: {
-      variant: 'Pending',
+      variant: 'PENDING',
       size: 'default',
     },
   }
 )
 
 export function QuotationStatusTag({ variant }: QuotationStatusTagProps) {
+  if (!variant) {
+    variant = 'PENDING'
+  }
+
   return <div className={cn(statusVariants({ variant }))}>{variant}</div>
 }
diff --git a/src/components/quotation/quotation-view-drawer.tsx b/src/components/quotation/quotation-view-drawer.tsx
index 1d5dd7e2..a02a6706 100644
--- a/src/components/quotation/quotation-view-drawer.tsx
+++ b/src/components/quotation/quotation-view-drawer.tsx
@@ -2,6 +2,7 @@ import { useState } from 'react'
 
 import { Package } from '@/actions/get-packages'
 import { Quotation } from '@/actions/get-quotations'
+import { calculateDurationFromDate } from '@/lib/utils'
 import { formatDateToString } from '@/lib/utils'
 import { QuotationStatus, WindowState } from '@/types/quotation'
 
@@ -67,11 +68,7 @@ export default function QuotationViewDrawer({
           from={formatDateToString(quotation.from).toString()}
           to={formatDateToString(quotation.to).toString()}
           description={quotation.description}
-          duration={
-            (new Date(quotation.to).getTime() -
-              new Date(quotation.from).getTime()) /
-            (1000 * 60 * 60)
-          }
+          duration={calculateDurationFromDate(quotation.from, quotation.to)}
           totalPrice={
             quotation.pricePerHour *
             ((new Date(quotation.to).getTime() -
@@ -126,11 +123,7 @@ export default function QuotationViewDrawer({
             from={formatDateToString(quotation.from)}
             to={formatDateToString(quotation.to)}
             description={quotation.description}
-            duration={
-              (new Date(quotation.to).getTime() -
-                new Date(quotation.from).getTime()) /
-              (1000 * 60 * 60)
-            }
+            duration={calculateDurationFromDate(quotation.from, quotation.to)}
             totalPrice={
               quotation.pricePerHour *
               ((new Date(quotation.to).getTime() -
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
index 8694c136..c26397ed 100644
--- a/src/components/ui/carousel.tsx
+++ b/src/components/ui/carousel.tsx
@@ -209,7 +209,7 @@ const CarouselPrevious = React.forwardRef<
       className={cn(
         'absolute h-8 w-8 rounded-full',
         orientation === 'horizontal'
-          ? '-left-12 top-1/2 -translate-y-1/2'
+          ? '-left-2 top-1/2 -translate-y-1/2'
           : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
         className
       )}
@@ -238,7 +238,7 @@ const CarouselNext = React.forwardRef<
       className={cn(
         'absolute h-8 w-8 rounded-full',
         orientation === 'horizontal'
-          ? '-right-12 top-1/2 -translate-y-1/2'
+          ? 'right-2 top-1/2 -translate-y-1/2'
           : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
         className
       )}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 4c5987e8..175cb180 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,28 +1,47 @@
 import { type ClassValue, clsx } from 'clsx'
+import {
+  format,
+  formatDuration,
+  intervalToDuration,
+  parse,
+  parseISO,
+} from 'date-fns'
 import { twMerge } from 'tailwind-merge'
 
 export function cn(...inputs: ClassValue[]) {
   return twMerge(clsx(inputs))
 }
 
-export function formatDateToDate(date: Date) {
-  const day = String(date.getDate()).padStart(2, '0')
-  const month = String(date.getMonth() + 1).padStart(2, '0') // Months are 0-based
-  const year = date.getFullYear()
-  const hours = String(date.getHours()).padStart(2, '0')
-  const minutes = String(date.getMinutes()).padStart(2, '0')
+export function formatDateToDate(date: Date): Date {
+  const formattedString = format(date, 'MM/dd/yyyy HH:mm')
+  return parse(formattedString, 'MM/dd/yyyy HH:mm', new Date())
+}
+
+export function formatDateToString(date: Date): string {
+  return format(date, 'dd/MM/yyyy HH:mm')
+}
 
-  return new Date(`${month}/${day}/${year} ${hours}:${minutes}`)
+export function calculateDurationFromString(
+  fromDate: string,
+  toDate: string
+): string {
+  const from = parseISO(fromDate)
+  const to = parseISO(toDate)
+
+  const duration = intervalToDuration({ start: from, end: to })
+
+  return formatDuration(duration, {
+    format: ['days', 'hours', 'minutes'],
+  })
 }
 
-export function formatDateToString(date: Date) {
-  return new Date(date)
-    .toLocaleString('en-GB', {
-      day: 'numeric',
-      month: 'numeric',
-      year: 'numeric',
-      hour: '2-digit',
-      minute: '2-digit',
-    })
-    .replace(/,/g, '')
+export function calculateDurationFromDate(
+  fromDate: Date,
+  toDate: Date
+): string {
+  const duration = intervalToDuration({ start: fromDate, end: toDate })
+
+  return formatDuration(duration, {
+    format: ['days', 'hours', 'minutes'],
+  })
 }
diff --git a/src/types/quotation.ts b/src/types/quotation.ts
index 5c2f69e2..b5e8779d 100644
--- a/src/types/quotation.ts
+++ b/src/types/quotation.ts
@@ -1,4 +1,4 @@
-export type QuotationStatus = 'Pending' | 'Confirm' | 'Paid' | 'Cancelled'
+export type QuotationStatus = 'PENDING' | 'CONFIRM' | 'PAID' | 'CANCELLED'
 
 export interface QuotationDetailsProps {
   quotationId: number
@@ -9,13 +9,13 @@ export interface QuotationDetailsProps {
   from: string
   to: string
   description: string
-  duration: number
+  duration: string
   totalPrice: number
 }
 
 export interface CustomerQuotationProps extends QuotationDetailsProps {
   photographerImageUrl: string
-  galleriesNumber: number
+  packageNumber: number
   quotationImages: { url: string; name: string }[]
 }
 


Reviews:
user: login: boomchanotai
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:48:57Z
user: login: NhongSun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T00:22:23Z
user: login: patthapolkit
body: some comment
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T06:47:56Z
user: login: boomchanotai
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:26:45Z

Review comments:
diff_hunk: @@ -33,7 +33,7 @@ import Container from '../container'
 import { Badge } from '../ui/badge'
 
 const profileSchema = z.object({
-  image: z.instanceof(File).optional(),
+  image: z.any(),
path: src/components/profile-page/index.tsx
user: login: boomchanotai
body: Fuck you any 
created_at: 2025-03-11T19:47:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,53 +12,73 @@ export default async function Page({
 }) {
   const quotationId = (await params).id
 
-  const mockData = {
-    status: 'Pending',
-    packageName: 'Package A',
-    photographer: {
-      name: 'Pattapol K.',
-      imageUrl: 'https://images.unsplash.com/photo-1517849845537-4d257902454a',
-    },
-    customerName: 'Chanotai K.',
-    from: '20/02/2025 14:00',
-    to: '20/02/2025 19:00',
-    description:
-      'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas augue tortor, luctus eget laoreet et, interdum fringilla urna. ',
-    duration: 5,
-    totalPrice: 2000,
-    images: [
-      {
-        url: 'https://thedaily.case.edu/wp-content/uploads/2023/06/underwater-view.jpg',
-        name: 'Ocean',
-      },
-      {
-        url: 'https://images.unsplash.com/photo-1425913397330-cf8af2ff40a1?q=80&w=3174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
-        name: 'Forrest',
-      },
-      {
-        url: 'https://www.allaboutbirds.org/news/wp-content/uploads/2024/05/549985061-Rufous_Hummingbird-Nathan_Wall.jpg',
-        name: 'Hummingbird',
-      },
-      {
-        url: 'https://media.newyorker.com/photos/660fe3deaba0b51c65382288/master/w_2240,c_limit/Galchen-Eclipse.jpg',
-        name: 'Solar Eclipse',
-      },
-      {
-        url: 'https://media.istockphoto.com/id/1224025134/th/%E0%B8%A3%E0%B8%B9%E0%B8%9B%E0%B8%96%E0%B9%88%E0%B8%B2%E0%B8%A2/%E0%B8%A0%E0%B8%B2%E0%B8%9E%E0%B8%A3%E0%B8%B0%E0%B8%A2%E0%B8%B0%E0%B9%83%E0%B8%81%E0%B8%A5%E0%B9%89%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B8%AA%E0%B9%80%E0%B8%95%E0%B9%87%E0%B8%81%E0%B8%99%E0%B8%B4%E0%B8%A7%E0%B8%A2%E0%B8%AD%E0%B8%A3%E0%B9%8C%E0%B8%81.jpg?s=1024x1024&w=is&k=20&c=r52awidhEO6DXvup515015SCZxavSK2Q-G7OExsLRMU=',
-        name: 'Steak',
-      },
-    ],
+  const { response, data } = await client.GET('/api/v1/quotations/{id}', {
+    params: { path: { id: quotationId } },
+  })
+
+  if (response.status === 404) {
+    return <p>Quotation not found</p>
+  }
+
+  if (response.status !== 200) {
+    return <p>Internal server error</p>
+  }
+
+  const quotation = data?.result
+
+  if (
+    !quotation ||
+    !quotation.fromDate ||
+    !quotation.toDate ||
+    !quotation.status
+  ) {
+    return <p>Internal server error</p>
   }
 
+  const formatDate = (date: string) => {
+    if (!date) {
+      return ''
+    }
+    return new Date(date)
+      .toLocaleString('en-GB', {
+        day: 'numeric',
+        month: 'numeric',
+        year: 'numeric',
+        hour: '2-digit',
+        minute: '2-digit',
+      })
+      .replace(/,/g, '')
+  }
path: src/app/quotation/[id]/page.tsx
user: login: boomchanotai
body: Use function from utils
created_at: 2025-03-11T19:48:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,7 +33,7 @@ import Container from '../container'
 import { Badge } from '../ui/badge'
 
 const profileSchema = z.object({
-  image: z.instanceof(File).optional(),
+  image: z.any(),
path: src/components/profile-page/index.tsx
user: login: NhongSun
body: 💀💀
created_at: 2025-03-12T00:22:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,3 +1,6 @@
+'use server'
+
path: src/app/quotation/[id]/page.tsx
user: login: patthapolkit
body: no need?
created_at: 2025-03-12T06:47:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: NhongSun
created_at: 2025-03-11T16:04:56Z
body: btw what should i do with non int duration (1 decimal point for now)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-12T09:34:04Z
body: Preview deployment is live at 
🔗https://pic-me-pls-frontend-9bu0g9w8v-chanatpakornds-projects.vercel.app
author_association: NONE