🐛 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} />, cell: ({ row }) => <Display type='currency' value={row.original.amount} />,
}, },
{ {
accessorKey: 'order_id', accessorKey: 'order_no',
header: t('column.orderId'), header: t('column.orderNo'),
cell: ({ row }) => <OrderLink orderId={row.original.order_id} />, cell: ({ row }) => <OrderLink orderId={row.original.order_no} />,
}, },
{ {
accessorKey: 'balance', accessorKey: 'balance',

View File

@ -1,79 +1,156 @@
'use client'; 'use client';
import { Button } from '@workspace/ui/components/button'; 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 { Popover, PopoverContent, PopoverTrigger } from '@workspace/ui/components/popover';
import { Icon } from '@workspace/ui/custom-components/icon'; import { Icon } from '@workspace/ui/custom-components/icon';
import { cn } from '@workspace/ui/lib/utils'; import { useLocale } from 'next-intl';
import { useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
interface TimezoneOption { interface TimezoneOption {
value: string; value: string;
label: 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 { try {
const timeZones = Intl.supportedValuesOf('timeZone'); const timeZones = Intl.supportedValuesOf('timeZone');
return [ const processed = timeZones
{ .map((tz) => {
value: 'UTC', try {
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})`;
}
return { return {
value: tz, value: tz,
label: label, label: tz,
offset: getTimezoneOffset(tz), timezone: getTimezoneOffset(tz),
}; };
}) } catch {
.sort((a, b) => a.label.localeCompare(b.label)), 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 { } catch {
return [ return [
{ {
value: 'UTC', value: 'UTC',
label: '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 { function getTimezoneOffset(timezone: string): string {
try { try {
const now = new Date(); const now = new Date();
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000); const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
const targetTime = new Date(utc.toLocaleString('en-US', { timeZone: timezone })); const targetTime = new Date(utc.toLocaleString('en-US', { timeZone: timezone }));
const offset = (targetTime.getTime() - utc.getTime()) / (1000 * 60 * 60); const offset = (targetTime.getTime() - utc.getTime()) / (1000 * 60 * 60);
const sign = offset >= 0 ? '+' : '-'; const sign = offset >= 0 ? '+' : '-';
const hours = Math.floor(Math.abs(offset)); const hours = Math.floor(Math.abs(offset));
const minutes = Math.floor((Math.abs(offset) - hours) * 60); 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 { } catch {
return '+00:00'; return 'UTC+00:00';
} }
} }
export default function TimezoneSwitch() { export default function TimezoneSwitch() {
const locale = useLocale();
const [timezone, setTimezone] = useState<string>('UTC'); const [timezone, setTimezone] = useState<string>('UTC');
const [open, setOpen] = useState(false); 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) => { const handleTimezoneChange = (newTimezone: string) => {
setTimezone(newTimezone); setTimezone(newTimezone);
@ -86,6 +163,9 @@ export default function TimezoneSwitch() {
}), }),
); );
}; };
const serverTimezones = timezoneOptions.filter(
(option) => getServerTimezones().includes(option.value) && option.value !== timezone,
);
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
@ -98,29 +178,101 @@ export default function TimezoneSwitch() {
<Command> <Command>
<CommandInput placeholder='Search...' /> <CommandInput placeholder='Search...' />
<CommandList> <CommandList>
{timezoneOptions.map((option) => ( <CommandGroup heading='Current'>
<CommandItem {timezoneOptions
key={option.value} .filter((option) => option.value === timezone)
value={`${option.label} ${option.value}`} .map((option) => (
onSelect={() => handleTimezoneChange(option.value)} <CommandItem
> key={option.value}
<div className='flex w-full items-center gap-3'> value={`${option.label} ${option.value}`}
<div className='flex flex-1 flex-col'> onSelect={() => handleTimezoneChange(option.value)}
<span className='font-medium'>{option.label}</span> className='bg-primary/10'
<span className='text-muted-foreground text-xs'> >
{option.value} {option.offset} <div className='flex w-full items-center gap-3'>
</span> <div className='flex flex-1 flex-col'>
</div> <span className='font-medium'>{option.value}</span>
<Icon <span className='text-muted-foreground text-xs'>
icon='uil:check' {option.timezone} {getCurrentTime(option.value)}
className={cn( </span>
'h-4 w-4', </div>
timezone === option.value ? 'opacity-100' : 'opacity-0', <Icon icon='uil:check' className='h-4 w-4 opacity-100' />
)} </div>
/> </CommandItem>
</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> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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