mirror of
				https://github.com/KevinMidboe/planetposen-frontend.git
				synced 2025-10-29 13:10:12 +00:00 
			
		
		
		
	Displays payment, shipping, errors page & edit and add shipment
This commit is contained in:
		
							
								
								
									
										202
									
								
								src/lib/components/ShipmentProgress.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								src/lib/components/ShipmentProgress.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import { onMount } from 'svelte'; | ||||||
|  |   import Time from './Time.svelte'; | ||||||
|  |   import CircleLoading from './loading/CircleLoading.svelte'; | ||||||
|  |   import { buildApiUrl } from '$lib/utils/apiUrl'; | ||||||
|  |   import type { IShipmentResponse } from '$lib/interfaces/IShipping'; | ||||||
|  |  | ||||||
|  |   export let shipment: IShipmentResponse; | ||||||
|  |  | ||||||
|  |   interface TrackingEvent { | ||||||
|  |     city: string; | ||||||
|  |     consignmentEvent: boolean; | ||||||
|  |     country: string; | ||||||
|  |     countryCode: string; | ||||||
|  |     date: string; | ||||||
|  |     dateIso: Date; | ||||||
|  |     description: string; | ||||||
|  |     displayDate: string; | ||||||
|  |     displayTime: string; | ||||||
|  |     gpsMapUrl: string; | ||||||
|  |     gpsXCoordinate: string; | ||||||
|  |     gpsYCoordinate: string; | ||||||
|  |     insignificant: boolean; | ||||||
|  |     lmCauseCode: string; | ||||||
|  |     lmEventCode: string; | ||||||
|  |     lmMeasureCode: string; | ||||||
|  |     postalCode: string; | ||||||
|  |     recipientSignature: null; | ||||||
|  |     status: string; | ||||||
|  |     unitId: string; | ||||||
|  |     unitInformationUrl: null; | ||||||
|  |     unitType: string; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let trackedShipment: TrackingEvent[] | null = null; | ||||||
|  |  | ||||||
|  |   async function getTracking() { | ||||||
|  |     const url = buildApiUrl(`/api/v1/shipment/${shipment?.shipment_id}/track`); | ||||||
|  |     try { | ||||||
|  |       trackedShipment = null; | ||||||
|  |       const trackingResponse = await fetch(url).then((resp) => resp.json()); | ||||||
|  |  | ||||||
|  |       trackedShipment = trackingResponse?.shipment?.events || []; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.log('api error from track:', error); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onMount(() => { | ||||||
|  |     if (!shipment) return; | ||||||
|  |     getTracking(); | ||||||
|  |   }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <section> | ||||||
|  |   <h2>Shipment history</h2> | ||||||
|  |  | ||||||
|  |   {#if trackedShipment === null} | ||||||
|  |     <div class="loading-content"> | ||||||
|  |       <CircleLoading /> | ||||||
|  |     </div> | ||||||
|  |   {:else if trackedShipment?.length > 0} | ||||||
|  |     <ul class="tracking"> | ||||||
|  |       {#each trackedShipment as event} | ||||||
|  |         <li> | ||||||
|  |           <span class="indicator"></span> | ||||||
|  |           <div class="details"> | ||||||
|  |             <span class="message">{event.description}</span> | ||||||
|  |  | ||||||
|  |             {#if event?.status} | ||||||
|  |               <div> | ||||||
|  |                 <span>{event.status} - </span> | ||||||
|  |                 <span style="text-transform: capitalize;"><Time time="{event.date}" /></span> | ||||||
|  |               </div> | ||||||
|  |             {/if} | ||||||
|  |  | ||||||
|  |             <div class="location"> | ||||||
|  |               {#if event.city.length > 0} | ||||||
|  |                 <span>{event?.city?.toLowerCase()}</span> | ||||||
|  |               {/if} | ||||||
|  |  | ||||||
|  |               {#if event.countryCode.length > 0} | ||||||
|  |                 <span data-separator="," | ||||||
|  |                   >{event.city.length > 0 ? event?.countryCode : event?.country}</span | ||||||
|  |                 > | ||||||
|  |               {/if} | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </li> | ||||||
|  |       {/each} | ||||||
|  |     </ul> | ||||||
|  |   {:else} | ||||||
|  |     <div> | ||||||
|  |       <h3>Error! Unable to retrieve shipment history</h3> | ||||||
|  |       <p> | ||||||
|  |         Try again later or contact <a class="link" href="mailto:support@planet.schleppe.cloud" | ||||||
|  |           >support@planet.schleppe.cloud</a | ||||||
|  |         > | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   {/if} | ||||||
|  | </section> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  |   @import '../../styles/media-queries.scss'; | ||||||
|  |   @import '../../styles/effects.scss'; | ||||||
|  |  | ||||||
|  |   h2 { | ||||||
|  |     font-size: 1.4rem; | ||||||
|  |     margin-bottom: 0; | ||||||
|  |     border-bottom: 1px solid rgba(0, 0, 0, 0.3); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .tracking { | ||||||
|  |     padding-left: 1.5rem; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       padding-left: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     li { | ||||||
|  |       display: flex; | ||||||
|  |       margin: 1rem 0.3rem; | ||||||
|  |       padding: 0.8rem 0.3rem; | ||||||
|  |       position: relative; | ||||||
|  |  | ||||||
|  |       &:first-of-type .indicator { | ||||||
|  |         @include pulse-dot; | ||||||
|  |         &::after { | ||||||
|  |           left: calc(0.6rem + 5.5px); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         &::before { | ||||||
|  |           height: 100%; | ||||||
|  |           top: 50%; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &:last-of-type .indicator::before { | ||||||
|  |         height: 100%; | ||||||
|  |         top: -50%; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .indicator { | ||||||
|  |       display: block; | ||||||
|  |       align-self: center; | ||||||
|  |       min-width: 20px; | ||||||
|  |       min-height: 20px; | ||||||
|  |       background-color: #e32d22; | ||||||
|  |       border-radius: 50%; | ||||||
|  |       margin-right: 2rem; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         margin-right: 1.5rem; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &::before { | ||||||
|  |         content: ''; | ||||||
|  |         position: absolute; | ||||||
|  |         top: 1rem; | ||||||
|  |         left: calc(0.3rem + 8.5px); | ||||||
|  |         height: calc(100% + 1rem); | ||||||
|  |         width: 3px; | ||||||
|  |         background-color: #e32d22; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .details { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |  | ||||||
|  |       .message { | ||||||
|  |         margin-bottom: 0.5rem; | ||||||
|  |         font-weight: bold; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .location { | ||||||
|  |         text-transform: capitalize; | ||||||
|  |  | ||||||
|  |         span:nth-of-type(2) { | ||||||
|  |           position: relative; | ||||||
|  |           margin-left: 3px; | ||||||
|  |           padding-left: 3px; | ||||||
|  |  | ||||||
|  |           &::before { | ||||||
|  |             content: attr(data-separator) ' '; | ||||||
|  |             position: absolute; | ||||||
|  |             left: -8px; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .loading-content { | ||||||
|  |     width: 100%; | ||||||
|  |     margin-top: 2rem; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										25
									
								
								src/lib/components/Time.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/lib/components/Time.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   export let time: Date | string | undefined; | ||||||
|  |  | ||||||
|  |   let dateLocaleString: string; | ||||||
|  |   const options: Intl.DateTimeFormatOptions = { | ||||||
|  |     weekday: 'short', | ||||||
|  |     day: 'numeric', | ||||||
|  |     month: 'numeric', | ||||||
|  |     year: 'numeric', | ||||||
|  |     hour: 'numeric', | ||||||
|  |     minute: 'numeric', | ||||||
|  |     second: 'numeric', | ||||||
|  |     timeZone: 'Europe/Oslo' | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   if (time) { | ||||||
|  |     dateLocaleString = new Date(time).toLocaleString('no-NB', options); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | {#if dateLocaleString} | ||||||
|  |   <span>{dateLocaleString}</span> | ||||||
|  | {:else} | ||||||
|  |   <span>(no date found)</span> | ||||||
|  | {/if} | ||||||
							
								
								
									
										42
									
								
								src/lib/interfaces/IShipping.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/lib/interfaces/IShipping.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | export interface IShippingCourier { | ||||||
|  |   id: string; | ||||||
|  |   name: string; | ||||||
|  |   website: string; | ||||||
|  |   has_api: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IShipment { | ||||||
|  |   id: string; | ||||||
|  |   courier_id: string; | ||||||
|  |   tracking_code: string; | ||||||
|  |   tracking_link: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IShipmentEvent { | ||||||
|  |   event_id: string; | ||||||
|  |   shipment_id: string; | ||||||
|  |   description: string; | ||||||
|  |   status: string; | ||||||
|  |   location: string; | ||||||
|  |   event_time: Date; | ||||||
|  |   updated: Date; | ||||||
|  |   created: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IShipmentResponse { | ||||||
|  |   shipment_id: string | ||||||
|  |   order_id: string | ||||||
|  |   courier: string | ||||||
|  |   has_api: boolean | ||||||
|  |   courier_id: number | ||||||
|  |   tracking_code: string | ||||||
|  |   tracking_link: string | ||||||
|  |   user_notified: boolean | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ICourier { | ||||||
|  |   courier_id: number | ||||||
|  |   name: string | ||||||
|  |   website: string | ||||||
|  |   has_api: boolean | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| import type { PageServerLoad } from './$types'; | import type { PageServerLoad } from './$types'; | ||||||
|  |  | ||||||
| export const load: PageServerLoad = async ({ fetch }) => { | export const load: PageServerLoad = async ({ fetch }) => { | ||||||
|   const res = await fetch('/api/orders'); |   const res = await fetch('/api/v1/orders'); | ||||||
|   const response = await res.json(); |   const response = await res.json(); | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|   | |||||||
| @@ -1,13 +1,15 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import OrdersTable from './OrdersTable.svelte'; |   import OrdersTable from './OrdersTable.svelte'; | ||||||
|   import BadgeType from '$lib/interfaces/BadgeType'; |   import BadgeType from '$lib/interfaces/BadgeType'; | ||||||
|  |   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; |   import type { IOrderSummary } from '$lib/interfaces/IOrder'; | ||||||
|  |  | ||||||
|   export let data: PageData; |   export let data: PageData; | ||||||
|   const orders = data.orders as Array<IOrder>; |   const orders = data.orders as Array<IOrderSummary>; | ||||||
|  |  | ||||||
|   const pendingOrders = orders.filter( |   const successfulOrders = orders.filter((el) => el.status === 'CONFIRMED'); | ||||||
|  |   const incompleteOrders = orders.filter( | ||||||
|     (el) => el.status === BadgeType.INFO || el.status === 'INITIATED' |     (el) => el.status === BadgeType.INFO || el.status === 'INITIATED' | ||||||
|   ); |   ); | ||||||
|   const inTransitOrders = orders.filter((el) => el.status === BadgeType.PENDING); |   const inTransitOrders = orders.filter((el) => el.status === BadgeType.PENDING); | ||||||
| @@ -17,38 +19,25 @@ | |||||||
|       el.status !== BadgeType.PENDING && |       el.status !== BadgeType.PENDING && | ||||||
|       el.status !== BadgeType.INFO && |       el.status !== BadgeType.INFO && | ||||||
|       el.status !== 'INITIATED' && |       el.status !== 'INITIATED' && | ||||||
|  |       el.status !== 'CONFIRMED' && | ||||||
|       el.status !== BadgeType.WARNING |       el.status !== BadgeType.WARNING | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const deliveredOrders: Array<IOrder> = []; |   const deliveredOrders: Array<IOrderSummary> = []; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <PageMeta title="Orders" description="View all webshop orders" /> | ||||||
| <div class="page"> | <div class="page"> | ||||||
|   <h1>Orders</h1> |   <h1>Orders</h1> | ||||||
|   <section class="content"> |   <section class="content"> | ||||||
|     {#if attentionOrders?.length} |     {#if attentionOrders?.length} | ||||||
|       <h2>⚠️ orders needing attention</h2> |       <OrdersTable title="⚠️ orders needing attention" orders="{attentionOrders}" /> | ||||||
|  |  | ||||||
|       <OrdersTable orders="{attentionOrders}" /> |  | ||||||
|     {/if} |     {/if} | ||||||
|  |  | ||||||
|     <h2>📬 pending orders</h2> |     <OrdersTable title="📬 purchased orders" orders="{successfulOrders}" /> | ||||||
|     <OrdersTable orders="{pendingOrders}" /> |     <OrdersTable title="📦 in transit" orders="{inTransitOrders}" /> | ||||||
|  |     <OrdersTable title="🙅♀️ cancelled/returns" orders="{otherOrders}" /> | ||||||
|     <h2>📦 in transit</h2> |     <OrdersTable title="💤 incomplete orders" orders="{incompleteOrders}" /> | ||||||
|     <OrdersTable orders="{inTransitOrders}" /> |     <OrdersTable title="🎁🏠 delivered orders" orders="{deliveredOrders}" /> | ||||||
|  |  | ||||||
|     <h2>🙅♀️ cancelled/returns</h2> |  | ||||||
|     <OrdersTable orders="{otherOrders}" /> |  | ||||||
|  |  | ||||||
|     <h2>🏠🎁 delivered orders</h2> |  | ||||||
|     <OrdersTable orders="{deliveredOrders}" /> |  | ||||||
|   </section> |   </section> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <style lang="scss" module="scoped"> |  | ||||||
|   section.content h2 { |  | ||||||
|     // text-decoration: underline; |  | ||||||
|     font-size: 1.2rem; |  | ||||||
|   } |  | ||||||
| </style> |  | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { goto } from '$app/navigation'; |   import { goto } from '$app/navigation'; | ||||||
|   import Badge from '$lib/components/Badge.svelte'; |   import Badge from '$lib/components/Badge.svelte'; | ||||||
|  |   import Time from '$lib/components/Time.svelte'; | ||||||
|   import type { IOrderSummary } from '$lib/interfaces/IOrder'; |   import type { IOrderSummary } from '$lib/interfaces/IOrder'; | ||||||
|  |  | ||||||
|  |   export let title: string; | ||||||
|   export let orders: Array<IOrderSummary>; |   export let orders: Array<IOrderSummary>; | ||||||
|  |  | ||||||
|   function navigate(order: IOrderSummary) { |   function navigate(order: IOrderSummary) { | ||||||
| @@ -10,15 +12,16 @@ | |||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <h2>{title} <span class="section-count">{orders?.length || 0}</span></h2> | ||||||
| {#if orders?.length} | {#if orders?.length} | ||||||
|   <table> |   <table> | ||||||
|     <thead> |     <thead> | ||||||
|       <tr> |       <tr> | ||||||
|         <th>Amount</th> |         <th>Amount</th> | ||||||
|         <th>Status</th> |         <th>Status</th> | ||||||
|         <th>Order ID</th> |  | ||||||
|         <th>Customer</th> |         <th>Customer</th> | ||||||
|         <th>Date</th> |         <th>Date</th> | ||||||
|  |         <th>Order ID</th> | ||||||
|         <th>Receipt</th> |         <th>Receipt</th> | ||||||
|       </tr> |       </tr> | ||||||
|     </thead> |     </thead> | ||||||
| @@ -29,18 +32,12 @@ | |||||||
|           <td>NOK {order.order_sum}</td> |           <td>NOK {order.order_sum}</td> | ||||||
|  |  | ||||||
|           <td> |           <td> | ||||||
|             <Badge title="{order.status}" type="{order?.status?.type}" /> |             <Badge title="{order.status}" /> | ||||||
|           </td> |           </td> | ||||||
|  |  | ||||||
|           <td>{order.order_id}</td> |  | ||||||
|           <td>{order.first_name} {order.last_name}</td> |           <td>{order.first_name} {order.last_name}</td> | ||||||
|           <td |           <td><Time time="{order?.created}" /></td> | ||||||
|             >{order?.created |           <td>{order.order_id}</td> | ||||||
|               ? new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format( |  | ||||||
|                   new Date(order.created) |  | ||||||
|                 ) |  | ||||||
|               : ''}</td |  | ||||||
|           > |  | ||||||
|           <td> |           <td> | ||||||
|             <a href="receipt/{order.order_id}?email={order.email}">🧾</a> |             <a href="receipt/{order.order_id}?email={order.email}">🧾</a> | ||||||
|           </td> |           </td> | ||||||
| @@ -56,11 +53,10 @@ | |||||||
|   @import '../../styles/media-queries.scss'; |   @import '../../styles/media-queries.scss'; | ||||||
|  |  | ||||||
|   h2 { |   h2 { | ||||||
|     // text-decoration: underline; |  | ||||||
|     font-size: 1.2rem; |     font-size: 1.2rem; | ||||||
|  |  | ||||||
|     .section-count { |     .section-count { | ||||||
|       background-color: rgba(0,0,0,0.15); |       background-color: rgba(0, 0, 0, 0.15); | ||||||
|       padding: 0.3rem 0.4rem; |       padding: 0.3rem 0.4rem; | ||||||
|       margin-left: 0.5rem; |       margin-left: 0.5rem; | ||||||
|       border-radius: 0.5rem; |       border-radius: 0.5rem; | ||||||
| @@ -105,8 +101,14 @@ | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     th:last-of-type, | ||||||
|  |     td:last-of-type { | ||||||
|  |       text-align: center; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @include mobile { |     @include mobile { | ||||||
|       tr > *:first-child { |       tr > *:nth-child(4), | ||||||
|  |       tr > *:nth-child(5) { | ||||||
|         display: none; |         display: none; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								src/routes/orders/[id]/+error.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/routes/orders/[id]/+error.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import { page } from '$app/stores'; | ||||||
|  |  | ||||||
|  |   console.log('page data:', $page.error); | ||||||
|  |  | ||||||
|  |   $: parsedApiResponse = JSON.stringify($page.error?.apiResponse || {}, null, 4); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <h1>Oisann! Klarte ikke hente order</h1> | ||||||
|  |  | ||||||
|  | <p> | ||||||
|  |   Det du søkte etter fantes ikke her. Om du tror dette er en feil, ta kontakt <a | ||||||
|  |     class="link" | ||||||
|  |     href="mailto:support@planetposen.no">support@planetposen.no</a | ||||||
|  |   > for spørsmål. | ||||||
|  | </p> | ||||||
|  | <div class="error"> | ||||||
|  |   <span>Internal error message:</span> | ||||||
|  |   <pre> | ||||||
|  |     <code>{parsedApiResponse}</code> | ||||||
|  |   </pre> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  |   .error { | ||||||
|  |     margin-top: 4rem; | ||||||
|  |  | ||||||
|  |     span { | ||||||
|  |       display: inline-block; | ||||||
|  |       font-size: 1.4rem; | ||||||
|  |       text-decoration: none; | ||||||
|  |       transition: all 0.3s ease; | ||||||
|  |       border-bottom: 2px solid var(--color-theme-1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     code { | ||||||
|  |       display: block; | ||||||
|  |       font-size: 1rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
| @@ -1,15 +1,21 @@ | |||||||
|  | import { error } from '@sveltejs/kit'; | ||||||
| import type { IOrderDTO } from '$lib/interfaces/ApiResponse'; | import type { IOrderDTO } from '$lib/interfaces/ApiResponse'; | ||||||
| import type { PageServerLoad } from './$types'; | import type { PageServerLoad } from './$types'; | ||||||
|  |  | ||||||
| export const load: PageServerLoad = async ({ fetch, params }) => { | export const load: PageServerLoad = async ({ fetch, params }) => { | ||||||
|   const { id } = params; |   const { id } = params; | ||||||
|  |  | ||||||
|   const res = await fetch(`/api/order/${id}`); |   const res = await fetch(`/api/v1/order/${id}`); | ||||||
|   const orderResponse: IOrderDTO = await res.json(); |   const orderResponse = await res.json(); | ||||||
|  |  | ||||||
|   if (orderResponse?.success == false || orderResponse?.order === undefined) { |   if (orderResponse?.success == false || orderResponse?.order === undefined) { | ||||||
|     throw Error(':('); |     console.log('throwing error', orderResponse); | ||||||
|  |  | ||||||
|  |     throw error(404, { | ||||||
|  |       apiResponse: orderResponse, | ||||||
|  |       message: 'Something went wrong! Unable to get order' | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return { order: orderResponse.order }; |   return { order: orderResponse?.order }; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -4,15 +4,15 @@ | |||||||
|   import PaymentDetails from './PaymentDetails.svelte'; |   import PaymentDetails from './PaymentDetails.svelte'; | ||||||
|   import CustomerDetails from './CustomerDetails.svelte'; |   import CustomerDetails from './CustomerDetails.svelte'; | ||||||
|   import TrackingDetails from './TrackingDetails.svelte'; |   import TrackingDetails from './TrackingDetails.svelte'; | ||||||
|  |   import ShipmentProgress from '$lib/components/ShipmentProgress.svelte'; | ||||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; |   import type { IOrder } from '$lib/interfaces/IOrder'; | ||||||
|   import type { PageServerData } from './$types'; |   import type { PageServerData } from './$types'; | ||||||
|  |  | ||||||
|   export let data: PageServerData; |   export let data: PageServerData; | ||||||
|   let order = data.order as IOrder; |   let order = data.order as IOrder; | ||||||
|   console.log('order:', order); |  | ||||||
|  |  | ||||||
|   function orderSubTotal() { |   function orderSubTotal() { | ||||||
|     if (!order || order.lineItems?.length === 0) return; |     if (!order || order?.lineItems?.length === 0) return; | ||||||
|  |  | ||||||
|     let sum = 0; |     let sum = 0; | ||||||
|     order.lineItems.forEach((lineItem) => (sum = sum + lineItem.quantity * lineItem.price)); |     order.lineItems.forEach((lineItem) => (sum = sum + lineItem.quantity * lineItem.price)); | ||||||
| @@ -21,20 +21,21 @@ | |||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <h1>Order: {order.orderid}</h1> | <h1>Order id: {order?.orderid}</h1> | ||||||
| <div class="order"> | <div class="order"> | ||||||
|   <!-- <p>Order: {JSON.stringify(order)}</p> --> |  | ||||||
|   <h2 class="price"><span class="amount">{orderSubTotal()}.00</span> Nok</h2> |   <h2 class="price"><span class="amount">{orderSubTotal()}.00</span> Nok</h2> | ||||||
|  |  | ||||||
|   <OrderSummary order="{order}" /> |   <OrderSummary order="{order}" /> | ||||||
|   <OrderProducts lineItems="{order?.lineItems}" /> |   <OrderProducts lineItems="{order?.lineItems}" /> | ||||||
|  |  | ||||||
|   <PaymentDetails order="{order}" /> |   <PaymentDetails payment="{order?.payment}" /> | ||||||
|   <CustomerDetails customer="{order?.customer}" /> |   <CustomerDetails customer="{order?.customer}" /> | ||||||
|   <TrackingDetails shipping="{order?.shipping}" /> |   <TrackingDetails shipping="{order?.shipping}" orderId="{order?.orderid}" /> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
|  |   @import '../../../styles/media-queries.scss'; | ||||||
|  |  | ||||||
|   h2.price { |   h2.price { | ||||||
|     font-size: 1.5rem; |     font-size: 1.5rem; | ||||||
|     color: grey; |     color: grey; | ||||||
|   | |||||||
| @@ -67,19 +67,20 @@ | |||||||
|     .image-column { |     .image-column { | ||||||
|       width: 4rem; |       width: 4rem; | ||||||
|       max-width: 4rem; |       max-width: 4rem; | ||||||
|       margin: 0 0.5rem; |  | ||||||
|  |       @include desktop { | ||||||
|  |         margin: 0 0.5rem; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     td, |     td, | ||||||
|     th { |     th { | ||||||
|       white-space: nowrap; |  | ||||||
|       padding: 0.4rem 0.6rem; |       padding: 0.4rem 0.6rem; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     tbody { |     tbody { | ||||||
|       img { |       img { | ||||||
|         width: 4rem; |         width: 4rem; | ||||||
|         height: 4rem; |  | ||||||
|         border-radius: 0.4rem; |         border-radius: 0.4rem; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -102,11 +103,5 @@ | |||||||
|         display: none; |         display: none; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // @include mobile { |  | ||||||
|     //   tr > *:last-child, tr > :nth-child(4) { |  | ||||||
|     //     display: none; |  | ||||||
|     //   } |  | ||||||
|     // } |  | ||||||
|   } |   } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,14 +1,25 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  |   import Badge from '$lib/components/Badge.svelte'; | ||||||
|  |   import Time from '$lib/components/Time.svelte'; | ||||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; |   import type { IOrder } from '$lib/interfaces/IOrder'; | ||||||
|  |  | ||||||
|   export let order: IOrder; |   export let order: IOrder; | ||||||
|   let paymentMethod: string = Math.random() > 0.5 ? 'Stripe' : 'ApplePay'; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <ul class="summary-list"> | <ul class="summary-list"> | ||||||
|  |   <li> | ||||||
|  |     <span class="label">Status</span> | ||||||
|  |     <Badge title="{order.status}" /> | ||||||
|  |   </li> | ||||||
|  |  | ||||||
|   <li> |   <li> | ||||||
|     <span class="label">Last update</span> |     <span class="label">Last update</span> | ||||||
|     <span>{order.updated}</span> |     <Time time="{order.updated}" /> | ||||||
|  |   </li> | ||||||
|  |  | ||||||
|  |   <li> | ||||||
|  |     <span class="label">Created</span> | ||||||
|  |     <Time time="{order.created}" /> | ||||||
|   </li> |   </li> | ||||||
|  |  | ||||||
|   <li> |   <li> | ||||||
| @@ -18,12 +29,12 @@ | |||||||
|  |  | ||||||
|   <li> |   <li> | ||||||
|     <span class="label">Receipt</span> |     <span class="label">Receipt</span> | ||||||
|     <span><a href="/receipt/{order.orderid}">{order.orderid}</a></span> |     <a href="/receipt/{order.orderid}" class="link">{order.orderid}</a> | ||||||
|   </li> |   </li> | ||||||
|  |  | ||||||
|   <li> |   <li> | ||||||
|     <span class="label">Payment method</span> |     <span class="label">Payment method</span> | ||||||
|     <span>{paymentMethod}</span> |     <span style="text-transform: capitalize;">{order?.payment?.type}</span> | ||||||
|   </li> |   </li> | ||||||
| </ul> | </ul> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,36 +1,94 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import Badge from '$lib/components/Badge.svelte'; |   import Badge from '$lib/components/Badge.svelte'; | ||||||
|   // import type BadgeType from '$lib/interfaces/BadgeType'; |   import Time from '$lib/components/Time.svelte'; | ||||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; |   import type { IStripePayment } from '$lib/interfaces/IOrder'; | ||||||
|  |  | ||||||
|   export let order: IOrder; |   export let payment: IStripePayment; | ||||||
|  |   const initiatedAmount = payment?.amount ? payment.amount / 100 : 0; | ||||||
|  |   const capturedAmount = payment?.amount_captured ? payment.amount_captured / 100 : 0; | ||||||
|  |   const refundedAmount = payment?.amount_refunded ? payment.amount_refunded / 100 : 0; | ||||||
|  |  | ||||||
|  |   function calculateApproximateFee() { | ||||||
|  |     if (!payment?.amount_captured) return null; | ||||||
|  |  | ||||||
|  |     const percentCut = 0.024; | ||||||
|  |     const fixedCut = 2; | ||||||
|  |  | ||||||
|  |     return capturedAmount * percentCut + fixedCut; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function round(num: number) { | ||||||
|  |     return Math.round(num * 100) / 100; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let stripeDashboardUrl = `https://dashboard.stripe.com/test/payments/${payment?.stripe_transaction_id}`; | ||||||
|  |   const fee = calculateApproximateFee(); | ||||||
|  |   const net = fee ? capturedAmount - fee - refundedAmount : null; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <section> | <section> | ||||||
|   <h2>Payment details</h2> |   <h2>Payment details</h2> | ||||||
|   <ul class="property-list"> |   <ul class="property-list"> | ||||||
|     <li> |     <li> | ||||||
|       <span class="label">Amount</span> |       <span class="label">Stripe link</span> | ||||||
|       <span>{order?.payment?.amount}.10 kr</span> |       {#if payment?.stripe_transaction_id} | ||||||
|  |         <a href="{stripeDashboardUrl}" class="link" target="_blank" rel="noreferrer"> | ||||||
|  |           <span>{payment?.stripe_transaction_id}</span> | ||||||
|  |         </a> | ||||||
|  |       {:else} | ||||||
|  |         <span>(No payment found)</span> | ||||||
|  |       {/if} | ||||||
|     </li> |     </li> | ||||||
|  |  | ||||||
|     <li> |     <li> | ||||||
|       <span class="label">Fee</span> |       <span class="label">Amount requested</span> | ||||||
|       <span>2.25 kr</span> |       <span>{initiatedAmount}.00 kr</span> | ||||||
|  |     </li> | ||||||
|  |  | ||||||
|  |     <li> | ||||||
|  |       <span class="label">Amount charged</span> | ||||||
|  |       <span>{capturedAmount}.00 kr</span> | ||||||
|  |     </li> | ||||||
|  |  | ||||||
|  |     {#if payment?.amount_refunded > 0} | ||||||
|  |       <li> | ||||||
|  |         <span class="label">Refunded</span> | ||||||
|  |         <span>{payment?.amount_refunded / 100}.00 kr</span> | ||||||
|  |       </li> | ||||||
|  |     {/if} | ||||||
|  |  | ||||||
|  |     <li> | ||||||
|  |       <span class="label">Fee (approx)</span> | ||||||
|  |       <span>{fee === null ? '-' : round(fee) + ' kr'}</span> | ||||||
|     </li> |     </li> | ||||||
|  |  | ||||||
|     <li> |     <li> | ||||||
|       <span class="label">Net</span> |       <span class="label">Net</span> | ||||||
|       <span>7.85 kr</span> |       <span>{net === null ? '-' : round(net) + ' kr'}</span> | ||||||
|     </li> |     </li> | ||||||
|  |  | ||||||
|     <li> |     <li> | ||||||
|       <span class="label">Status</span> |       <span class="label">Status</span> | ||||||
|       <Badge title="{order?.status?.text || order.status}" type="{order.status.type}" /> |       <Badge title="{payment?.stripe_status}" /> | ||||||
|  |     </li> | ||||||
|  |  | ||||||
|  |     <li> | ||||||
|  |       <span class="label">Created</span> | ||||||
|  |       <Time time="{payment?.created}" /> | ||||||
|  |     </li> | ||||||
|  |  | ||||||
|  |     <li> | ||||||
|  |       <span class="label">Updated</span> | ||||||
|  |       <Time time="{payment?.updated}" /> | ||||||
|     </li> |     </li> | ||||||
|   </ul> |   </ul> | ||||||
| </section> | </section> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
|   @import './styles-order-page.scss'; |   @import './styles-order-page.scss'; | ||||||
|  |  | ||||||
|  |   a.link { | ||||||
|  |     max-width: 60%; | ||||||
|  |     overflow-x: hidden; | ||||||
|  |   } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,35 +1,139 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { IShipping } from '$lib/interfaces/IOrder'; |   import Button from '$lib/components/Button.svelte'; | ||||||
|  |   import ShipmentProgress from '$lib/components/ShipmentProgress.svelte'; | ||||||
|  |   import { buildApiUrl } from '$lib/utils/apiUrl'; | ||||||
|  |   import type { IShipmentResponse, ICourier } from '$lib/interfaces/IShipping'; | ||||||
|  |  | ||||||
|   export let shipping: IShipping; |   export let shipping: IShipmentResponse; | ||||||
|  |   export let orderId: string; | ||||||
|  |  | ||||||
|  |   function fetchCouriers() { | ||||||
|  |     if (couriers?.length > 0) return couriers; | ||||||
|  |  | ||||||
|  |     const url = buildApiUrl('/api/v1/shipment/couriers'); | ||||||
|  |     fetch(url) | ||||||
|  |       .then((resp) => resp.json()) | ||||||
|  |       .then((response) => (couriers = response?.couriers || [])); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function updateShipment() { | ||||||
|  |     const url = buildApiUrl(`/api/v1/shipment/${shipping.shipment_id}`); | ||||||
|  |     const options = { | ||||||
|  |       method: 'PUT', | ||||||
|  |       body: JSON.stringify({ | ||||||
|  |         tracking_code: trackingCode, | ||||||
|  |         tracking_link: trackingLink, | ||||||
|  |         courier_id: selectedCourier | ||||||
|  |       }), | ||||||
|  |       headers: { 'Content-Type': 'application/json' } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     fetch(url, options) | ||||||
|  |       .then((resp) => resp.json()) | ||||||
|  |       .then((response) => shipping = response?.shipment || shipping); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function addShipment() { | ||||||
|  |     const url = buildApiUrl(`/api/v1/shipment/${orderId}`); | ||||||
|  |     fetch(url, { method: 'POST' }) | ||||||
|  |       .then((resp) => resp.json()) | ||||||
|  |       .then((response) => { | ||||||
|  |         shipping = response?.shipment; | ||||||
|  |         toggleEdit(); | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function toggleEdit() { | ||||||
|  |     edit = !edit; | ||||||
|  |     if (edit) fetchCouriers(); | ||||||
|  |     else updateShipment(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let edit: boolean = false; | ||||||
|  |   let trackingCode: string = shipping?.tracking_code; | ||||||
|  |   let trackingLink: string = shipping?.tracking_link; | ||||||
|  |   let courier: string = shipping?.courier; | ||||||
|  |   let selectedCourier: any = shipping?.courier_id; | ||||||
|  |   let couriers: ICourier[]; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if shipping} | <section> | ||||||
|   <section> |   <h2>Tracking</h2> | ||||||
|     <h2>Tracking</h2> |  | ||||||
|  |   {#if shipping} | ||||||
|     <ul class="property-list"> |     <ul class="property-list"> | ||||||
|       <li> |       <li> | ||||||
|         <span class="label">Tracking code</span> |         <span class="label" for="courier">Tracking company</span> | ||||||
|         <span>{shipping.tracking_code}</span> |         {#if !edit} | ||||||
|  |           <span>{shipping.courier}</span> | ||||||
|  |         {:else if couriers?.length > 0} | ||||||
|  |           <select bind:value="{selectedCourier}" name="couriers" id="courier"> | ||||||
|  |             <option value="">--Please choose an option--</option> | ||||||
|  |             {#each couriers as courier} | ||||||
|  |               <option value="{courier.courier_id}">{courier.name}</option> | ||||||
|  |             {/each} | ||||||
|  |           </select> | ||||||
|  |         {/if} | ||||||
|       </li> |       </li> | ||||||
|  |  | ||||||
|       <li> |       <li> | ||||||
|         <span class="label">Tracking company</span> |         <span class="label">Tracking code</span> | ||||||
|         <span>{shipping.company}</span> |         {#if !edit} | ||||||
|  |           <span>{shipping.tracking_code}</span> | ||||||
|  |         {:else} | ||||||
|  |           <input bind:value="{trackingCode}" /> | ||||||
|  |         {/if} | ||||||
|       </li> |       </li> | ||||||
|  |  | ||||||
|       <li> |       <li> | ||||||
|         <span class="label">Link</span> |         <span class="label">Link</span> | ||||||
|         <span |         {#if !edit} | ||||||
|           ><a href="{shipping.tracking_link}" target="_blank" rel="noopener noreferrer" |           <a href="{shipping.tracking_link}" class="link" target="_blank" rel="noopener noreferrer"> | ||||||
|             >{shipping.tracking_link}</a |             {shipping.tracking_link} | ||||||
|           ></span |           </a> | ||||||
|         > |         {:else} | ||||||
|  |           <input style="margin-bottom: 0" class="wide" bind:value="{trackingLink}" /> | ||||||
|  |         {/if} | ||||||
|       </li> |       </li> | ||||||
|     </ul> |     </ul> | ||||||
|   </section> |  | ||||||
| {/if} |     <div class="button-container"> | ||||||
|  |       <Button text="{!edit ? 'Edit' : 'Save'}" on:click="{toggleEdit}" /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     {#if shipping?.tracking_code && shipping?.has_api} | ||||||
|  |       {#key shipping.tracking_code} | ||||||
|  |         <ShipmentProgress shipment="{shipping}" /> | ||||||
|  |       {/key} | ||||||
|  |     {/if} | ||||||
|  |   {/if} | ||||||
|  |  | ||||||
|  |   {#if !shipping} | ||||||
|  |     <div class="button-container"> | ||||||
|  |       <Button text="Add" on:click="{addShipment}" /> | ||||||
|  |     </div> | ||||||
|  |   {/if} | ||||||
|  | </section> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
|   @import './styles-order-page.scss'; |   @import './styles-order-page.scss'; | ||||||
|  |  | ||||||
|  |   a { | ||||||
|  |     max-width: 60%; | ||||||
|  |     white-space: pre-wrap; | ||||||
|  |     word-wrap: break-word; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   select { | ||||||
|  |     margin: -1px 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   input { | ||||||
|  |     margin: -2px 0 -2px -2px; | ||||||
|  |     padding: 0; | ||||||
|  |  | ||||||
|  |     &.wide { | ||||||
|  |       width: -webkit-fill-available; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -16,8 +16,7 @@ section { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .label, | .label { | ||||||
| .empty { |  | ||||||
|   color: grey; |   color: grey; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -51,7 +50,6 @@ ul.property-list { | |||||||
|  |  | ||||||
|   li span:last-of-type { |   li span:last-of-type { | ||||||
|     @include mobile { |     @include mobile { | ||||||
|       min-width: 60%; |  | ||||||
|       white-space: normal; |       white-space: normal; | ||||||
|       overflow-wrap: break-word; |       overflow-wrap: break-word; | ||||||
|     } |     } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user