import { GlobeIcon, MoneyIcon } from "@/assets/icons"; import TagInput from "@/components/FormElements/InputGroup/tag-input"; import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area"; import MultiSelect from "@/components/FormElements/MultiSelect"; import { Select } from "@/components/FormElements/select"; import type { DynamicFormSection } from "@/components/forms/DynamicFormBuilder"; import { REGEX } from "@/constants/regex"; import { FormErrorMessages } from "@/constants/Texts"; import type { ICreateCompanyCredentials } from "@/lib/api"; import type { ILabelValue } from "@/types/shared"; import type { UseFormSetValue, UseFormWatch } from "react-hook-form"; import { v4 as uuidv4 } from "uuid"; import CompanyDocuments from "./documents/CompanyDocuments"; import CompanyLinks from "./links/CompanyLinks"; const companyInformationSectionId = uuidv4(); const businessInformationSectionId = uuidv4(); const settingsSectionId = uuidv4(); const addressSectionId = uuidv4(); const tagsSectionId = uuidv4(); const documentsSectionId = uuidv4(); const linksSectionId = uuidv4(); interface CompanyFormSectionsOptions { editMode?: boolean; id?: string; categoryItems: ILabelValue[]; watch: UseFormWatch; setValue: UseFormSetValue; persistDocumentFileIds?: (ids: string[]) => Promise; } const optionalNumber = (value: unknown) => { if (value === "" || value === null || value === undefined) return undefined; return Number(value); }; const optionalNumberRange = ({ min, max, integer = false, }: { min: number; max: number; integer?: boolean; }) => { return (value: unknown) => { const numberValue = optionalNumber(value); if (numberValue === undefined) return true; if (!Number.isFinite(numberValue)) return "Enter a valid number"; if (integer && !Number.isInteger(numberValue)) { return "This field must be a whole number"; } if (numberValue < min) return FormErrorMessages.min(min); if (numberValue > max) return FormErrorMessages.max(max); return true; }; }; const languageItems: ILabelValue[] = [ { value: "en", label: "EN" }, { value: "ar", label: "AR" }, { value: "fr", label: "FR" }, { value: "es", label: "ES" }, { value: "de", label: "DE" }, { value: "zh", label: "ZH" }, { value: "jEna", label: "jEna" }, { value: "ko", label: "KO" }, ]; const currencyItems: ILabelValue[] = [ { label: "USD", value: "USD" }, { label: "EUR", value: "EUR" }, { label: "GBP", value: "GBP" }, { label: "JPY", value: "JPY" }, { label: "CAD", value: "CAD" }, { label: "AUD", value: "AUD" }, { label: "CHF", value: "CHF" }, { label: "CNY", value: "CNY" }, { label: "INR", value: "INR" }, { 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, categoryItems, watch, setValue, persistDocumentFileIds, }: CompanyFormSectionsOptions): DynamicFormSection[] => [ { id: companyInformationSectionId, title: "Company information", rootClassName: "lg:col-span-2", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", fields: [ { kind: "input", name: "name", label: "Name", type: "text", placeholder: "Legal name as registered", required: true, className: "sm:col-span-2", rules: { required: { message: FormErrorMessages.required, value: true }, minLength: { value: 2, message: FormErrorMessages.minLength(2) }, maxLength: { value: 200, message: FormErrorMessages.maxLength(200) }, }, }, { kind: "input", name: "email", label: "Email", type: "email", placeholder: "name@company.com", required: true, rules: { required: { message: FormErrorMessages.required, value: true }, pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: FormErrorMessages.invalidEmail, }, }, }, { kind: "input", name: "phone", label: "Phone", type: "tel", placeholder: "+971 50 123 4567", required: true, rules: { required: { message: FormErrorMessages.required, value: true }, minLength: { value: 10, message: FormErrorMessages.minLength(10) }, maxLength: { value: 20, message: FormErrorMessages.maxLength(20) }, pattern: { value: REGEX.PHONE, message: FormErrorMessages.invalidPattern, }, }, }, { kind: "custom", name: "type", render: ({ register, errors }) => ( {...register("type", { required: { message: FormErrorMessages.required, value: true }, })} errors={errors} name="type" label="Company type" required items={[ { value: "private", label: "Private" }, { value: "public", label: "Public" }, { value: "government", label: "Government" }, { value: "ngo", label: "NGO" }, { value: "startup", label: "Startup" }, ]} defaultValue="Private" prefixIcon={} /> ), }, { kind: "input", name: "industry", label: "Industry", type: "text", placeholder: "e.g. construction, software, logistics", rules: { minLength: { value: 2, message: FormErrorMessages.minLength(2) }, maxLength: { value: 100, message: FormErrorMessages.maxLength(100) }, }, }, { kind: "input", name: "registration_number", label: "Registration number", type: "text", placeholder: "Registry or license number", rules: { minLength: { value: 5, message: FormErrorMessages.minLength(5) }, maxLength: { value: 50, message: FormErrorMessages.maxLength(50) }, }, }, { kind: "input", name: "tax_id", label: "Tax id", type: "text", placeholder: "National tax identification number", rules: { minLength: { value: 5, message: FormErrorMessages.minLength(5) }, maxLength: { value: 50, message: FormErrorMessages.maxLength(50) }, }, }, { kind: "input", name: "website", label: "Website", type: "text", placeholder: "https://www.example.com", required: true, className: "sm:col-span-2", rules: { required: { message: FormErrorMessages.required, value: true }, minLength: { value: 2, message: FormErrorMessages.minLength(2) }, maxLength: { value: 100, message: FormErrorMessages.maxLength(100) }, pattern: { value: REGEX.URL, message: FormErrorMessages.invalidPattern, }, }, }, { kind: "custom", name: "description", className: "sm:col-span-2", render: ({ register, errors }) => ( label="Description" {...register("description", { minLength: { value: 10, message: FormErrorMessages.minLength(10), }, maxLength: { value: 1000, message: FormErrorMessages.maxLength(1000), }, })} errors={errors} name="description" /> ), }, ], }, { id: businessInformationSectionId, title: "Business information", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", fields: [ { kind: "input", name: "employee_count", label: "Employee count", type: "number", rules: { setValueAs: optionalNumber, validate: optionalNumberRange({ min: 1, max: 100000, integer: true }), }, }, { kind: "input", name: "annual_revenue", label: "Annual revenue", type: "number", rules: { setValueAs: optionalNumber, validate: optionalNumberRange({ min: 0, max: 999999999999 }), }, }, { kind: "input", name: "founded_year", label: "Founded year", type: "number", className: "sm:col-span-2", rules: { setValueAs: optionalNumber, validate: optionalNumberRange({ min: 1800, max: 2100, integer: true, }), }, }, ], }, { id: settingsSectionId, title: "Settings", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", fields: [ { kind: "custom", name: "language", render: ({ register, errors }) => ( {...register("language")} errors={errors} name="language" label="Language" items={languageItems} defaultValue="en" prefixIcon={} /> ), }, { kind: "custom", name: "currency", render: ({ register, errors }) => ( {...register("currency")} errors={errors} name="currency" label="Currency" items={currencyItems} defaultValue="USD" prefixIcon={} /> ), }, { kind: "custom", name: "timezone", className: "sm:col-span-2", 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", "")} /> ), }, ], }, { id: addressSectionId, title: "Address", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", fields: [ { kind: "input", name: "address.street", label: "Street", type: "text", placeholder: "Building, street, suite or unit", className: "sm:col-span-2", rules: { minLength: { value: 5, message: FormErrorMessages.minLength(5) }, maxLength: { value: 200, message: FormErrorMessages.maxLength(200) }, }, }, { kind: "input", name: "address.city", label: "City", type: "text", placeholder: "City or town", rules: { minLength: { value: 2, message: FormErrorMessages.minLength(2) }, maxLength: { value: 100, message: FormErrorMessages.maxLength(100) }, }, }, { kind: "input", name: "address.state", label: "State", type: "text", placeholder: "State, province, or region", rules: { minLength: { value: 2, message: FormErrorMessages.minLength(2) }, maxLength: { value: 100, message: FormErrorMessages.maxLength(100) }, }, }, { kind: "input", name: "address.postal_code", label: "Postal code", type: "text", placeholder: "Postal or ZIP code", rules: { minLength: { value: 3, message: FormErrorMessages.minLength(3) }, maxLength: { value: 20, message: FormErrorMessages.maxLength(20) }, }, }, { kind: "input", name: "address.country", label: "Country", type: "text", placeholder: "Full country name", rules: { minLength: { value: 2, message: FormErrorMessages.minLength(2) }, maxLength: { value: 100, message: FormErrorMessages.maxLength(100) }, }, }, ], }, { id: tagsSectionId, title: "Tags", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", fields: [ { kind: "custom", name: "tags.keywords", render: ({ register }) => ( {...register("tags.keywords")} name="tags.keywords" label="Keywords" placeholder="" register={register} setValue={setValue} watch={watch} /> ), }, { kind: "custom", name: "tags.categories", render: ({ register }) => ( {...register("tags.categories")} value={watch("tags.categories")} name="tags.categories" label="Categories" items={categoryItems} placeholder="" /> ), }, { kind: "custom", name: "tags.cpv_codes", render: ({ register }) => ( {...register("tags.cpv_codes")} label="CPV codes" name="tags.cpv_codes" shouldAddWithSpace watch={watch} setValue={setValue} placeholder="" /> ), }, { kind: "custom", name: "tags.certifications", render: ({ register }) => ( {...register("tags.certifications")} label="Certifications" name="tags.certifications" watch={watch} setValue={setValue} placeholder="" /> ), }, { kind: "custom", name: "tags.specializations", className: "sm:col-span-2", render: ({ register, error }) => ( <> {...register("tags.specializations")} label="Specializations" name="tags.specializations" watch={watch} setValue={setValue} placeholder="" /> {error ? (

{error}

) : null} ), }, ], }, { id: documentsSectionId, title: "Documents", rootClassName: "lg:col-span-2", className: "md:grid-cols-1", fields: [ { kind: "custom", name: "document_file_ids", render: () => ( setValue("document_file_ids", ids, { shouldDirty: true, shouldTouch: true, shouldValidate: true, }) } onRemovePersist={persistDocumentFileIds} /> ), }, ], }, { id: linksSectionId, title: "Links", rootClassName: "lg:col-span-2", className: "md:grid-cols-1", fields: [ { kind: "custom", name: "links", render: ({ control, errors, register }) => ( ), }, ], }, ];