Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions client/src/components/store/ShippingTimeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { StoreShippingOption } from "../../types";
import { IconCheck, IconSettings, IconTruck, IconHome } from "@tabler/icons-react";

interface ShippingTimelineProps {
shippingOption: StoreShippingOption;
}

export default function ShippingTimeline({ shippingOption }: ShippingTimelineProps) {
if (
!shippingOption.production_start_date_estimate ||
!shippingOption.production_end_date_estimate ||
!shippingOption.ship_date_start_estimate ||
!shippingOption.ship_date_end_estimate ||
!shippingOption.delivery_date_start_estimate ||
!shippingOption.delivery_date_end_estimate
) {
// Don't render if date fields are missing
return null;
}

const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
}).format(date);
};

const formatDateRange = (start: string, end: string): string => {
return `${formatDate(start)} - ${formatDate(end)}`;
};

const today = new Date();
today.setHours(0, 0, 0, 0);
const todayFormatted = formatDate(today.toISOString().split('T')[0]);

const timelineSteps = [
{
name: "Order Placed",
icon: IconCheck,
date: `Today (${todayFormatted})`,
completed: true,
},
{
name: "Printing (On-Demand)",
icon: IconSettings,
date: formatDateRange(
shippingOption.production_start_date_estimate,
shippingOption.production_end_date_estimate
),
completed: false,
},
{
name: "Shipped",
icon: IconTruck,
date: formatDateRange(
shippingOption.ship_date_start_estimate,
shippingOption.ship_date_end_estimate
),
completed: false,
},
{
name: "Estimated Arrival",
icon: IconHome,
date: formatDateRange(
shippingOption.delivery_date_start_estimate,
shippingOption.delivery_date_end_estimate
),
completed: false,
},
];

return (
<div className="mt-6">
{/* Title outside the box, matching "Order summary" style */}
<h3 className="text-lg font-medium text-gray-900">
Estimated Production & Delivery Timeline
</h3>

{/* Content box with same styling as order summary */}
<div className="mt-4 rounded-lg border border-gray-200 bg-white shadow-sm px-4 py-6 sm:px-6">
<div className="flex items-center justify-between">
{timelineSteps.map((step, stepIdx) => (
<div key={step.name} className="flex flex-col items-center flex-1">
<div className="relative flex items-center">
<div
className={`flex h-10 w-10 items-center justify-center rounded-full border-2 ${
step.completed
? "border-primary bg-primary"
: "border-gray-300 bg-white"
}`}
>
<step.icon
className={`h-6 w-6 ${
step.completed ? "text-white" : "text-gray-400"
}`}
aria-hidden="true"
/>
</div>
{stepIdx < timelineSteps.length - 1 && (
<div
className={`absolute left-1/2 top-5 h-0.5 w-full ${
step.completed ? "bg-primary" : "bg-gray-300"
}`}
style={{ transform: "translateX(50%)" }}
aria-hidden="true"
/>
)}
</div>
<div className="mt-4 text-center">
<p
className={`text-sm font-medium ${
step.completed ? "text-primary" : "text-gray-500"
}`}
>
{step.name}
</p>
<p className="mt-1 text-xs text-gray-500">{step.date}</p>
</div>
</div>
))}
</div>

<p className="mt-6 text-sm text-gray-500 text-center">
*Please note: Because your book is printed specially for you upon ordering,
production times can vary before shipping begins.
</p>
</div>
</div>
);
}
48 changes: 32 additions & 16 deletions client/src/screens/conductor/store/shipping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import LoadingSpinner from "../../../components/LoadingSpinner";
import { useTypedSelector } from "../../../state/hooks";
import { useModals } from "../../../context/ModalContext";
import ConfirmOrderModal from "../../../components/store/ConfirmOrderModal";
import ShippingTimeline from "../../../components/store/ShippingTimeline";

const STATE_CODES = [
{
Expand Down Expand Up @@ -415,6 +416,20 @@ export default function ShippingPage() {
setShippingOptions(response.data.options);
shippingCalculated.current = true;

console.log("=== Shipping Options Response ===");
console.log("Full response:", response.data);
if (Array.isArray(response.data.options) && response.data.options.length > 0) {
console.log("First shipping option:", response.data.options[0]);
console.log("Has date fields?", {
production_start: !!response.data.options[0].production_start_date_estimate,
delivery_end: !!response.data.options[0].delivery_date_end_estimate,
});
}

if (Array.isArray(response.data.options) && response.data.options.length > 0) {
console.log("Shipping option with dates:", response.data.options[0]);
}

if (response.data.options === "digital_delivery_only") {
setSelectedShippingOption(null);
} else if (response.data.options.length > 0) {
Expand Down Expand Up @@ -507,7 +522,7 @@ export default function ShippingPage() {
<AlternateLayout>
<div className="mx-auto max-w-2xl px-4 pb-24 pt-16 sm:px-6 lg:max-w-7xl lg:px-8">
<form
className="lg:grid lg:grid-cols-2 lg:gap-x-12 xl:gap-x-16 min-w-[90vw] lg:min-w-[1200px] lg:items-start"
className="lg:grid lg:grid-cols-2 lg:gap-x-12 xl:gap-x-16"
onSubmit={(e) => {
e.preventDefault();
}}
Expand Down Expand Up @@ -898,22 +913,23 @@ export default function ShippingPage() {
</div>
</div>
)}
<div className="border-t border-gray-200 px-4 py-6 sm:px-6">
<button
type="submit"
className="w-full rounded-md border border-transparent bg-primary px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-50 disabled:bg-opacity-55 disabled:cursor-not-allowed"
onClick={handleProceedToPayment}
disabled={proceedDisabled || loading || shippingLoading}
>
{loading || shippingLoading ? (
<LoadingSpinner iconOnly />
) : (
<Icon name="arrow right" className="!mb-1 !mr-2" />
)}
Proceed to Payment
</button>
</div>
</div>
{hasPhysicalProducts && selectedShippingOption && (
<ShippingTimeline shippingOption={selectedShippingOption} />
)}
<button
type="submit"
className="mt-6 w-full rounded-md border border-transparent bg-primary px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-50 disabled:bg-opacity-55 disabled:cursor-not-allowed"
onClick={handleProceedToPayment}
disabled={proceedDisabled || loading || shippingLoading}
>
{loading || shippingLoading ? (
<LoadingSpinner iconOnly />
) : (
<Icon name="arrow right" className="!mb-1 !mr-2" />
)}
Proceed to Payment
</button>
<p className="mt-4 text-sm text-gray-500 text-center">
If you have any questions or concerns, please contact our{" "}
<a
Expand Down
7 changes: 7 additions & 0 deletions client/src/types/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,14 @@ export type StoreShippingOption = {
id: number;
total_days_min: number;
total_days_max: number;
lulu_shipping_level: string;
cost_excl_tax: number;
production_start_date_estimate: string;
production_end_date_estimate: string;
ship_date_start_estimate: string;
ship_date_end_estimate: string;
delivery_date_start_estimate: string;
delivery_date_end_estimate: string;
}

export type StoreDigitalDeliveryOption = "apply_to_account" | "email_access_codes";
Expand Down
51 changes: 51 additions & 0 deletions server/api/services/store-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,39 @@ class StoreService {
throw new Error("No shipping options found for the provided items");
}

const shippingLevelDeliveryDays: Record<string, { min: number; max: number }> = {
'MAIL': { min: 13, max: 15 },
'PRIORITY_MAIL': { min: 11, max: 13 },
'GROUND_HD': { min: 12, max: 14 },
'GROUND_BUS': { min: 12, max: 14 },
'GROUND': { min: 12, max: 14 },
'EXPEDITED': { min: 8, max: 9 },
'EXPRESS': { min: 9, max: 10 },
};

// Get today's date (order date)
const today = new Date();
today.setHours(0, 0, 0, 0);

// Helper function to add business days (excluding weekends)
const addBusinessDays = (date: Date, days: number): Date => {
const result = new Date(date);
let added = 0;
while (added < days) {
result.setDate(result.getDate() + 1);
// Skip weekends (Saturday = 6, Sunday = 0)
if (result.getDay() !== 0 && result.getDay() !== 6) {
added++;
}
}
return result;
};

// Helper function to format date as ISO string
const formatDate = (date: Date): string => {
return date.toISOString().split('T')[0];
};

const mapped = filtered_shipping_options.map((opt) => {
// ensure cost_excl_tax is a number and convert it to cents
if (!opt.cost_excl_tax || isNaN(parseFloat(opt.cost_excl_tax))) {
Expand All @@ -540,13 +573,31 @@ class StoreService {
}
const costInCents = Math.round(parseFloat(opt.cost_excl_tax) * 100);

// Get delivery days for this shipping level (fallback to MAIL if not found)
const shippingLevel = opt.level || 'MAIL';
const deliveryDays = shippingLevelDeliveryDays[shippingLevel] || shippingLevelDeliveryDays['MAIL'];

// Calculate date estimates
const productionStartDate = addBusinessDays(today, 2);
const productionEndDate = addBusinessDays(today, 4);
const shipDateStart = addBusinessDays(today, 6);
const shipDateEnd = addBusinessDays(today, 10);
const deliveryDateStart = addBusinessDays(today, deliveryDays.min);
const deliveryDateEnd = addBusinessDays(today, deliveryDays.max);

return {
id: opt.id,
title: `${opt.level}${opt.carrier_service_name ? ` (${opt.carrier_service_name})` : ''}`,
total_days_min: opt.total_days_min,
total_days_max: opt.total_days_max,
lulu_shipping_level: opt.level,
cost_excl_tax: costInCents,
production_start_date_estimate: formatDate(productionStartDate),
production_end_date_estimate: formatDate(productionEndDate),
ship_date_start_estimate: formatDate(shipDateStart),
ship_date_end_estimate: formatDate(shipDateEnd),
delivery_date_start_estimate: formatDate(deliveryDateStart),
delivery_date_end_estimate: formatDate(deliveryDateEnd),
}
});

Expand Down
6 changes: 6 additions & 0 deletions server/types/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export type StoreShippingOption = {
total_days_max: number;
lulu_shipping_level: string;
cost_excl_tax: number;
production_start_date_estimate: string;
production_end_date_estimate: string;
ship_date_start_estimate: string;
ship_date_end_estimate: string;
delivery_date_start_estimate: string;
delivery_date_end_estimate: string;
}

/**
Expand Down