🐛 fix: Update order_id to order_no in BalanceLogPage and related typings; enhance timezone switch component with additional features and localization updates

This commit is contained in:
web 2025-09-05 08:50:57 -07:00
parent a4de9df1fc
commit ac36075e7b
33 changed files with 287 additions and 66 deletions

View File

@ -44,9 +44,9 @@ export default function BalanceLogPage() {
cell: ({ row }) => <Display type='currency' value={row.original.amount} />,
},
{
accessorKey: 'order_id',
header: t('column.orderId'),
cell: ({ row }) => <OrderLink orderId={row.original.order_id} />,
accessorKey: 'order_no',
header: t('column.orderNo'),
cell: ({ row }) => <OrderLink orderId={row.original.order_no} />,
},
{
accessorKey: 'balance',

View File

@ -1,79 +1,156 @@
'use client';
import { Button } from '@workspace/ui/components/button';
import { Command, CommandInput, CommandItem, CommandList } from '@workspace/ui/components/command';
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@workspace/ui/components/command';
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/ui/components/popover';
import { Icon } from '@workspace/ui/custom-components/icon';
import { cn } from '@workspace/ui/lib/utils';
import { useMemo, useState } from 'react';
import { useLocale } from 'next-intl';
import { useEffect, useMemo, useState } from 'react';
interface TimezoneOption {
value: string;
label: string;
offset: string;
timezone: string;
}
function getAllTimezones(): TimezoneOption[] {
function getCurrentTime(timezone: string): string {
try {
const now = new Date();
return now.toLocaleTimeString('en-US', {
timeZone: timezone,
hour12: false,
hour: '2-digit',
minute: '2-digit',
});
} catch {
return '--:--';
}
}
function getAllTimezones(locale: string = 'en-US'): TimezoneOption[] {
try {
const timeZones = Intl.supportedValuesOf('timeZone');
return [
{
value: 'UTC',
label: 'UTC',
offset: '+00:00',
},
].concat(
timeZones
.map((tz) => {
const parts = tz.split('/');
let label = tz;
if (parts.length >= 2) {
const region = parts[0];
const city = parts[1]?.replace(/_/g, ' ') || '';
label = `${city} (${region})`;
}
const processed = timeZones
.map((tz) => {
try {
return {
value: tz,
label: label,
offset: getTimezoneOffset(tz),
label: tz,
timezone: getTimezoneOffset(tz),
};
})
.sort((a, b) => a.label.localeCompare(b.label)),
);
} catch {
return {
value: tz,
label: tz,
timezone: 'UTC+00:00',
};
}
})
.filter(Boolean)
.sort((a, b) => a.label.localeCompare(b.label, locale));
const hasUTC = processed.some((tz) => tz.value === 'UTC');
if (!hasUTC) {
processed.unshift({
value: 'UTC',
label: 'UTC',
timezone: 'UTC+00:00',
});
}
return processed;
} catch {
return [
{
value: 'UTC',
label: 'UTC',
offset: '+00:00',
timezone: 'UTC+00:00',
},
];
}
}
function getServerTimezones(): string[] {
return ['UTC'];
}
function getRecommendedTimezones(): string[] {
try {
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (browserTimezone.startsWith('Asia/')) {
return ['Asia/Shanghai', 'Asia/Tokyo', 'Asia/Kolkata', 'Asia/Singapore', 'Asia/Seoul'];
} else if (browserTimezone.startsWith('Europe/')) {
return ['Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Rome', 'Europe/Madrid'];
} else if (browserTimezone.startsWith('America/')) {
return [
'America/New_York',
'America/Los_Angeles',
'America/Chicago',
'America/Denver',
'America/Toronto',
];
} else if (browserTimezone.startsWith('Australia/')) {
return ['Australia/Sydney', 'Australia/Melbourne', 'Australia/Perth', 'Australia/Brisbane'];
} else {
return [
'America/New_York',
'Europe/London',
'Asia/Shanghai',
'Asia/Tokyo',
'Australia/Sydney',
];
}
} catch {
return ['America/New_York', 'Europe/London', 'Asia/Shanghai', 'Asia/Tokyo', 'Australia/Sydney'];
}
}
function getTimezoneOffset(timezone: string): string {
try {
const now = new Date();
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
const targetTime = new Date(utc.toLocaleString('en-US', { timeZone: timezone }));
const offset = (targetTime.getTime() - utc.getTime()) / (1000 * 60 * 60);
const sign = offset >= 0 ? '+' : '-';
const hours = Math.floor(Math.abs(offset));
const minutes = Math.floor((Math.abs(offset) - hours) * 60);
return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
return `UTC${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
} catch {
return '+00:00';
return 'UTC+00:00';
}
}
export default function TimezoneSwitch() {
const locale = useLocale();
const [timezone, setTimezone] = useState<string>('UTC');
const [open, setOpen] = useState(false);
const timezoneOptions = useMemo(() => getAllTimezones(), []);
const timezoneOptions = useMemo(() => getAllTimezones(locale), [locale]);
useEffect(() => {
const savedTimezone = localStorage.getItem('timezone');
if (savedTimezone) {
setTimezone(savedTimezone);
} else {
try {
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
setTimezone(browserTimezone);
localStorage.setItem('timezone', browserTimezone);
} catch {
setTimezone('UTC');
}
}
}, []);
const handleTimezoneChange = (newTimezone: string) => {
setTimezone(newTimezone);
@ -86,6 +163,9 @@ export default function TimezoneSwitch() {
}),
);
};
const serverTimezones = timezoneOptions.filter(
(option) => getServerTimezones().includes(option.value) && option.value !== timezone,
);
return (
<Popover open={open} onOpenChange={setOpen}>
@ -98,29 +178,101 @@ export default function TimezoneSwitch() {
<Command>
<CommandInput placeholder='Search...' />
<CommandList>
{timezoneOptions.map((option) => (
<CommandItem
key={option.value}
value={`${option.label} ${option.value}`}
onSelect={() => handleTimezoneChange(option.value)}
>
<div className='flex w-full items-center gap-3'>
<div className='flex flex-1 flex-col'>
<span className='font-medium'>{option.label}</span>
<span className='text-muted-foreground text-xs'>
{option.value} {option.offset}
</span>
</div>
<Icon
icon='uil:check'
className={cn(
'h-4 w-4',
timezone === option.value ? 'opacity-100' : 'opacity-0',
)}
/>
</div>
</CommandItem>
))}
<CommandGroup heading='Current'>
{timezoneOptions
.filter((option) => option.value === timezone)
.map((option) => (
<CommandItem
key={option.value}
value={`${option.label} ${option.value}`}
onSelect={() => handleTimezoneChange(option.value)}
className='bg-primary/10'
>
<div className='flex w-full items-center gap-3'>
<div className='flex flex-1 flex-col'>
<span className='font-medium'>{option.value}</span>
<span className='text-muted-foreground text-xs'>
{option.timezone} {getCurrentTime(option.value)}
</span>
</div>
<Icon icon='uil:check' className='h-4 w-4 opacity-100' />
</div>
</CommandItem>
))}
</CommandGroup>
{serverTimezones.length > 0 && (
<CommandGroup heading='Server'>
{serverTimezones.map((option) => (
<CommandItem
key={option.value}
value={`${option.label} ${option.value}`}
onSelect={() => handleTimezoneChange(option.value)}
>
<div className='flex w-full items-center gap-3'>
<div className='flex flex-1 flex-col'>
<span className='font-medium'>{option.value}</span>
<span className='text-muted-foreground text-xs'>
{option.timezone} {getCurrentTime(option.value)}
</span>
</div>
<Icon icon='uil:check' className='h-4 w-4 opacity-0' />
</div>
</CommandItem>
))}
</CommandGroup>
)}
<CommandGroup heading='Recommended'>
{timezoneOptions
.filter(
(option) =>
getRecommendedTimezones().includes(option.value) && option.value !== timezone,
)
.map((option) => (
<CommandItem
key={option.value}
value={`${option.label} ${option.value}`}
onSelect={() => handleTimezoneChange(option.value)}
>
<div className='flex w-full items-center gap-3'>
<div className='flex flex-1 flex-col'>
<span className='font-medium'>{option.value}</span>
<span className='text-muted-foreground text-xs'>
{option.timezone} {getCurrentTime(option.value)}
</span>
</div>
<Icon icon='uil:check' className='h-4 w-4 opacity-0' />
</div>
</CommandItem>
))}
</CommandGroup>
<CommandGroup heading='All'>
{timezoneOptions
.filter(
(option) =>
!getServerTimezones().includes(option.value) &&
!getRecommendedTimezones().includes(option.value) &&
option.value !== timezone,
)
.map((option) => (
<CommandItem
key={option.value}
value={`${option.label} ${option.value}`}
onSelect={() => handleTimezoneChange(option.value)}
>
<div className='flex w-full items-center gap-3'>
<div className='flex flex-1 flex-col'>
<span className='font-medium'>{option.value}</span>
<span className='text-muted-foreground text-xs'>
{option.timezone} {getCurrentTime(option.value)}
</span>
</div>
<Icon icon='uil:check' className='h-4 w-4 opacity-0' />
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>

View File

@ -72,9 +72,12 @@
"323": "Platba",
"324": "Vrácení",
"325": "Odměna",
"326": "Úprava správce",
"331": "Nákup",
"332": "Obnovení",
"333": "Vrácení",
"334": "Výběr",
"335": "Úprava správce",
"341": "Zvýšení",
"342": "Snížení"
},

View File

@ -72,9 +72,12 @@
"323": "Zahlung",
"324": "Rückerstattung",
"325": "Belohnung",
"326": "Admin Anpassung",
"331": "Kauf",
"332": "Verlängerung",
"333": "Rückerstattung",
"334": "Abheben",
"335": "Admin Anpassung",
"341": "Erhöhen",
"342": "Reduzieren"
},

View File

@ -72,9 +72,12 @@
"323": "Payment",
"324": "Refund",
"325": "Reward",
"326": "Admin Adjust",
"331": "Purchase",
"332": "Renewal",
"333": "Refund",
"334": "Withdraw",
"335": "Admin Adjust",
"341": "Increase",
"342": "Reduce"
},

View File

@ -72,9 +72,12 @@
"323": "Pago",
"324": "Reembolso",
"325": "Recompensa",
"326": "Ajuste de Administrador",
"331": "Compra",
"332": "Renovación",
"333": "Reembolso",
"334": "Retiro",
"335": "Ajuste de Administrador",
"341": "Aumentar",
"342": "Reducir"
},

View File

@ -72,9 +72,12 @@
"323": "Pago",
"324": "Reembolso",
"325": "Recompensa",
"326": "Ajuste del administrador",
"331": "Compra",
"332": "Renovación",
"333": "Reembolso",
"334": "Retiro",
"335": "Ajuste del administrador",
"341": "Aumento",
"342": "Reducción"
},

View File

@ -72,9 +72,12 @@
"323": "پرداخت",
"324": "بازگشت",
"325": "پاداش",
"326": "تنظیم مدیر",
"331": "خرید",
"332": "تمدید",
"333": "بازگشت",
"334": "برداشت",
"335": "تنظیم مدیر",
"341": "افزایش",
"342": "کاهش"
},

View File

@ -72,9 +72,12 @@
"323": "Maksu",
"324": "Hyvitys",
"325": "Palkinto",
"326": "Ylläpitäjän säätö",
"331": "Osto",
"332": "Uusiminen",
"333": "Hyvitys",
"334": "Nosto",
"335": "Ylläpitäjän säätö",
"341": "Lisäys",
"342": "Vähennys"
},

View File

@ -72,9 +72,12 @@
"323": "Paiement",
"324": "Remboursement",
"325": "Récompense",
"326": "Ajustement Administrateur",
"331": "Achat",
"332": "Renouvellement",
"333": "Remboursement",
"334": "Retrait",
"335": "Ajustement Administrateur",
"341": "Augmenter",
"342": "Réduire"
},

View File

@ -72,9 +72,12 @@
"323": "भुगतान",
"324": "वापसी",
"325": "इनाम",
"326": "व्यवस्थापक समायोजन",
"331": "खरीद",
"332": "नवीनीकरण",
"333": "वापसी",
"334": "निकासी",
"335": "व्यवस्थापक समायोजन",
"341": "वृद्धि",
"342": "कमी"
},

View File

@ -72,9 +72,12 @@
"323": "Fizetés",
"324": "Visszatérítés",
"325": "Jutalom",
"326": "Adminisztrátori kiigazítás",
"331": "Vásárlás",
"332": "Megújítás",
"333": "Visszatérítés",
"334": "Kivét",
"335": "Adminisztrátori kiigazítás",
"341": "Növelés",
"342": "Csökkentés"
},

View File

@ -72,9 +72,12 @@
"323": "支払い",
"324": "返金",
"325": "報酬",
"326": "管理者調整",
"331": "購入",
"332": "更新",
"333": "返金",
"334": "引き出し",
"335": "管理者調整",
"341": "増加",
"342": "減少"
},

View File

@ -72,9 +72,12 @@
"323": "결제",
"324": "환불",
"325": "보상",
"326": "관리자 조정",
"331": "구매",
"332": "갱신",
"333": "환불",
"334": "출금",
"335": "관리자 조정",
"341": "증가",
"342": "감소"
},

View File

@ -72,9 +72,12 @@
"323": "Betaling",
"324": "Refusjon",
"325": "Belønning",
"326": "Administrator justering",
"331": "Kjøp",
"332": "Fornyelse",
"333": "Refusjon",
"334": "Uttak",
"335": "Administrator justering",
"341": "Økning",
"342": "Reduksjon"
},

View File

@ -72,9 +72,12 @@
"323": "Płatność",
"324": "Zwrot",
"325": "Nagroda",
"326": "Korekta Administratora",
"331": "Zakup",
"332": "Odnowienie",
"333": "Zwrot",
"334": "Wypłata",
"335": "Korekta Administratora",
"341": "Zwiększenie",
"342": "Zmniejszenie"
},

View File

@ -72,9 +72,12 @@
"323": "Pagamento",
"324": "Reembolso",
"325": "Recompensa",
"326": "Ajuste do Administrador",
"331": "Compra",
"332": "Renovação",
"333": "Reembolso",
"334": "Retirada",
"335": "Ajuste do Administrador",
"341": "Aumentar",
"342": "Reduzir"
},

View File

@ -72,9 +72,12 @@
"323": "Plată",
"324": "Rambursare",
"325": "Recompensă",
"326": "Ajustare Administrator",
"331": "Achiziție",
"332": "Reînnoire",
"333": "Rambursare",
"334": "Retragere",
"335": "Ajustare Administrator",
"341": "Creștere",
"342": "Scădere"
},

View File

@ -72,9 +72,12 @@
"323": "Платеж",
"324": "Возврат",
"325": "Награда",
"326": "Корректировка администратора",
"331": "Покупка",
"332": "Продление",
"333": "Возврат",
"334": "Вывод",
"335": "Корректировка администратора",
"341": "Увеличение",
"342": "Уменьшение"
},

View File

@ -72,9 +72,12 @@
"323": "การชำระเงิน",
"324": "คืนเงิน",
"325": "รางวัล",
"326": "การปรับโดยผู้ดูแล",
"331": "ซื้อ",
"332": "ต่ออายุ",
"333": "คืนเงิน",
"334": "ถอนเงิน",
"335": "การปรับโดยผู้ดูแล",
"341": "เพิ่ม",
"342": "ลด"
},

View File

@ -72,9 +72,12 @@
"323": "Ödeme",
"324": "İade",
"325": "Ödül",
"326": "Yönetici Ayarı",
"331": "Alım",
"332": "Yenileme",
"333": "İade",
"334": "Çekim",
"335": "Yönetici Ayarı",
"341": "Artış",
"342": "Azalış"
},

View File

@ -72,9 +72,12 @@
"323": "Платіж",
"324": "Повернення",
"325": "Нагорода",
"326": "Коригування адміністратора",
"331": "Покупка",
"332": "Поновлення",
"333": "Повернення",
"334": "Виведення",
"335": "Коригування адміністратора",
"341": "Збільшення",
"342": "Зменшення"
},

View File

@ -72,9 +72,12 @@
"323": "Thanh toán",
"324": "Hoàn tiền",
"325": "Phần thưởng",
"326": "Điều chỉnh Quản trị viên",
"331": "Mua hàng",
"332": "Gia hạn",
"333": "Hoàn tiền",
"334": "Rút tiền",
"335": "Điều chỉnh Quản trị viên",
"341": "Tăng",
"342": "Giảm"
},

View File

@ -72,9 +72,12 @@
"323": "支付",
"324": "退款",
"325": "奖励",
"326": "管理员调整",
"331": "购买",
"332": "续费",
"333": "退款",
"334": "提现",
"335": "管理员调整",
"341": "增加",
"342": "减少"
},

View File

@ -72,9 +72,12 @@
"323": "付款",
"324": "退款",
"325": "獎勵",
"326": "管理員調整",
"331": "購買",
"332": "續費",
"333": "退款",
"334": "提現",
"335": "管理員調整",
"341": "增加",
"342": "減少"
},

View File

@ -1,5 +1,5 @@
// @ts-ignore
// API 更新时间:
// API 唯一标识:
import * as ads from './ads';

View File

@ -122,7 +122,7 @@ declare namespace API {
type: number;
user_id: number;
amount: number;
order_id?: number;
order_no?: string;
balance: number;
timestamp: number;
};

View File

@ -1,5 +1,5 @@
// @ts-ignore
// API 更新时间:
// API 唯一标识:
import * as auth from './auth';

View File

@ -128,7 +128,7 @@ declare namespace API {
type: number;
user_id: number;
amount: number;
order_id?: number;
order_no?: string;
balance: number;
timestamp: number;
};

View File

@ -1,5 +1,5 @@
// @ts-ignore
// API 更新时间:
// API 唯一标识:
import * as auth from './auth';

View File

@ -128,7 +128,7 @@ declare namespace API {
type: number;
user_id: number;
amount: number;
order_id?: number;
order_no?: string;
balance: number;
timestamp: number;
};

View File

@ -1,5 +1,5 @@
// @ts-ignore
// API 更新时间:
// API 唯一标识:
import * as announcement from './announcement';

View File

@ -122,7 +122,7 @@ declare namespace API {
type: number;
user_id: number;
amount: number;
order_id?: number;
order_no?: string;
balance: number;
timestamp: number;
};