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() {
{ e.preventDefault(); }} @@ -898,22 +913,23 @@ 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; } /**