From 98fac2c8671bb7f6e1b574c39ff87c38ca5d3f34 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 4 Jul 2026 13:52:43 +0330 Subject: [PATCH] Use timezone dropdown for company form --- .../forms/companies/companyFormSections.tsx | 178 +++++++++++++++++- 1 file changed, 171 insertions(+), 7 deletions(-) diff --git a/src/components/forms/companies/companyFormSections.tsx b/src/components/forms/companies/companyFormSections.tsx index 38f464a..d7db6ce 100644 --- a/src/components/forms/companies/companyFormSections.tsx +++ b/src/components/forms/companies/companyFormSections.tsx @@ -79,6 +79,161 @@ const currencyItems: ILabelValue[] = [ { label: "BRL", value: "BRL" }, ]; +const allTimezones: Record = { + "Pacific/Midway": "Midway Island, Samoa", + "Pacific/Honolulu": "Hawaii", + "America/Juneau": "Alaska", + "America/Boise": "Mountain Time", + "America/Dawson": "Dawson, Yukon", + "America/Chihuahua": "Chihuahua, La Paz, Mazatlan", + "America/Phoenix": "Arizona", + "America/Los_Angeles": "Pacific Time", + "America/Chicago": "Central Time", + "America/Regina": "Saskatchewan", + "America/Mexico_City": "Guadalajara, Mexico City, Monterrey", + "America/Belize": "Central America", + "America/Detroit": "Eastern Time", + "America/Bogota": "Bogota, Lima, Quito", + "America/Caracas": "Caracas, La Paz", + "America/Santiago": "Santiago", + "America/St_Johns": "Newfoundland and Labrador", + "America/Sao_Paulo": "Brasilia", + "America/Tijuana": "Tijuana", + "America/Montevideo": "Montevideo", + "America/Argentina/Buenos_Aires": "Buenos Aires, Georgetown", + "America/Godthab": "Greenland", + "Atlantic/Azores": "Azores", + "Atlantic/Cape_Verde": "Cape Verde Islands", + GMT: "UTC", + "Europe/London": "Edinburgh, London", + "Europe/Dublin": "Dublin", + "Europe/Lisbon": "Lisbon", + "Africa/Casablanca": "Casablanca, Monrovia", + "Atlantic/Canary": "Canary Islands", + "Europe/Belgrade": "Belgrade, Bratislava, Budapest, Ljubljana, Prague", + "Europe/Sarajevo": "Sarajevo, Skopje, Warsaw, Zagreb", + "Europe/Brussels": "Brussels, Copenhagen, Madrid, Paris", + "Europe/Amsterdam": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna", + "Africa/Algiers": "West Central Africa", + "Europe/Bucharest": "Bucharest", + "Africa/Cairo": "Cairo", + "Europe/Helsinki": "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius", + "Europe/Athens": "Athens", + "Asia/Jerusalem": "Jerusalem", + "Africa/Harare": "Harare, Pretoria", + "Europe/Moscow": "Istanbul, Minsk, Moscow, St. Petersburg, Volgograd", + "Asia/Kuwait": "Kuwait, Riyadh", + "Africa/Nairobi": "Nairobi", + "Asia/Baghdad": "Baghdad", + "Asia/Tehran": "Tehran", + "Asia/Dubai": "Abu Dhabi, Muscat", + "Asia/Baku": "Baku, Tbilisi, Yerevan", + "Asia/Kabul": "Kabul", + "Asia/Yekaterinburg": "Ekaterinburg", + "Asia/Karachi": "Islamabad, Karachi, Tashkent", + "Asia/Kolkata": "Chennai, Kolkata, Mumbai, New Delhi", + "Asia/Kathmandu": "Kathmandu", + "Asia/Dhaka": "Astana, Dhaka", + "Asia/Colombo": "Sri Jayawardenepura", + "Asia/Almaty": "Almaty, Novosibirsk", + "Asia/Rangoon": "Yangon Rangoon", + "Asia/Bangkok": "Bangkok, Hanoi, Jakarta", + "Asia/Krasnoyarsk": "Krasnoyarsk", + "Asia/Shanghai": "Beijing, Chongqing, Hong Kong SAR, Urumqi", + "Asia/Kuala_Lumpur": "Kuala Lumpur, Singapore", + "Asia/Taipei": "Taipei", + "Australia/Perth": "Perth", + "Asia/Irkutsk": "Irkutsk, Ulaanbaatar", + "Asia/Seoul": "Seoul", + "Asia/Tokyo": "Osaka, Sapporo, Tokyo", + "Asia/Yakutsk": "Yakutsk", + "Australia/Darwin": "Darwin", + "Australia/Adelaide": "Adelaide", + "Australia/Sydney": "Canberra, Melbourne, Sydney", + "Australia/Brisbane": "Brisbane", + "Australia/Hobart": "Hobart", + "Asia/Vladivostok": "Vladivostok", + "Pacific/Guam": "Guam, Port Moresby", + "Asia/Magadan": "Magadan, Solomon Islands, New Caledonia", + "Asia/Kamchatka": "Kamchatka, Marshall Islands", + "Pacific/Fiji": "Fiji Islands", + "Pacific/Auckland": "Auckland, Wellington", + "Pacific/Tongatapu": "Nuku'alofa", +}; + +const getTimezoneOffsetMinutes = (timezone: string, date = new Date()) => { + try { + const formatter = new Intl.DateTimeFormat("en", { + timeZone: timezone, + timeZoneName: "shortOffset", + }); + const offset = + formatter + .formatToParts(date) + .find((part) => part.type === "timeZoneName")?.value ?? ""; + const label = offset.replace(/^GMT/, "") || "+0"; + const match = label.match(/^([+-])(\d{1,2})(?::(\d{2}))?$/); + + if (!match) return 0; + + const sign = match[1] === "-" ? -1 : 1; + const hours = Number(match[2]); + const minutes = Number(match[3] ?? 0); + + return sign * (hours * 60 + minutes); + } catch { + return 0; + } +}; + +const formatTimezoneOffset = (offsetMinutes: number) => { + const sign = offsetMinutes < 0 ? "-" : "+"; + const absoluteMinutes = Math.abs(offsetMinutes); + const hours = Math.trunc(absoluteMinutes / 60); + const minutes = absoluteMinutes % 60; + + return `${sign}${hours}:${minutes.toString().padStart(2, "0")}`; +}; + +const hasTimezoneDst = (timezone: string) => { + const januaryOffset = getTimezoneOffsetMinutes( + timezone, + new Date(new Date().getFullYear(), 0, 1), + ); + const julyOffset = getTimezoneOffsetMinutes( + timezone, + new Date(new Date().getFullYear(), 6, 1), + ); + + return januaryOffset !== julyOffset; +}; + +const timezoneItems: ILabelValue[] = Object.entries(allTimezones) + .map(([timezone, name]) => { + const offsetMinutes = getTimezoneOffsetMinutes(timezone); + + return { + timezone, + name, + hasDst: hasTimezoneDst(timezone), + offsetMinutes, + }; + }) + .sort((a, b) => a.offsetMinutes - b.offsetMinutes) + .filter((item, index, items) => { + return ( + items.findIndex( + (timezone) => + timezone.offsetMinutes === item.offsetMinutes && + timezone.hasDst === item.hasDst, + ) === index + ); + }) + .map(({ timezone, name, offsetMinutes }) => ({ + label: `(GMT${formatTimezoneOffset(offsetMinutes)}) ${name}`, + value: timezone, + })); + export const createCompanyFormSections = ({ editMode, id, @@ -315,15 +470,24 @@ export const createCompanyFormSections = ({ ), }, { - kind: "input", + kind: "custom", name: "timezone", - label: "Timezone", - type: "text", className: "sm:col-span-2", - rules: { - minLength: { value: 3, message: FormErrorMessages.minLength(3) }, - maxLength: { value: 50, message: FormErrorMessages.maxLength(50) }, - }, + render: ({ register, errors }) => ( + + {...register("timezone")} + errors={errors} + name="timezone" + label="Timezone" + items={timezoneItems} + value={watch("timezone") ?? ""} + placeholder="Select timezone" + searchable + searchPlaceholder="Search timezone..." + clearable + onClear={() => setValue("timezone", "")} + /> + ), }, ], },