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'; | ||||
|  | ||||
| export const load: PageServerLoad = async ({ fetch }) => { | ||||
|   const res = await fetch('/api/orders'); | ||||
|   const res = await fetch('/api/v1/orders'); | ||||
|   const response = await res.json(); | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -1,13 +1,15 @@ | ||||
| <script lang="ts"> | ||||
|   import OrdersTable from './OrdersTable.svelte'; | ||||
|   import BadgeType from '$lib/interfaces/BadgeType'; | ||||
|   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; | ||||
|   import type { IOrderSummary } from '$lib/interfaces/IOrder'; | ||||
|  | ||||
|   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' | ||||
|   ); | ||||
|   const inTransitOrders = orders.filter((el) => el.status === BadgeType.PENDING); | ||||
| @@ -17,38 +19,25 @@ | ||||
|       el.status !== BadgeType.PENDING && | ||||
|       el.status !== BadgeType.INFO && | ||||
|       el.status !== 'INITIATED' && | ||||
|       el.status !== 'CONFIRMED' && | ||||
|       el.status !== BadgeType.WARNING | ||||
|   ); | ||||
|  | ||||
|   const deliveredOrders: Array<IOrder> = []; | ||||
|   const deliveredOrders: Array<IOrderSummary> = []; | ||||
| </script> | ||||
|  | ||||
| <PageMeta title="Orders" description="View all webshop orders" /> | ||||
| <div class="page"> | ||||
|   <h1>Orders</h1> | ||||
|   <section class="content"> | ||||
|     {#if attentionOrders?.length} | ||||
|       <h2>⚠️ orders needing attention</h2> | ||||
|  | ||||
|       <OrdersTable orders="{attentionOrders}" /> | ||||
|       <OrdersTable title="⚠️ orders needing attention" orders="{attentionOrders}" /> | ||||
|     {/if} | ||||
|  | ||||
|     <h2>📬 pending orders</h2> | ||||
|     <OrdersTable orders="{pendingOrders}" /> | ||||
|  | ||||
|     <h2>📦 in transit</h2> | ||||
|     <OrdersTable orders="{inTransitOrders}" /> | ||||
|  | ||||
|     <h2>🙅♀️ cancelled/returns</h2> | ||||
|     <OrdersTable orders="{otherOrders}" /> | ||||
|  | ||||
|     <h2>🏠🎁 delivered orders</h2> | ||||
|     <OrdersTable orders="{deliveredOrders}" /> | ||||
|     <OrdersTable title="📬 purchased orders" orders="{successfulOrders}" /> | ||||
|     <OrdersTable title="📦 in transit" orders="{inTransitOrders}" /> | ||||
|     <OrdersTable title="🙅♀️ cancelled/returns" orders="{otherOrders}" /> | ||||
|     <OrdersTable title="💤 incomplete orders" orders="{incompleteOrders}" /> | ||||
|     <OrdersTable title="🎁🏠 delivered orders" orders="{deliveredOrders}" /> | ||||
|   </section> | ||||
| </div> | ||||
|  | ||||
| <style lang="scss" module="scoped"> | ||||
|   section.content h2 { | ||||
|     // text-decoration: underline; | ||||
|     font-size: 1.2rem; | ||||
|   } | ||||
| </style> | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| <script lang="ts"> | ||||
|   import { goto } from '$app/navigation'; | ||||
|   import Badge from '$lib/components/Badge.svelte'; | ||||
|   import Time from '$lib/components/Time.svelte'; | ||||
|   import type { IOrderSummary } from '$lib/interfaces/IOrder'; | ||||
|  | ||||
|   export let title: string; | ||||
|   export let orders: Array<IOrderSummary>; | ||||
|  | ||||
|   function navigate(order: IOrderSummary) { | ||||
| @@ -10,15 +12,16 @@ | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <h2>{title} <span class="section-count">{orders?.length || 0}</span></h2> | ||||
| {#if orders?.length} | ||||
|   <table> | ||||
|     <thead> | ||||
|       <tr> | ||||
|         <th>Amount</th> | ||||
|         <th>Status</th> | ||||
|         <th>Order ID</th> | ||||
|         <th>Customer</th> | ||||
|         <th>Date</th> | ||||
|         <th>Order ID</th> | ||||
|         <th>Receipt</th> | ||||
|       </tr> | ||||
|     </thead> | ||||
| @@ -29,18 +32,12 @@ | ||||
|           <td>NOK {order.order_sum}</td> | ||||
|  | ||||
|           <td> | ||||
|             <Badge title="{order.status}" type="{order?.status?.type}" /> | ||||
|             <Badge title="{order.status}" /> | ||||
|           </td> | ||||
|  | ||||
|           <td>{order.order_id}</td> | ||||
|           <td>{order.first_name} {order.last_name}</td> | ||||
|           <td | ||||
|             >{order?.created | ||||
|               ? new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format( | ||||
|                   new Date(order.created) | ||||
|                 ) | ||||
|               : ''}</td | ||||
|           > | ||||
|           <td><Time time="{order?.created}" /></td> | ||||
|           <td>{order.order_id}</td> | ||||
|           <td> | ||||
|             <a href="receipt/{order.order_id}?email={order.email}">🧾</a> | ||||
|           </td> | ||||
| @@ -56,11 +53,10 @@ | ||||
|   @import '../../styles/media-queries.scss'; | ||||
|  | ||||
|   h2 { | ||||
|     // text-decoration: underline; | ||||
|     font-size: 1.2rem; | ||||
|  | ||||
|     .section-count { | ||||
|       background-color: rgba(0,0,0,0.15); | ||||
|       background-color: rgba(0, 0, 0, 0.15); | ||||
|       padding: 0.3rem 0.4rem; | ||||
|       margin-left: 0.5rem; | ||||
|       border-radius: 0.5rem; | ||||
| @@ -105,8 +101,14 @@ | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     th:last-of-type, | ||||
|     td:last-of-type { | ||||
|       text-align: center; | ||||
|     } | ||||
|  | ||||
|     @include mobile { | ||||
|       tr > *:first-child { | ||||
|       tr > *:nth-child(4), | ||||
|       tr > *:nth-child(5) { | ||||
|         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 { PageServerLoad } from './$types'; | ||||
|  | ||||
| export const load: PageServerLoad = async ({ fetch, params }) => { | ||||
|   const { id } = params; | ||||
|  | ||||
|   const res = await fetch(`/api/order/${id}`); | ||||
|   const orderResponse: IOrderDTO = await res.json(); | ||||
|   const res = await fetch(`/api/v1/order/${id}`); | ||||
|   const orderResponse = await res.json(); | ||||
|  | ||||
|   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 CustomerDetails from './CustomerDetails.svelte'; | ||||
|   import TrackingDetails from './TrackingDetails.svelte'; | ||||
|   import ShipmentProgress from '$lib/components/ShipmentProgress.svelte'; | ||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; | ||||
|   import type { PageServerData } from './$types'; | ||||
|  | ||||
|   export let data: PageServerData; | ||||
|   let order = data.order as IOrder; | ||||
|   console.log('order:', order); | ||||
|  | ||||
|   function orderSubTotal() { | ||||
|     if (!order || order.lineItems?.length === 0) return; | ||||
|     if (!order || order?.lineItems?.length === 0) return; | ||||
|  | ||||
|     let sum = 0; | ||||
|     order.lineItems.forEach((lineItem) => (sum = sum + lineItem.quantity * lineItem.price)); | ||||
| @@ -21,20 +21,21 @@ | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <h1>Order: {order.orderid}</h1> | ||||
| <h1>Order id: {order?.orderid}</h1> | ||||
| <div class="order"> | ||||
|   <!-- <p>Order: {JSON.stringify(order)}</p> --> | ||||
|   <h2 class="price"><span class="amount">{orderSubTotal()}.00</span> Nok</h2> | ||||
|  | ||||
|   <OrderSummary order="{order}" /> | ||||
|   <OrderProducts lineItems="{order?.lineItems}" /> | ||||
|  | ||||
|   <PaymentDetails order="{order}" /> | ||||
|   <PaymentDetails payment="{order?.payment}" /> | ||||
|   <CustomerDetails customer="{order?.customer}" /> | ||||
|   <TrackingDetails shipping="{order?.shipping}" /> | ||||
|   <TrackingDetails shipping="{order?.shipping}" orderId="{order?.orderid}" /> | ||||
| </div> | ||||
|  | ||||
| <style lang="scss"> | ||||
|   @import '../../../styles/media-queries.scss'; | ||||
|  | ||||
|   h2.price { | ||||
|     font-size: 1.5rem; | ||||
|     color: grey; | ||||
|   | ||||
| @@ -67,19 +67,20 @@ | ||||
|     .image-column { | ||||
|       width: 4rem; | ||||
|       max-width: 4rem; | ||||
|  | ||||
|       @include desktop { | ||||
|         margin: 0 0.5rem; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     td, | ||||
|     th { | ||||
|       white-space: nowrap; | ||||
|       padding: 0.4rem 0.6rem; | ||||
|     } | ||||
|  | ||||
|     tbody { | ||||
|       img { | ||||
|         width: 4rem; | ||||
|         height: 4rem; | ||||
|         border-radius: 0.4rem; | ||||
|       } | ||||
|  | ||||
| @@ -102,11 +103,5 @@ | ||||
|         display: none; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // @include mobile { | ||||
|     //   tr > *:last-child, tr > :nth-child(4) { | ||||
|     //     display: none; | ||||
|     //   } | ||||
|     // } | ||||
|   } | ||||
| </style> | ||||
|   | ||||
| @@ -1,14 +1,25 @@ | ||||
| <script lang="ts"> | ||||
|   import Badge from '$lib/components/Badge.svelte'; | ||||
|   import Time from '$lib/components/Time.svelte'; | ||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; | ||||
|  | ||||
|   export let order: IOrder; | ||||
|   let paymentMethod: string = Math.random() > 0.5 ? 'Stripe' : 'ApplePay'; | ||||
| </script> | ||||
|  | ||||
| <ul class="summary-list"> | ||||
|   <li> | ||||
|     <span class="label">Status</span> | ||||
|     <Badge title="{order.status}" /> | ||||
|   </li> | ||||
|  | ||||
|   <li> | ||||
|     <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> | ||||
| @@ -18,12 +29,12 @@ | ||||
|  | ||||
|   <li> | ||||
|     <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> | ||||
|     <span class="label">Payment method</span> | ||||
|     <span>{paymentMethod}</span> | ||||
|     <span style="text-transform: capitalize;">{order?.payment?.type}</span> | ||||
|   </li> | ||||
| </ul> | ||||
|  | ||||
|   | ||||
| @@ -1,36 +1,94 @@ | ||||
| <script lang="ts"> | ||||
|   import Badge from '$lib/components/Badge.svelte'; | ||||
|   // import type BadgeType from '$lib/interfaces/BadgeType'; | ||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; | ||||
|   import Time from '$lib/components/Time.svelte'; | ||||
|   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> | ||||
|  | ||||
| <section> | ||||
|   <h2>Payment details</h2> | ||||
|   <ul class="property-list"> | ||||
|     <li> | ||||
|       <span class="label">Amount</span> | ||||
|       <span>{order?.payment?.amount}.10 kr</span> | ||||
|       <span class="label">Stripe link</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> | ||||
|       <span class="label">Fee</span> | ||||
|       <span>2.25 kr</span> | ||||
|       <span class="label">Amount requested</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> | ||||
|       <span class="label">Net</span> | ||||
|       <span>7.85 kr</span> | ||||
|       <span>{net === null ? '-' : round(net) + ' kr'}</span> | ||||
|     </li> | ||||
|  | ||||
|     <li> | ||||
|       <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> | ||||
|   </ul> | ||||
| </section> | ||||
|  | ||||
| <style lang="scss"> | ||||
|   @import './styles-order-page.scss'; | ||||
|  | ||||
|   a.link { | ||||
|     max-width: 60%; | ||||
|     overflow-x: hidden; | ||||
|   } | ||||
| </style> | ||||
|   | ||||
| @@ -1,35 +1,139 @@ | ||||
| <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> | ||||
|  | ||||
| {#if shipping} | ||||
|   <section> | ||||
| <section> | ||||
|   <h2>Tracking</h2> | ||||
|  | ||||
|   {#if shipping} | ||||
|     <ul class="property-list"> | ||||
|       <li> | ||||
|         <span class="label">Tracking code</span> | ||||
|         <span>{shipping.tracking_code}</span> | ||||
|         <span class="label" for="courier">Tracking company</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> | ||||
|         <span class="label">Tracking company</span> | ||||
|         <span>{shipping.company}</span> | ||||
|         <span class="label">Tracking code</span> | ||||
|         {#if !edit} | ||||
|           <span>{shipping.tracking_code}</span> | ||||
|         {:else} | ||||
|           <input bind:value="{trackingCode}" /> | ||||
|         {/if} | ||||
|       </li> | ||||
|  | ||||
|       <li> | ||||
|         <span class="label">Link</span> | ||||
|         <span | ||||
|           ><a href="{shipping.tracking_link}" target="_blank" rel="noopener noreferrer" | ||||
|             >{shipping.tracking_link}</a | ||||
|           ></span | ||||
|         > | ||||
|         {#if !edit} | ||||
|           <a href="{shipping.tracking_link}" class="link" target="_blank" rel="noopener noreferrer"> | ||||
|             {shipping.tracking_link} | ||||
|           </a> | ||||
|         {:else} | ||||
|           <input style="margin-bottom: 0" class="wide" bind:value="{trackingLink}" /> | ||||
|         {/if} | ||||
|       </li> | ||||
|     </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"> | ||||
|   @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> | ||||
|   | ||||
| @@ -16,8 +16,7 @@ section { | ||||
|   } | ||||
| } | ||||
|  | ||||
| .label, | ||||
| .empty { | ||||
| .label { | ||||
|   color: grey; | ||||
| } | ||||
|  | ||||
| @@ -51,7 +50,6 @@ ul.property-list { | ||||
|  | ||||
|   li span:last-of-type { | ||||
|     @include mobile { | ||||
|       min-width: 60%; | ||||
|       white-space: normal; | ||||
|       overflow-wrap: break-word; | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user