Skip to content

Commit 4f7629b

Browse files
committed
fix!: fix timezone problems.
Always convert time to UTC when sending data to the server and only display the local time on the client.
1 parent 20faaad commit 4f7629b

18 files changed

Lines changed: 130 additions & 113 deletions

File tree

bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
"docker:down:volumes": "docker-compose -f docker-compose.yml -f docker-compose.db.yml -p pizza down --volumes"
2525
},
2626
"dependencies": {
27+
"@date-fns/utc": "^2.1.0",
2728
"@tailwindcss/postcss": "^4.1.8",
2829
"@types/js-yaml": "^4.0.9",
2930
"@yudiel/react-qr-scanner": "^2.3.1",
3031
"accept-language": "^3.0.20",
3132
"caniuse-lite": "^1.0.30001718",
3233
"date-fns": "^4.1.0",
34+
"date-fns-tz": "^3.2.0",
3335
"js-yaml": "^4.1.0",
3436
"lucide-react": "^0.513.0",
3537
"mongoose": "^8.15.1",
@@ -47,10 +49,10 @@
4749
"@types/react": "^19.1.7",
4850
"@types/react-dom": "^19.0.0",
4951
"autoprefixer": "^10.4.21",
52+
"bun-types": "^1.2.15",
5053
"oxlint": "^0.18.1",
5154
"postcss": "^8.5.4",
5255
"tailwindcss": "^4.1.8",
53-
"bun-types": "^1.2.15",
5456
"ts-morph": "^26.0.0",
5557
"typescript": "^5.8.3"
5658
},

src/app/admin/prepare/order/page.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import React, { useEffect, useMemo, useState } from 'react';
44
import { AlertTriangle, CheckCircle, Minus, Pizza, Plus, ScanIcon, XIcon } from 'lucide-react';
55
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
66
import { ITEM_STATUSES, ItemStatus, ORDER_STATUSES, OrderDocument } from '@/model/order';
7-
import { getDateFromTimeSlot } from '@/lib/time';
7+
import { timeslotToDate, timeslotToLocalTime } from '@/lib/time';
88
import SearchInput from '@/app/components/SearchInput';
99
import Button from '@/app/components/Button';
1010
import { ItemDocument } from "@/model/item";
1111
import { ordersSortedByTimeslots } from "@/lib/order";
1212
import { Heading } from "@/app/components/layout/Heading";
13-
import { format } from "date-fns";
1413
import ErrorMessage from "@/app/components/ErrorMessage";
1514

1615
interface ItemType {
@@ -159,7 +158,6 @@ const OrderCard = ({
159158
onPay: (orderId: string) => void;
160159
onDeliver: (orderId: string) => void;
161160
}) => {
162-
const timeslotDate = getDateFromTimeSlot(order.timeslot);
163161
const readyItems = order.items.filter(item => item.status === ITEM_STATUSES.READY).length;
164162
const totalItems = order.items.length;
165163
const progress = (readyItems / totalItems) * 100;
@@ -176,7 +174,7 @@ const OrderCard = ({
176174
<div className={`px-3 py-1 rounded-full text-sm font-medium ${
177175
isOverdue ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'
178176
}`}>
179-
{format(timeslotDate, 'HH:mm')}
177+
{timeslotToLocalTime(order.timeslot)}
180178
</div>
181179
</div>
182180

@@ -488,8 +486,8 @@ const OrderManagerDashboard = () => {
488486

489487
// Sort by timeslot
490488
return filtered.sort((a, b) => {
491-
const timeA = getDateFromTimeSlot(a.timeslot).getTime();
492-
const timeB = getDateFromTimeSlot(b.timeslot).getTime();
489+
const timeA = timeslotToDate(a.timeslot).getTime();
490+
const timeB = timeslotToDate(b.timeslot).getTime();
493491
return timeA - timeB;
494492
});
495493
}, [orders, activeTab, searchFilter]);
@@ -499,7 +497,7 @@ const OrderManagerDashboard = () => {
499497
const now = Date.now()
500498
return filteredOrders.filter(order =>
501499
(order.status !== ORDER_STATUSES.DELIVERED && order.status !== ORDER_STATUSES.CANCELLED) &&
502-
getDateFromTimeSlot(order.timeslot).getTime() < now
500+
timeslotToDate(order.timeslot).getTime() < now
503501
);
504502
}, [filteredOrders]);
505503

@@ -588,7 +586,7 @@ const OrderManagerDashboard = () => {
588586
{filteredOrders.map(order => {
589587
const now = Date.now()
590588
const isOverdue = (order.status !== ORDER_STATUSES.DELIVERED && order.status !== ORDER_STATUSES.CANCELLED)
591-
&& getDateFromTimeSlot(order.timeslot).getTime() < now
589+
&& timeslotToDate(order.timeslot).getTime() < now
592590

593591
return (
594592
<OrderCard

src/app/admin/prepare/pizza/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ItemDocument } from "@/model/item";
88
import { ordersSortedByTimeslots } from "@/lib/order";
99
import { Heading } from "@/app/components/layout/Heading";
1010
import { useTranslations } from 'next-intl';
11+
import { timeslotToLocalTime } from "@/lib/time";
1112

1213
interface ItemType {
1314
id: string;
@@ -150,7 +151,7 @@ const PizzaMakerStation = () => {
150151
key={`${order._id.toString()}-${item.item._id.toString()}-${item.status}-${index}`}
151152
className={`border px-3 py-1 rounded-lg text-sm flex items-center justify-between ${item.status !== ITEM_STATUSES.PREPPING ? 'bg-green-50 border-green-100 text-gray-400' : ''}`}>
152153
{item.item.name} <span
153-
className="bg-gray-100 py-0.5 px-1 rounded-2xl">{order.timeslot}</span>
154+
className="bg-gray-100 py-0.5 px-1 rounded-2xl">{timeslotToLocalTime(order.timeslot)}</span>
154155
</div>)
155156
)}
156157
</>

src/app/api/timeline/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import dbConnect from "@/lib/dbConnect";
33
import { aggregateOrdersIntoSlots, formatResponseData, generateTimeSlots, getAlignedSlotStartTime } from "@/lib/time";
44
import { ItemDocument, ItemModel } from "@/model/item";
55
import { OrderDocument, OrderModel } from "@/model/order";
6+
import { UTCDate } from "@date-fns/utc";
67

78
export const dynamic = "force-dynamic";
89
export const fetchCache = "force-no-store";
@@ -28,12 +29,12 @@ async function fetchAndMapItems(): Promise<Record<string, ItemDocument>> {
2829
}, {});
2930
}
3031

31-
32-
// --- API Route Handler ---
3332
export async function GET(request: Request) {
3433
await dbConnect();
3534

36-
const now = new Date();
35+
// We don't know which time zone the server has, thus we use and send the UTC time
36+
// which we then locally convert to the correct time
37+
const now = new UTCDate();
3738

3839
// 1. Determine the reference start time for generating slots
3940
const firstSlotStartTime = getAlignedSlotStartTime(
@@ -66,8 +67,7 @@ export async function GET(request: Request) {
6667
// 6. Format the response
6768
// The "current" slot in the UI corresponds to the one that `now` would fall into,
6869
// which is `PAST_SLOTS_TO_SHOW` index in our 0-indexed `timeSlots` array.
69-
const currentSlotDisplayIndex = TIME_SLOT_CONFIG.PAST_SLOTS_TO_SHOW;
70-
const responseData = formatResponseData(timeSlots, orderAmountsPerSlot, currentSlotDisplayIndex);
70+
const responseData = formatResponseData(timeSlots, orderAmountsPerSlot);
7171

7272
return Response.json(responseData);
7373
}

src/app/components/Timeline.tsx

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
'use client'
22

3-
import React, { useCallback, useEffect, useState } from 'react';
3+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
44
import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
55
import { useTranslations } from 'next-intl';
66
import useOrderStore from "@/app/zustand/order";
77
import { AggregatedSlotData } from "@/model/timeslot";
8-
import { ORDER_AMOUNT_THRESHOLDS } from "@/config";
9-
10-
interface TimelineProps {
11-
startDate: Date;
12-
stopDate: Date;
13-
every_x_seconds: number;
14-
}
8+
import { ORDER_AMOUNT_THRESHOLDS, TIME_SLOT_CONFIG } from "@/config";
9+
import { timeslotToLocalTime } from "@/lib/time";
1510

1611
const generateAllTimeslots = (startDate: Date, stopDate: Date): AggregatedSlotData[] => {
1712
const timeSlots: AggregatedSlotData[] = [];
@@ -28,16 +23,24 @@ const generateAllTimeslots = (startDate: Date, stopDate: Date): AggregatedSlotDa
2823
return timeSlots;
2924
};
3025

31-
const Timeline: React.FC<TimelineProps> = ({
32-
startDate,
33-
stopDate,
34-
every_x_seconds
35-
}) => {
26+
const Timeline = () => {
3627
const [timeslots, setTimeslots] = useState<AggregatedSlotData[]>([]);
3728
const [isLoading, setIsLoading] = useState(true);
3829
const [error, setError] = useState<string | null>(null);
3930
const t = useTranslations();
4031

32+
// Timeline date range calculation (can be done outside component if static)
33+
const { startDate, stopDate } = useMemo(() => {
34+
const startDate = new Date()
35+
startDate.setHours(startDate.getHours() - 1);
36+
startDate.setMinutes(0, 0, 0);
37+
38+
const stopDate = new Date();
39+
stopDate.setHours(stopDate.getHours() + 1);
40+
stopDate.setMinutes(59, 59, 999);
41+
return { startDate, stopDate };
42+
}, []);
43+
4144
const { setTimeslot } = useOrderStore();
4245
const fetchTimeline = useCallback(async () => {
4346
setError(null);
@@ -47,18 +50,22 @@ const Timeline: React.FC<TimelineProps> = ({
4750
setError('Failed to fetch timeline data');
4851
apiSlots = generateAllTimeslots(startDate, stopDate);
4952
} else {
50-
apiSlots = await response.json();
53+
apiSlots = await response.json()
54+
apiSlots = apiSlots.map((item: AggregatedSlotData) => ({
55+
...item,
56+
time: timeslotToLocalTime(item.time),
57+
}))
5158
}
5259

5360
setTimeslots(apiSlots);
5461
setIsLoading(false);
55-
}, [startDate, stopDate, every_x_seconds]);
62+
}, [startDate, stopDate, TIME_SLOT_CONFIG.UPDATE_EVERY_SECONDS]);
5663

5764
useEffect(() => {
5865
fetchTimeline();
59-
const interval = setInterval(fetchTimeline, every_x_seconds * 1000);
66+
const interval = setInterval(fetchTimeline, TIME_SLOT_CONFIG.UPDATE_EVERY_SECONDS * 1000);
6067
return () => clearInterval(interval);
61-
}, [fetchTimeline, every_x_seconds]);
68+
}, [fetchTimeline, TIME_SLOT_CONFIG.UPDATE_EVERY_SECONDS]);
6269

6370
const handleBarClick = useCallback((event: any) => {
6471
if (event?.activeLabel) {

src/app/components/order/OrderSummary.tsx

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ItemDocument } from "@/model/item"; // Assuming this path is correct
22
import OrderButton from "@/app/components/order/OrderButton";
3-
import React, { useMemo } from "react";
3+
import React from "react";
44
import useOrderStore, { useCurrentOrder } from "@/app/zustand/order";
55
import { ArrowLeft, Plus, X } from "lucide-react";
6-
import { TIME_SLOT_CONFIG } from "@/config";
76
import Timeline from "@/app/components/Timeline";
87
import ErrorMessage from "@/app/components/ErrorMessage";
98
import Button from "@/app/components/Button";
109
import { useTranslations } from 'next-intl';
10+
import { timeslotToLocalTime } from "@/lib/time";
1111

1212
interface OrderSummaryPageProps {
1313
onClose: () => void;
@@ -37,18 +37,6 @@ const OrderSummary: React.FC<OrderSummaryPageProps> = ({
3737

3838
const isEmpty = aggregatedItems.length === 0;
3939

40-
// Timeline date range calculation (can be done outside component if static)
41-
const { start, end } = useMemo(() => {
42-
const startDate = new Date();
43-
startDate.setHours(startDate.getHours() - 1);
44-
startDate.setMinutes(0, 0, 0);
45-
46-
const endDate = new Date();
47-
endDate.setHours(endDate.getHours() + 1);
48-
endDate.setMinutes(59, 59, 999);
49-
return { start: startDate, end: endDate };
50-
}, []);
51-
5240
return (
5341
<div>
5442
<div className="mx-auto space-y-6">
@@ -127,7 +115,7 @@ const OrderSummary: React.FC<OrderSummaryPageProps> = ({
127115
<div className="flex justify-between text-sm text-gray-700 mt-2">
128116
<span>{t('cart.timeslot.title')}</span>
129117
<span>
130-
{order.timeslot}
118+
{timeslotToLocalTime(order.timeslot)}
131119
</span>
132120
</div>
133121
</div>
@@ -157,15 +145,10 @@ const OrderSummary: React.FC<OrderSummaryPageProps> = ({
157145
<p className="mb-4 text-base font-light leading-relaxed text-gray-700">
158146
{t('cart.timeslot.subtitle')}
159147
</p>
160-
<Timeline
161-
startDate={start}
162-
stopDate={end}
163-
every_x_seconds={TIME_SLOT_CONFIG.UPDATE_EVERY_SECONDS}
164-
/>
148+
<Timeline/>
165149
</div>
166150
</div>
167151

168-
{/* Error message */}
169152
{error && (
170153
<div className="px-4 py-2">
171154
<ErrorMessage error={error}/>

src/app/order/[orderNumber]/clientOrderPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useEffect, useState } from "react";
4-
import { getDateFromTimeSlot } from "@/lib/time";
4+
import { timeslotToDate, timeslotToLocalTime } from "@/lib/time";
55
import { Order, OrderStatus } from "@/model/order";
66
import OrderQR from "@/app/components/order/OrderQR";
77
import { Loading } from "@/app/components/Loading";
@@ -125,10 +125,10 @@ export default function ClientOrderPage({ orderNumber }: { orderNumber: string }
125125
<div className="bg-blue-50 rounded-2xl p-6 mb-6 max-w-sm mx-auto">
126126
<p className="text-sm text-gray-600 mb-1">{t('order_status.status.ready_by')}</p>
127127
<p className="text-3xl font-light text-blue-600">
128-
{formatDate(getDateFromTimeSlot(order.timeslot), 'HH:mm')}
128+
{timeslotToLocalTime(formatDate(timeslotToDate(order.timeslot), 'HH:mm'))}
129129
</p>
130130
<p className="text-sm text-gray-600">
131-
{formatDate(getDateFromTimeSlot(order.timeslot), 'dd.MM.yyyy')}
131+
{formatDate(timeslotToDate(order.timeslot), 'dd.MM.yyyy')}
132132
</p>
133133
</div>
134134
)}

src/app/order/[orderNumber]/thank-you/page.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Suspense } from 'react';
22
import ThankYouPage from './thankYouPage';
33
import { getTranslations } from 'next-intl/server';
4+
import { Loading } from '@/app/components/Loading';
45

56
// Server Component that receives params as Promise
67
export default async function Page({
@@ -13,9 +14,7 @@ export default async function Page({
1314
const t = await getTranslations();
1415

1516
return (
16-
<Suspense fallback={<div className="p-6 max-w-md mx-auto">
17-
<h1 className="text-2xl font-semibold">{t('order.suspense.loading')}</h1>
18-
</div>}>
17+
<Suspense fallback={<Loading message={t('order_status.suspense.loading')}/>}>
1918
<ThankYouPage orderNumber={orderNumber}/>
2019
</Suspense>
2120
);

0 commit comments

Comments
 (0)