diff --git a/client/src/components/store/ShippingTimeline.tsx b/client/src/components/store/ShippingTimeline.tsx
new file mode 100644
index 00000000..66661060
--- /dev/null
+++ b/client/src/components/store/ShippingTimeline.tsx
@@ -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 (
+
+ {/* Title outside the box, matching "Order summary" style */}
+
+ Estimated Production & Delivery Timeline
+
+
+ {/* Content box with same styling as order summary */}
+
+
+ {timelineSteps.map((step, stepIdx) => (
+
+
+
+
+
+ {stepIdx < timelineSteps.length - 1 && (
+
+ )}
+
+
+
+ {step.name}
+
+
{step.date}
+
+
+ ))}
+
+
+
+ *Please note: Because your book is printed specially for you upon ordering,
+ production times can vary before shipping begins.
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/screens/conductor/store/shipping.tsx b/client/src/screens/conductor/store/shipping.tsx
index 5d60c7b1..c4a4373f 100644
--- a/client/src/screens/conductor/store/shipping.tsx
+++ b/client/src/screens/conductor/store/shipping.tsx
@@ -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 = [
{
@@ -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) {
@@ -507,7 +522,7 @@ export default function ShippingPage() {
)}
-
-
-
+ {hasPhysicalProducts && selectedShippingOption && (
+
+ )}
+
If you have any questions or concerns, please contact our{" "}
= {
+ '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))) {
@@ -540,6 +573,18 @@ 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})` : ''}`,
@@ -547,6 +592,12 @@ class StoreService {
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),
}
});
diff --git a/server/types/Store.ts b/server/types/Store.ts
index 1bc674af..e0effb0d 100644
--- a/server/types/Store.ts
+++ b/server/types/Store.ts
@@ -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;
}
/**