Displays payment, shipping, errors page & edit and add shipment

This commit is contained in:
2022-12-29 23:33:52 +01:00
parent dc7663edce
commit eefd3aa6d6
14 changed files with 564 additions and 90 deletions

View 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>

View 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}

View 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
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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;
}
}

View 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>

View File

@@ -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 };
};

View File

@@ -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;

View File

@@ -67,19 +67,20 @@
.image-column {
width: 4rem;
max-width: 4rem;
margin: 0 0.5rem;
@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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
<h2>Tracking</h2>
<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>

View File

@@ -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;
}