From 4d2cb1774778b3aa298c30d735ed68488ec18cf6 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 14 Jan 2025 22:07:55 +0530 Subject: [PATCH 01/10] Switch to `libphonenumber-js`; fixes #9966 --- package-lock.json | 7 + package.json | 1 + public/locale/en.json | 1 + src/Utils/utils.ts | 146 --------- src/common/constants.tsx | 123 -------- src/common/static/countryPhoneAndFlags.json | 251 --------------- src/common/validation.tsx | 5 - .../Common/SearchByMultipleFields.tsx | 17 +- .../Facility/CreateFacilityForm.tsx | 26 +- src/components/Facility/FacilityCreate.tsx | 48 +-- src/components/Form/FieldValidators.tsx | 52 ---- src/components/Form/FormFields/FormField.tsx | 3 +- .../Form/FormFields/PhoneNumberFormField.tsx | 292 ------------------ src/components/Form/FormFields/Utils.ts | 4 +- src/components/Form/Utils.ts | 6 +- src/components/Patient/PatientIndex.tsx | 4 +- .../Patient/PatientRegistration.tsx | 12 +- src/components/Resource/ResourceCreate.tsx | 35 +-- src/components/Users/UserListAndCard.tsx | 10 +- src/components/ui/input-phone.tsx | 38 +++ .../PublicAppointments/auth/PatientLogin.tsx | 37 +-- 21 files changed, 119 insertions(+), 999 deletions(-) delete mode 100644 src/common/static/countryPhoneAndFlags.json delete mode 100644 src/components/Form/FieldValidators.tsx delete mode 100644 src/components/Form/FormFields/PhoneNumberFormField.tsx create mode 100644 src/components/ui/input-phone.tsx diff --git a/package-lock.json b/package-lock.json index ab497f086a9..61d79fa3b23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", "input-otp": "^1.4.2", + "libphonenumber-js": "^1.11.17", "lodash-es": "^4.17.21", "lucide-react": "^0.471.0", "markdown-it": "^14.1.0", @@ -12955,6 +12956,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.17", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.17.tgz", + "integrity": "sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==", + "license": "MIT" + }, "node_modules/lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", diff --git a/package.json b/package.json index de3f587bcfd..9ab5411c9a0 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", "input-otp": "^1.4.2", + "libphonenumber-js": "^1.11.17", "lodash-es": "^4.17.21", "lucide-react": "^0.471.0", "markdown-it": "^14.1.0", diff --git a/public/locale/en.json b/public/locale/en.json index 82a217ccffc..e3f832ac08d 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1555,6 +1555,7 @@ "phone_number_min_error": "Phone number must be at least 10 characters long", "phone_number_must_be_10_digits": "Phone number must be a 10-digit mobile number", "phone_number_not_found": "Phone number not found", + "phone_number_placeholder": "+91xxxxxxxxxx", "phone_number_validation": "Phone number must start with +91 followed by 10 digits", "phone_number_verified": "Phone Number Verified", "pincode": "Pincode", diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 5b6af0ac5a6..88f67a2bd43 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -1,9 +1,6 @@ import { differenceInMinutes, format } from "date-fns"; import html2canvas from "html2canvas"; -import { AREACODES, IN_LANDLINE_AREA_CODES } from "@/common/constants"; -import phoneCodesJson from "@/common/static/countryPhoneAndFlags.json"; - import dayjs from "@/Utils/dayjs"; import { Time } from "@/Utils/types"; import { Patient } from "@/types/emr/newPatient"; @@ -107,155 +104,12 @@ export const classNames = (...classes: (string | boolean | undefined)[]) => { return classes.filter(Boolean).join(" "); }; -export const getPincodeDetails = async (pincode: string, apiKey: string) => { - const response = await fetch( - `https://api.data.gov.in/resource/6176ee09-3d56-4a3b-8115-21841576b2f6?api-key=${apiKey}&format=json&filters[pincode]=${pincode}&limit=1`, - ); - const data = await response.json(); - return data.records[0]; -}; - export const isUserOnline = (user: { last_login: DateLike }) => { return user.last_login ? dayjs().subtract(5, "minutes").isBefore(user.last_login) : false; }; -export interface CountryData { - flag: string; - name: string; - code: string; -} - -export const parsePhoneNumber = (phoneNumber: string, countryCode?: string) => { - if (!phoneNumber) return ""; - if (phoneNumber === "+91") return ""; - const phoneCodes: Record = phoneCodesJson; - let parsedNumber = phoneNumber.replace(/[-+() ]/g, ""); - if (parsedNumber.length < 12) return ""; - if (countryCode && phoneCodes[countryCode]) { - parsedNumber = phoneCodes[countryCode].code + parsedNumber; - } else if (!phoneNumber.startsWith("+")) { - return undefined; - } - parsedNumber = "+" + parsedNumber; - return parsedNumber; -}; - -export const formatPhoneNumber = (phoneNumber: string) => { - if (phoneNumber.startsWith("+91")) { - phoneNumber = phoneNumber.startsWith("+910") - ? phoneNumber.slice(4) - : phoneNumber.slice(3); - const landline_code = IN_LANDLINE_AREA_CODES.find((code) => - phoneNumber.startsWith(code), - ); - if (landline_code === undefined) - return "+91" + " " + phoneNumber.slice(0, 5) + " " + phoneNumber.slice(5); - const subscriber_no_length = 10 - landline_code.length; - return ( - "+91" + - " " + - landline_code + - " " + - phoneNumber.slice( - landline_code.length, - subscriber_no_length / 2 + landline_code.length, - ) + - " " + - phoneNumber.slice(subscriber_no_length / 2 + landline_code.length) - ); - } else if (phoneNumber.startsWith("1800")) { - return "1800" + " " + phoneNumber.slice(4, 7) + " " + phoneNumber.slice(7); - } else if (phoneNumber.startsWith("+")) { - const countryCode = getCountryCode(phoneNumber); - if (!countryCode) return phoneNumber; - const phoneCodes: Record = phoneCodesJson; - return ( - "+" + - phoneCodes[countryCode].code + - " " + - phoneNumber.slice(phoneCodes[countryCode].code.length + 1) - ); - } - return phoneNumber; -}; - -export const getCountryCode = (phoneNumber: string) => { - if (phoneNumber.startsWith("+")) { - const phoneCodes: Record = phoneCodesJson; - const phoneCodesArr = Object.keys(phoneCodes); - phoneNumber = phoneNumber.slice(1); - const allMatchedCountries: { name: string; code: string }[] = []; - for (let i = 0; i < phoneCodesArr.length; i++) { - if ( - phoneNumber.startsWith( - phoneCodes[phoneCodesArr[i]].code.replaceAll("-", ""), - ) - ) { - allMatchedCountries.push({ - name: phoneCodesArr[i], - code: phoneCodes[phoneCodesArr[i]].code.replaceAll("-", ""), - }); - } - } - // returns the country which is longest in case there are multiple matches - if (allMatchedCountries.length === 0) return undefined; - const matchedCountry = allMatchedCountries.reduce((max, country) => - max.code > country.code ? max : country, - ); - const sameCodeCountries = allMatchedCountries.filter( - (country) => country.code === matchedCountry.code, - ); - if (matchedCountry === undefined) return undefined; - // some countries share same country code but differ in area codes - // The area codes are checked for such countries - if (matchedCountry.code == "1") { - const areaCode = phoneNumber.substring(1, 4); - return ( - sameCodeCountries.find((country) => - AREACODES[country.name]?.includes(areaCode), - )?.name ?? "US" - ); - } else if (matchedCountry.code === "262") { - const areaCode = phoneNumber.substring(3, 6); - return sameCodeCountries.find((country) => - AREACODES[country.name]?.includes(areaCode), - )?.name; - } else if (matchedCountry.code === "61") { - const areaCode = phoneNumber.substring(2, 7); - return ( - sameCodeCountries.find((country) => - AREACODES[country.name]?.includes(areaCode), - )?.name ?? "AU" - ); - } else if (matchedCountry.code === "599") { - const areaCode = phoneNumber.substring(3, 4); - return ( - sameCodeCountries.find((country) => - AREACODES[country.name]?.includes(areaCode), - )?.name ?? "CW" - ); - } else if (matchedCountry.code == "7") { - const areaCode = phoneNumber.substring(1, 2); - return ( - sameCodeCountries.find((country) => - AREACODES[country.name]?.includes(areaCode), - )?.name ?? "RU" - ); - } else if (matchedCountry.code == "47") { - const areaCode = phoneNumber.substring(2, 4); - return ( - sameCodeCountries.find((country) => - AREACODES[country.name]?.includes(areaCode), - )?.name ?? "NO" - ); - } - return matchedCountry.name; - } - return undefined; -}; - const getRelativeDateSuffix = (abbreviated: boolean) => { return { day: abbreviated ? "d" : "days", diff --git a/src/common/constants.tsx b/src/common/constants.tsx index a88b50e35b0..2aea03a09b0 100644 --- a/src/common/constants.tsx +++ b/src/common/constants.tsx @@ -376,129 +376,6 @@ export const FACILITY_FEATURE_TYPES: { }, ]; -export const AREACODES: Record = { - CA: [ - "403", - "587", - "250", - "604", - "778", - "204", - "431", - "506", - "709", - "867", - "902", - "226", - "249", - "289", - "343", - "365", - "416", - "437", - "519", - "613", - "647", - "705", - "807", - "902", - "418", - "438", - "450", - "514", - "579", - "581", - "819", - "306", - "639", - "867", - ], - JM: ["658", "876"], - PR: ["787", "939"], - DO: ["809", "829"], - RE: ["262", "263", "692", "693"], - YT: ["269", "639"], - CC: ["89162"], - CX: ["89164"], - BQ: ["9"], - KZ: ["6", "7"], - SJ: ["79"], -}; - -export const IN_LANDLINE_AREA_CODES = [ - "11", - "22", - "33", - "44", - "20", - "40", - "79", - "80", - "120", - "124", - "129", - "135", - "141", - "160", - "161", - "172", - "175", - "181", - "183", - "233", - "240", - "241", - "250", - "251", - "253", - "257", - "260", - "261", - "265", - "343", - "413", - "422", - "431", - "435", - "452", - "462", - "471", - "474", - "477", - "478", - "481", - "484", - "485", - "487", - "490", - "497", - "512", - "522", - "532", - "542", - "551", - "562", - "581", - "591", - "621", - "612", - "641", - "657", - "712", - "721", - "724", - "751", - "761", - "821", - "824", - "831", - "836", - "866", - "870", - "891", - "4822", -]; - export const SOCIOECONOMIC_STATUS_CHOICES = [ "MIDDLE_CLASS", "POOR", diff --git a/src/common/static/countryPhoneAndFlags.json b/src/common/static/countryPhoneAndFlags.json deleted file mode 100644 index ecc024647eb..00000000000 --- a/src/common/static/countryPhoneAndFlags.json +++ /dev/null @@ -1,251 +0,0 @@ -{ - "AD": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ฉ", "name": "Andorra", "code": "376" }, - "AE": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ช", "name": "United Arab Emirates", "code": "971" }, - "AF": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ซ", "name": "Afghanistan", "code": "93" }, - "AG": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ฌ", "name": "Antigua & Barbuda", "code": "1-268" }, - "AI": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ฎ", "name": "Anguilla", "code": "1-264" }, - "AL": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ฑ", "name": "Albania", "code": "355" }, - "AM": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ฒ", "name": "Armenia", "code": "374" }, - "AO": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ด", "name": "Angola", "code": "244" }, - "AR": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ท", "name": "Argentina", "code": "54" }, - "AS": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ธ", "name": "American Samoa", "code": "1-684" }, - "AT": { "flag": "๐Ÿ‡ฆ๐Ÿ‡น", "name": "Austria", "code": "43" }, - "AU": { "flag": "๐Ÿ‡ฆ๐Ÿ‡บ", "name": "Australia", "code": "61" }, - "AW": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ผ", "name": "Aruba", "code": "297" }, - "AX": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ฝ", "name": "ร…land Islands", "code": "358-18" }, - "AZ": { "flag": "๐Ÿ‡ฆ๐Ÿ‡ฟ", "name": "Azerbaijan", "code": "994" }, - "BA": { "flag": "๐Ÿ‡ง๐Ÿ‡ฆ", "name": "Bosnia & Herzegovina", "code": "387" }, - "BB": { "flag": "๐Ÿ‡ง๐Ÿ‡ง", "name": "Barbados", "code": "1-246" }, - "BD": { "flag": "๐Ÿ‡ง๐Ÿ‡ฉ", "name": "Bangladesh", "code": "880" }, - "BE": { "flag": "๐Ÿ‡ง๐Ÿ‡ช", "name": "Belgium", "code": "32" }, - "BF": { "flag": "๐Ÿ‡ง๐Ÿ‡ซ", "name": "Burkina Faso", "code": "226" }, - "BG": { "flag": "๐Ÿ‡ง๐Ÿ‡ฌ", "name": "Bulgaria", "code": "359" }, - "BH": { "flag": "๐Ÿ‡ง๐Ÿ‡ญ", "name": "Bahrain", "code": "973" }, - "BI": { "flag": "๐Ÿ‡ง๐Ÿ‡ฎ", "name": "Burundi", "code": "257" }, - "BJ": { "flag": "๐Ÿ‡ง๐Ÿ‡ฏ", "name": "Benin", "code": "229" }, - "BL": { "flag": "๐Ÿ‡ง๐Ÿ‡ฑ", "name": "St. Barthรฉlemy", "code": "590" }, - "BM": { "flag": "๐Ÿ‡ง๐Ÿ‡ฒ", "name": "Bermuda", "code": "1-441" }, - "BN": { "flag": "๐Ÿ‡ง๐Ÿ‡ณ", "name": "Brunei", "code": "673" }, - "BO": { "flag": "๐Ÿ‡ง๐Ÿ‡ด", "name": "Bolivia", "code": "591" }, - "BQ": { "flag": "๐Ÿ‡ง๐Ÿ‡ถ", "name": "Caribbean Netherlands", "code": "599" }, - "BR": { "flag": "๐Ÿ‡ง๐Ÿ‡ท", "name": "Brazil", "code": "55" }, - "BS": { "flag": "๐Ÿ‡ง๐Ÿ‡ธ", "name": "Bahamas", "code": "1-242" }, - "BT": { "flag": "๐Ÿ‡ง๐Ÿ‡น", "name": "Bhutan", "code": "975" }, - "BW": { "flag": "๐Ÿ‡ง๐Ÿ‡ผ", "name": "Botswana", "code": "267" }, - "BY": { "flag": "๐Ÿ‡ง๐Ÿ‡พ", "name": "Belarus", "code": "375" }, - "BZ": { "flag": "๐Ÿ‡ง๐Ÿ‡ฟ", "name": "Belize", "code": "501" }, - "CA": { "flag": "๐Ÿ‡จ๐Ÿ‡ฆ", "name": "Canada", "code": "1" }, - "CC": { "flag": "๐Ÿ‡จ๐Ÿ‡จ", "name": "Cocos (Keeling) Islands", "code": "61" }, - "CD": { "flag": "๐Ÿ‡จ๐Ÿ‡ฉ", "name": "Congo - Kinshasa", "code": "243" }, - "CF": { "flag": "๐Ÿ‡จ๐Ÿ‡ซ", "name": "Central African Republic", "code": "236" }, - "CG": { "flag": "๐Ÿ‡จ๐Ÿ‡ฌ", "name": "Congo - Brazzaville", "code": "242" }, - "CH": { "flag": "๐Ÿ‡จ๐Ÿ‡ญ", "name": "Switzerland", "code": "41" }, - "CI": { "flag": "๐Ÿ‡จ๐Ÿ‡ฎ", "name": "Cรดte dโ€™Ivoire", "code": "225" }, - "CK": { "flag": "๐Ÿ‡จ๐Ÿ‡ฐ", "name": "Cook Islands", "code": "682" }, - "CL": { "flag": "๐Ÿ‡จ๐Ÿ‡ฑ", "name": "Chile", "code": "56" }, - "CM": { "flag": "๐Ÿ‡จ๐Ÿ‡ฒ", "name": "Cameroon", "code": "237" }, - "CN": { "flag": "๐Ÿ‡จ๐Ÿ‡ณ", "name": "China", "code": "86" }, - "CO": { "flag": "๐Ÿ‡จ๐Ÿ‡ด", "name": "Colombia", "code": "57" }, - "CR": { "flag": "๐Ÿ‡จ๐Ÿ‡ท", "name": "Costa Rica", "code": "506" }, - "CU": { "flag": "๐Ÿ‡จ๐Ÿ‡บ", "name": "Cuba", "code": "53" }, - "CV": { "flag": "๐Ÿ‡จ๐Ÿ‡ป", "name": "Cape Verde", "code": "238" }, - "CW": { "flag": "๐Ÿ‡จ๐Ÿ‡ผ", "name": "Curaรงao", "code": "599" }, - "CX": { "flag": "๐Ÿ‡จ๐Ÿ‡ฝ", "name": "Christmas Island", "code": "61" }, - "CY": { "flag": "๐Ÿ‡จ๐Ÿ‡พ", "name": "Cyprus", "code": "357" }, - "CZ": { "flag": "๐Ÿ‡จ๐Ÿ‡ฟ", "name": "Czechia", "code": "420" }, - "DE": { "flag": "๐Ÿ‡ฉ๐Ÿ‡ช", "name": "Germany", "code": "49" }, - "DJ": { "flag": "๐Ÿ‡ฉ๐Ÿ‡ฏ", "name": "Djibouti", "code": "253" }, - "DK": { "flag": "๐Ÿ‡ฉ๐Ÿ‡ฐ", "name": "Denmark", "code": "45" }, - "DM": { "flag": "๐Ÿ‡ฉ๐Ÿ‡ฒ", "name": "Dominica", "code": "1-767" }, - "DO": { "flag": "๐Ÿ‡ฉ๐Ÿ‡ด", "name": "Dominican Republic", "code": "1" }, - "DZ": { "flag": "๐Ÿ‡ฉ๐Ÿ‡ฟ", "name": "Algeria", "code": "213" }, - "EC": { "flag": "๐Ÿ‡ช๐Ÿ‡จ", "name": "Ecuador", "code": "593" }, - "EE": { "flag": "๐Ÿ‡ช๐Ÿ‡ช", "name": "Estonia", "code": "372" }, - "EG": { "flag": "๐Ÿ‡ช๐Ÿ‡ฌ", "name": "Egypt", "code": "20" }, - "EH": { "flag": "๐Ÿ‡ช๐Ÿ‡ญ", "name": "Western Sahara", "code": "212" }, - "ER": { "flag": "๐Ÿ‡ช๐Ÿ‡ท", "name": "Eritrea", "code": "291" }, - "ES": { "flag": "๐Ÿ‡ช๐Ÿ‡ธ", "name": "Spain", "code": "34" }, - "ET": { "flag": "๐Ÿ‡ช๐Ÿ‡น", "name": "Ethiopia", "code": "251" }, - "FI": { "flag": "๐Ÿ‡ซ๐Ÿ‡ฎ", "name": "Finland", "code": "358" }, - "FJ": { "flag": "๐Ÿ‡ซ๐Ÿ‡ฏ", "name": "Fiji", "code": "679" }, - "FK": { "flag": "๐Ÿ‡ซ๐Ÿ‡ฐ", "name": "Falkland Islands", "code": "500" }, - "FM": { "flag": "๐Ÿ‡ซ๐Ÿ‡ฒ", "name": "Micronesia", "code": "691" }, - "FO": { "flag": "๐Ÿ‡ซ๐Ÿ‡ด", "name": "Faroe Islands", "code": "298" }, - "FR": { "flag": "๐Ÿ‡ซ๐Ÿ‡ท", "name": "France", "code": "33" }, - "GA": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ฆ", "name": "Gabon", "code": "241" }, - "GB": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ง", "name": "United Kingdom", "code": "44" }, - "GD": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ฉ", "name": "Grenada", "code": "1-473" }, - "GE": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ช", "name": "Georgia", "code": "995" }, - "GF": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ซ", "name": "French Guiana", "code": "594" }, - "GG": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ฌ", "name": "Guernsey", "code": "44-1481" }, - "GH": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ญ", "name": "Ghana", "code": "233" }, - "GI": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ฎ", "name": "Gibraltar", "code": "350" }, - "GL": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ฑ", "name": "Greenland", "code": "299" }, - "GM": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ฒ", "name": "Gambia", "code": "220" }, - "GN": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ณ", "name": "Guinea", "code": "224" }, - "GP": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ต", "name": "Guadeloupe", "code": "590" }, - "GQ": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ถ", "name": "Equatorial Guinea", "code": "240" }, - "GR": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ท", "name": "Greece", "code": "30" }, - "GT": { "flag": "๐Ÿ‡ฌ๐Ÿ‡น", "name": "Guatemala", "code": "502" }, - "GU": { "flag": "๐Ÿ‡ฌ๐Ÿ‡บ", "name": "Guam", "code": "1-671" }, - "GW": { "flag": "๐Ÿ‡ฌ๐Ÿ‡ผ", "name": "Guinea-Bissau", "code": "245" }, - "GY": { "flag": "๐Ÿ‡ฌ๐Ÿ‡พ", "name": "Guyana", "code": "592" }, - "HK": { "flag": "๐Ÿ‡ญ๐Ÿ‡ฐ", "name": "Hong Kong SAR China", "code": "852" }, - "HM": { "flag": "๐Ÿ‡ญ๐Ÿ‡ฒ", "name": "Heard & McDonald Islands", "code": " " }, - "HN": { "flag": "๐Ÿ‡ญ๐Ÿ‡ณ", "name": "Honduras", "code": "504" }, - "HR": { "flag": "๐Ÿ‡ญ๐Ÿ‡ท", "name": "Croatia", "code": "385" }, - "HT": { "flag": "๐Ÿ‡ญ๐Ÿ‡น", "name": "Haiti", "code": "509" }, - "HU": { "flag": "๐Ÿ‡ญ๐Ÿ‡บ", "name": "Hungary", "code": "36" }, - "ID": { "flag": "๐Ÿ‡ฎ๐Ÿ‡ฉ", "name": "Indonesia", "code": "62" }, - "IE": { "flag": "๐Ÿ‡ฎ๐Ÿ‡ช", "name": "Ireland", "code": "353" }, - "IL": { "flag": "๐Ÿ‡ฎ๐Ÿ‡ฑ", "name": "Israel", "code": "972" }, - "IM": { "flag": "๐Ÿ‡ฎ๐Ÿ‡ฒ", "name": "Isle of Man", "code": "44-1624" }, - "IN": { "flag": "๐Ÿ‡ฎ๐Ÿ‡ณ", "name": "India", "code": "91" }, - "IO": { - "flag": "๐Ÿ‡ฎ๐Ÿ‡ด", - "name": "British Indian Ocean Territory", - "code": "246" - }, - "IQ": { "flag": "๐Ÿ‡ฎ๐Ÿ‡ถ", "name": "Iraq", "code": "964" }, - "IR": { "flag": "๐Ÿ‡ฎ๐Ÿ‡ท", "name": "Iran", "code": "98" }, - "IS": { "flag": "๐Ÿ‡ฎ๐Ÿ‡ธ", "name": "Iceland", "code": "354" }, - "IT": { "flag": "๐Ÿ‡ฎ๐Ÿ‡น", "name": "Italy", "code": "39" }, - "JE": { "flag": "๐Ÿ‡ฏ๐Ÿ‡ช", "name": "Jersey", "code": "44-1534" }, - "JM": { "flag": "๐Ÿ‡ฏ๐Ÿ‡ฒ", "name": "Jamaica", "code": "1" }, - "JO": { "flag": "๐Ÿ‡ฏ๐Ÿ‡ด", "name": "Jordan", "code": "962" }, - "JP": { "flag": "๐Ÿ‡ฏ๐Ÿ‡ต", "name": "Japan", "code": "81" }, - "KE": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ช", "name": "Kenya", "code": "254" }, - "KG": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ฌ", "name": "Kyrgyzstan", "code": "996" }, - "KH": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ญ", "name": "Cambodia", "code": "855" }, - "KI": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ฎ", "name": "Kiribati", "code": "686" }, - "KM": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ฒ", "name": "Comoros", "code": "269" }, - "KN": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ณ", "name": "St. Kitts & Nevis", "code": "1-869" }, - "KP": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ต", "name": "North Korea", "code": "850" }, - "KR": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ท", "name": "South Korea", "code": "82" }, - "KW": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ผ", "name": "Kuwait", "code": "965" }, - "KY": { "flag": "๐Ÿ‡ฐ๐Ÿ‡พ", "name": "Cayman Islands", "code": "1-345" }, - "KZ": { "flag": "๐Ÿ‡ฐ๐Ÿ‡ฟ", "name": "Kazakhstan", "code": "7" }, - "LA": { "flag": "๐Ÿ‡ฑ๐Ÿ‡ฆ", "name": "Laos", "code": "856" }, - "LB": { "flag": "๐Ÿ‡ฑ๐Ÿ‡ง", "name": "Lebanon", "code": "961" }, - "LC": { "flag": "๐Ÿ‡ฑ๐Ÿ‡จ", "name": "St. Lucia", "code": "1-758" }, - "LI": { "flag": "๐Ÿ‡ฑ๐Ÿ‡ฎ", "name": "Liechtenstein", "code": "423" }, - "LK": { "flag": "๐Ÿ‡ฑ๐Ÿ‡ฐ", "name": "Sri Lanka", "code": "94" }, - "LR": { "flag": "๐Ÿ‡ฑ๐Ÿ‡ท", "name": "Liberia", "code": "231" }, - "LS": { "flag": "๐Ÿ‡ฑ๐Ÿ‡ธ", "name": "Lesotho", "code": "266" }, - "LT": { "flag": "๐Ÿ‡ฑ๐Ÿ‡น", "name": "Lithuania", "code": "370" }, - "LU": { "flag": "๐Ÿ‡ฑ๐Ÿ‡บ", "name": "Luxembourg", "code": "352" }, - "LV": { "flag": "๐Ÿ‡ฑ๐Ÿ‡ป", "name": "Latvia", "code": "371" }, - "LY": { "flag": "๐Ÿ‡ฑ๐Ÿ‡พ", "name": "Libya", "code": "218" }, - "MA": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ฆ", "name": "Morocco", "code": "212" }, - "MC": { "flag": "๐Ÿ‡ฒ๐Ÿ‡จ", "name": "Monaco", "code": "377" }, - "MD": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ฉ", "name": "Moldova", "code": "373" }, - "ME": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ช", "name": "Montenegro", "code": "382" }, - "MF": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ซ", "name": "St. Martin", "code": "590" }, - "MG": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ฌ", "name": "Madagascar", "code": "261" }, - "MH": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ญ", "name": "Marshall Islands", "code": "692" }, - "MK": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ฐ", "name": "North Macedonia", "code": "389" }, - "ML": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ฑ", "name": "Mali", "code": "223" }, - "MM": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ฒ", "name": "Myanmar (Burma)", "code": "95" }, - "MN": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ณ", "name": "Mongolia", "code": "976" }, - "MO": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ด", "name": "Macao SAR China", "code": "853" }, - "MP": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ต", "name": "Northern Mariana Islands", "code": "1-670" }, - "MQ": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ถ", "name": "Martinique", "code": "596" }, - "MR": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ท", "name": "Mauritania", "code": "222" }, - "MS": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ธ", "name": "Montserrat", "code": "1-664" }, - "MT": { "flag": "๐Ÿ‡ฒ๐Ÿ‡น", "name": "Malta", "code": "356" }, - "MU": { "flag": "๐Ÿ‡ฒ๐Ÿ‡บ", "name": "Mauritius", "code": "230" }, - "MV": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ป", "name": "Maldives", "code": "960" }, - "MW": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ผ", "name": "Malawi", "code": "265" }, - "MX": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ฝ", "name": "Mexico", "code": "52" }, - "MY": { "flag": "๐Ÿ‡ฒ๐Ÿ‡พ", "name": "Malaysia", "code": "60" }, - "MZ": { "flag": "๐Ÿ‡ฒ๐Ÿ‡ฟ", "name": "Mozambique", "code": "258" }, - "NA": { "flag": "๐Ÿ‡ณ๐Ÿ‡ฆ", "name": "Namibia", "code": "264" }, - "NC": { "flag": "๐Ÿ‡ณ๐Ÿ‡จ", "name": "New Caledonia", "code": "687" }, - "NE": { "flag": "๐Ÿ‡ณ๐Ÿ‡ช", "name": "Niger", "code": "227" }, - "NF": { "flag": "๐Ÿ‡ณ๐Ÿ‡ซ", "name": "Norfolk Island", "code": "672" }, - "NG": { "flag": "๐Ÿ‡ณ๐Ÿ‡ฌ", "name": "Nigeria", "code": "234" }, - "NI": { "flag": "๐Ÿ‡ณ๐Ÿ‡ฎ", "name": "Nicaragua", "code": "505" }, - "NL": { "flag": "๐Ÿ‡ณ๐Ÿ‡ฑ", "name": "Netherlands", "code": "31" }, - "NO": { "flag": "๐Ÿ‡ณ๐Ÿ‡ด", "name": "Norway", "code": "47" }, - "NP": { "flag": "๐Ÿ‡ณ๐Ÿ‡ต", "name": "Nepal", "code": "977" }, - "NR": { "flag": "๐Ÿ‡ณ๐Ÿ‡ท", "name": "Nauru", "code": "674" }, - "NU": { "flag": "๐Ÿ‡ณ๐Ÿ‡บ", "name": "Niue", "code": "683" }, - "NZ": { "flag": "๐Ÿ‡ณ๐Ÿ‡ฟ", "name": "New Zealand", "code": "64" }, - "OM": { "flag": "๐Ÿ‡ด๐Ÿ‡ฒ", "name": "Oman", "code": "968" }, - "PA": { "flag": "๐Ÿ‡ต๐Ÿ‡ฆ", "name": "Panama", "code": "507" }, - "PE": { "flag": "๐Ÿ‡ต๐Ÿ‡ช", "name": "Peru", "code": "51" }, - "PF": { "flag": "๐Ÿ‡ต๐Ÿ‡ซ", "name": "French Polynesia", "code": "689" }, - "PG": { "flag": "๐Ÿ‡ต๐Ÿ‡ฌ", "name": "Papua New Guinea", "code": "675" }, - "PH": { "flag": "๐Ÿ‡ต๐Ÿ‡ญ", "name": "Philippines", "code": "63" }, - "PK": { "flag": "๐Ÿ‡ต๐Ÿ‡ฐ", "name": "Pakistan", "code": "92" }, - "PL": { "flag": "๐Ÿ‡ต๐Ÿ‡ฑ", "name": "Poland", "code": "48" }, - "PM": { "flag": "๐Ÿ‡ต๐Ÿ‡ฒ", "name": "St. Pierre & Miquelon", "code": "508" }, - "PN": { "flag": "๐Ÿ‡ต๐Ÿ‡ณ", "name": "Pitcairn Islands", "code": "870" }, - "PR": { "flag": "๐Ÿ‡ต๐Ÿ‡ท", "name": "Puerto Rico", "code": "1" }, - "PS": { "flag": "๐Ÿ‡ต๐Ÿ‡ธ", "name": "Palestinian Territories", "code": "970" }, - "PT": { "flag": "๐Ÿ‡ต๐Ÿ‡น", "name": "Portugal", "code": "351" }, - "PW": { "flag": "๐Ÿ‡ต๐Ÿ‡ผ", "name": "Palau", "code": "680" }, - "PY": { "flag": "๐Ÿ‡ต๐Ÿ‡พ", "name": "Paraguay", "code": "595" }, - "QA": { "flag": "๐Ÿ‡ถ๐Ÿ‡ฆ", "name": "Qatar", "code": "974" }, - "RE": { "flag": "๐Ÿ‡ท๐Ÿ‡ช", "name": "Rรฉunion", "code": "262" }, - "RO": { "flag": "๐Ÿ‡ท๐Ÿ‡ด", "name": "Romania", "code": "40" }, - "RS": { "flag": "๐Ÿ‡ท๐Ÿ‡ธ", "name": "Serbia", "code": "381" }, - "RU": { "flag": "๐Ÿ‡ท๐Ÿ‡บ", "name": "Russia", "code": "7" }, - "RW": { "flag": "๐Ÿ‡ท๐Ÿ‡ผ", "name": "Rwanda", "code": "250" }, - "SA": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฆ", "name": "Saudi Arabia", "code": "966" }, - "SB": { "flag": "๐Ÿ‡ธ๐Ÿ‡ง", "name": "Solomon Islands", "code": "677" }, - "SC": { "flag": "๐Ÿ‡ธ๐Ÿ‡จ", "name": "Seychelles", "code": "248" }, - "SD": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฉ", "name": "Sudan", "code": "249" }, - "SE": { "flag": "๐Ÿ‡ธ๐Ÿ‡ช", "name": "Sweden", "code": "46" }, - "SG": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฌ", "name": "Singapore", "code": "65" }, - "SH": { "flag": "๐Ÿ‡ธ๐Ÿ‡ญ", "name": "St. Helena", "code": "290" }, - "SI": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฎ", "name": "Slovenia", "code": "386" }, - "SJ": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฏ", "name": "Svalbard & Jan Mayen", "code": "47" }, - "SK": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฐ", "name": "Slovakia", "code": "421" }, - "SL": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฑ", "name": "Sierra Leone", "code": "232" }, - "SM": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฒ", "name": "San Marino", "code": "378" }, - "SN": { "flag": "๐Ÿ‡ธ๐Ÿ‡ณ", "name": "Senegal", "code": "221" }, - "SO": { "flag": "๐Ÿ‡ธ๐Ÿ‡ด", "name": "Somalia", "code": "252" }, - "SR": { "flag": "๐Ÿ‡ธ๐Ÿ‡ท", "name": "Suriname", "code": "597" }, - "SS": { "flag": "๐Ÿ‡ธ๐Ÿ‡ธ", "name": "South Sudan", "code": "211" }, - "ST": { "flag": "๐Ÿ‡ธ๐Ÿ‡น", "name": "Sรฃo Tomรฉ & Prรญncipe", "code": "239" }, - "SV": { "flag": "๐Ÿ‡ธ๐Ÿ‡ป", "name": "El Salvador", "code": "503" }, - "SX": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฝ", "name": "Sint Maarten", "code": "1-721" }, - "SY": { "flag": "๐Ÿ‡ธ๐Ÿ‡พ", "name": "Syria", "code": "963" }, - "SZ": { "flag": "๐Ÿ‡ธ๐Ÿ‡ฟ", "name": "Eswatini", "code": "268" }, - "TC": { "flag": "๐Ÿ‡น๐Ÿ‡จ", "name": "Turks & Caicos Islands", "code": "1-649" }, - "TD": { "flag": "๐Ÿ‡น๐Ÿ‡ฉ", "name": "Chad", "code": "235" }, - "TG": { "flag": "๐Ÿ‡น๐Ÿ‡ฌ", "name": "Togo", "code": "228" }, - "TH": { "flag": "๐Ÿ‡น๐Ÿ‡ญ", "name": "Thailand", "code": "66" }, - "TJ": { "flag": "๐Ÿ‡น๐Ÿ‡ฏ", "name": "Tajikistan", "code": "992" }, - "TK": { "flag": "๐Ÿ‡น๐Ÿ‡ฐ", "name": "Tokelau", "code": "690" }, - "TL": { "flag": "๐Ÿ‡น๐Ÿ‡ฑ", "name": "Timor-Leste", "code": "670" }, - "TM": { "flag": "๐Ÿ‡น๐Ÿ‡ฒ", "name": "Turkmenistan", "code": "993" }, - "TN": { "flag": "๐Ÿ‡น๐Ÿ‡ณ", "name": "Tunisia", "code": "216" }, - "TO": { "flag": "๐Ÿ‡น๐Ÿ‡ด", "name": "Tonga", "code": "676" }, - "TR": { "flag": "๐Ÿ‡น๐Ÿ‡ท", "name": "Turkey", "code": "90" }, - "TT": { "flag": "๐Ÿ‡น๐Ÿ‡น", "name": "Trinidad & Tobago", "code": "1-868" }, - "TV": { "flag": "๐Ÿ‡น๐Ÿ‡ป", "name": "Tuvalu", "code": "688" }, - "TW": { "flag": "๐Ÿ‡น๐Ÿ‡ผ", "name": "Taiwan", "code": "886" }, - "TZ": { "flag": "๐Ÿ‡น๐Ÿ‡ฟ", "name": "Tanzania", "code": "255" }, - "UA": { "flag": "๐Ÿ‡บ๐Ÿ‡ฆ", "name": "Ukraine", "code": "380" }, - "UG": { "flag": "๐Ÿ‡บ๐Ÿ‡ฌ", "name": "Uganda", "code": "256" }, - "UM": { "flag": "๐Ÿ‡บ๐Ÿ‡ฒ", "name": "U.S. Outlying Islands", "code": "1" }, - "US": { "flag": "๐Ÿ‡บ๐Ÿ‡ธ", "name": "United States", "code": "1" }, - "UY": { "flag": "๐Ÿ‡บ๐Ÿ‡พ", "name": "Uruguay", "code": "598" }, - "UZ": { "flag": "๐Ÿ‡บ๐Ÿ‡ฟ", "name": "Uzbekistan", "code": "998" }, - "VA": { "flag": "๐Ÿ‡ป๐Ÿ‡ฆ", "name": "Vatican City", "code": "379" }, - "VC": { "flag": "๐Ÿ‡ป๐Ÿ‡จ", "name": "St. Vincent & Grenadines", "code": "1-784" }, - "VE": { "flag": "๐Ÿ‡ป๐Ÿ‡ช", "name": "Venezuela", "code": "58" }, - "VG": { "flag": "๐Ÿ‡ป๐Ÿ‡ฌ", "name": "British Virgin Islands", "code": "1-284" }, - "VI": { "flag": "๐Ÿ‡ป๐Ÿ‡ฎ", "name": "U.S. Virgin Islands", "code": "1-340" }, - "VN": { "flag": "๐Ÿ‡ป๐Ÿ‡ณ", "name": "Vietnam", "code": "84" }, - "VU": { "flag": "๐Ÿ‡ป๐Ÿ‡บ", "name": "Vanuatu", "code": "678" }, - "WF": { "flag": "๐Ÿ‡ผ๐Ÿ‡ซ", "name": "Wallis & Futuna", "code": "681" }, - "WS": { "flag": "๐Ÿ‡ผ๐Ÿ‡ธ", "name": "Samoa", "code": "685" }, - "YE": { "flag": "๐Ÿ‡พ๐Ÿ‡ช", "name": "Yemen", "code": "967" }, - "YT": { "flag": "๐Ÿ‡พ๐Ÿ‡น", "name": "Mayotte", "code": "262" }, - "ZA": { "flag": "๐Ÿ‡ฟ๐Ÿ‡ฆ", "name": "South Africa", "code": "27" }, - "ZM": { "flag": "๐Ÿ‡ฟ๐Ÿ‡ฒ", "name": "Zambia", "code": "260" }, - "ZW": { "flag": "๐Ÿ‡ฟ๐Ÿ‡ผ", "name": "Zimbabwe", "code": "263" } -} diff --git a/src/common/validation.tsx b/src/common/validation.tsx index 1c6343f6069..f50e33b14e6 100644 --- a/src/common/validation.tsx +++ b/src/common/validation.tsx @@ -1,8 +1,3 @@ -export const phonePreg = (phone: string) => { - const pattern = /^((\+91|91|0)[- ]{0,1})?[123456789]\d{9}$/; - return pattern.test(phone); -}; - const valueIsBetween = (val: number, a: number, b: number) => a <= val && val <= b; diff --git a/src/components/Common/SearchByMultipleFields.tsx b/src/components/Common/SearchByMultipleFields.tsx index cde64c75031..de38c0e4248 100644 --- a/src/components/Common/SearchByMultipleFields.tsx +++ b/src/components/Common/SearchByMultipleFields.tsx @@ -1,3 +1,4 @@ +import { isValidPhoneNumber } from "libphonenumber-js"; import React, { useCallback, useEffect, @@ -19,15 +20,13 @@ import { CommandList, } from "@/components/ui/command"; import { Input } from "@/components/ui/input"; +import { PhoneInput } from "@/components/ui/input-phone"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { FieldError } from "@/components/Form/FieldValidators"; -import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField"; - interface SearchOption { key: string; type: "text" | "phone"; @@ -181,15 +180,17 @@ const SearchByMultipleFields: React.FC = ({ switch (selectedOption.type) { case "phone": return ( - setError(error)} + onValueChange={(value) => { + handleSearchChange({ value }); + setError( + isValidPhoneNumber(value) ? undefined : "Invalid phone number", + ); + }} /> ); default: diff --git a/src/components/Facility/CreateFacilityForm.tsx b/src/components/Facility/CreateFacilityForm.tsx index 718da1d2318..401864a683e 100644 --- a/src/components/Facility/CreateFacilityForm.tsx +++ b/src/components/Facility/CreateFacilityForm.tsx @@ -1,5 +1,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + isValidPhoneNumber, + parsePhoneNumberWithError, +} from "libphonenumber-js"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -19,6 +23,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { PhoneInput } from "@/components/ui/input-phone"; import { Select, SelectContent, @@ -39,7 +44,6 @@ import { import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; -import { parsePhoneNumber } from "@/Utils/utils"; import OrganizationSelector from "@/pages/Organization/components/OrganizationSelector"; import { BaseFacility } from "@/types/facility/facility"; @@ -52,10 +56,8 @@ const facilityFormSchema = z.object({ address: z.string().min(1, "Address is required"), phone_number: z .string() - .regex( - /^\+91[0-9]{10}$/, - "Phone number must start with +91 followed by 10 digits", - ), + .refine(isValidPhoneNumber, "Invalid phone number") + .transform((value) => parsePhoneNumberWithError(value).number.toString()), latitude: z .string() .optional() @@ -119,11 +121,7 @@ export default function CreateFacilityForm({ }); const onSubmit = (data: FacilityFormValues) => { - createFacility({ - ...data, - phone_number: parsePhoneNumber(data.phone_number), - geo_organization: organizationId, - }); + createFacility({ ...data, geo_organization: organizationId }); }; const handleFeatureChange = (value: any) => { @@ -262,13 +260,7 @@ export default function CreateFacilityForm({ Phone Number - + diff --git a/src/components/Facility/FacilityCreate.tsx b/src/components/Facility/FacilityCreate.tsx index 59b87633a8f..7a7611c2bb7 100644 --- a/src/components/Facility/FacilityCreate.tsx +++ b/src/components/Facility/FacilityCreate.tsx @@ -6,6 +6,10 @@ import { } from "@headlessui/react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; +import { + isValidPhoneNumber, + parsePhoneNumberWithError, +} from "libphonenumber-js"; import { navigate } from "raviger"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -27,6 +31,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { PhoneInput } from "@/components/ui/input-phone"; import { Select, SelectContent, @@ -40,8 +45,6 @@ import GLocationPicker from "@/components/Common/GLocationPicker"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; import { FacilityRequest } from "@/components/Facility/models"; -import { PhoneNumberValidator } from "@/components/Form/FieldValidators"; -import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField"; import { MultiSelectFormField } from "@/components/Form/FormFields/SelectFormField"; import TextAreaFormField from "@/components/Form/FormFields/TextAreaFormField"; import TextFormField from "@/components/Form/FormFields/TextFormField"; @@ -50,7 +53,6 @@ import useAppHistory from "@/hooks/useAppHistory"; import { FACILITY_FEATURE_TYPES, FACILITY_TYPES } from "@/common/constants"; import { - phonePreg, validateLatitude, validateLongitude, validatePincode, @@ -59,7 +61,6 @@ import { import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; import request from "@/Utils/request/request"; -import { parsePhoneNumber } from "@/Utils/utils"; import OrganizationSelector from "@/pages/Organization/components/OrganizationSelector"; interface FacilityProps { @@ -84,20 +85,8 @@ export const FacilityCreate = (props: FacilityProps) => { phone_number: z .string() .min(1, { message: t("required") }) - .refine( - (val: string) => { - if ( - !PhoneNumberValidator(["mobile", "landline"])(val) === undefined || - !phonePreg(val) - ) { - return false; - } - return true; - }, - { - message: t("invalid_phone_number"), - }, - ), + .refine(isValidPhoneNumber, t("invalid_phone_number")) + .transform((value) => parsePhoneNumberWithError(value).number.toString()), latitude: z .string() .min(1, { message: t("required") }) @@ -186,10 +175,7 @@ export const FacilityCreate = (props: FacilityProps) => { const onSubmit = async (data: FacilityFormValues) => { setIsLoading(true); try { - const requestData: FacilityRequest = { - ...data, - phone_number: parsePhoneNumber(data.phone_number), - }; + const requestData: FacilityRequest = { ...data }; const { res, data: responseData } = facilityId ? await request(routes.updateFacility, { @@ -360,22 +346,12 @@ export const FacilityCreate = (props: FacilityProps) => { control={form.control} name="phone_number" render={({ field }) => ( - + + {t("emergency_contact_number")} - - {t("emergency_contact_number")} - - } - {...field} - types={["mobile", "landline"]} - onChange={(value) => { - field.onChange(value.value); - }} - error={form.formState.errors.phone_number?.message} - /> + + )} /> diff --git a/src/components/Form/FieldValidators.tsx b/src/components/Form/FieldValidators.tsx deleted file mode 100644 index de0e8fff0cb..00000000000 --- a/src/components/Form/FieldValidators.tsx +++ /dev/null @@ -1,52 +0,0 @@ -export type FieldError = string | undefined; - -export const RegexValidator = (regex: RegExp, message = "Invalid input") => { - return (value: string): FieldError => { - return regex.test(value) ? undefined : message; - }; -}; - -// const PHONE_NUMBER_REGEX = -// /^(?:(?:(?:\+|0{0,2})91|0{0,2})(?:\()?\d{3}(?:\))?[-]?\d{3}[-]?\d{4})$/; - -// export const PhoneNumberValidator = (message = "Invalid phone number") => { -// return RegexValidator(PHONE_NUMBER_REGEX, message); -// }; - -// const SUPPORT_PHONE_NUMBER_REGEX = /^1800[-]?\d{3}[-]?\d{3,4}$/; - -// export const SupportPhoneNumberValidator = ( -// message = "Invalid support phone number" -// ) => { -// return RegexValidator(SUPPORT_PHONE_NUMBER_REGEX, message); -// }; - -// References: https://trai.gov.in/sites/default/files/Recommendations_29052020.pdf -const INDIAN_MOBILE_NUMBER_REGEX = /^(?=^\+91)(^\+91[6-9]\d{9}$)/; -const INTERNATIONAL_MOBILE_NUMBER_REGEX = /^(?!^\+91)(^\+\d{1,3}\d{8,14}$)/; -const MOBILE_NUMBER_REGEX = new RegExp( - `(${INDIAN_MOBILE_NUMBER_REGEX.source})|(${INTERNATIONAL_MOBILE_NUMBER_REGEX.source})`, -); -const INDIAN_LANDLINE_NUMBER_REGEX = /^\+91[2-9]\d{9}$/; -const INDIAN_SUPPORT_NUMBER_REGEX = /^(1800|1860)\d{6,7}$/; - -const PHONE_NUMBER_REGEX_MAP = { - indian_mobile: INDIAN_MOBILE_NUMBER_REGEX, - international_mobile: INTERNATIONAL_MOBILE_NUMBER_REGEX, - mobile: MOBILE_NUMBER_REGEX, - landline: INDIAN_LANDLINE_NUMBER_REGEX, - support: INDIAN_SUPPORT_NUMBER_REGEX, -}; - -export type PhoneNumberType = keyof typeof PHONE_NUMBER_REGEX_MAP; - -export const PhoneNumberValidator = ( - type: PhoneNumberType[] = ["mobile", "landline"], - message = "Invalid phone number", -) => { - const regexes = type.map((t) => PHONE_NUMBER_REGEX_MAP[t]); - return RegexValidator( - new RegExp(regexes.map((r) => r.source).join("|")), - message, - ); -}; diff --git a/src/components/Form/FormFields/FormField.tsx b/src/components/Form/FormFields/FormField.tsx index f3ad0559e44..1e5d2d58938 100644 --- a/src/components/Form/FormFields/FormField.tsx +++ b/src/components/Form/FormFields/FormField.tsx @@ -1,4 +1,3 @@ -import { FieldError } from "@/components/Form/FieldValidators"; import { FormFieldBaseProps } from "@/components/Form/FormFields/Utils"; import { classNames } from "@/Utils/utils"; @@ -30,7 +29,7 @@ export const FieldLabel = (props: LabelProps) => { }; type ErrorProps = { - error: FieldError; + error: string | undefined; className?: string | undefined; }; diff --git a/src/components/Form/FormFields/PhoneNumberFormField.tsx b/src/components/Form/FormFields/PhoneNumberFormField.tsx deleted file mode 100644 index 72a3e0c3076..00000000000 --- a/src/components/Form/FormFields/PhoneNumberFormField.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import React from "react"; -import { useTranslation } from "react-i18next"; - -import { cn } from "@/lib/utils"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { - FieldError, - PhoneNumberType, - PhoneNumberValidator, -} from "@/components/Form/FieldValidators"; -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; - -import phoneCodesJson from "@/common/static/countryPhoneAndFlags.json"; - -import { - CountryData, - formatPhoneNumber as formatPhoneNumberUtil, - getCountryCode, - humanizeStrings, - parsePhoneNumber, -} from "@/Utils/utils"; - -const phoneCodes: Record = phoneCodesJson; - -interface Props extends FormFieldBaseProps { - onError?: (error: FieldError) => void; - hideHelp?: boolean; - types: PhoneNumberType[]; - placeholder?: string; - autoComplete?: string; - disableValidation?: boolean; -} - -const PhoneNumberFormField = React.forwardRef( - (props, ref) => { - const field = useFormFieldPropsResolver(props); - const [error, setError] = useState(); - const [country, setCountry] = useState({ - flag: "๐Ÿ‡ฎ๐Ÿ‡ณ", - name: "India", - code: "91", - }); - const validator = useMemo( - () => PhoneNumberValidator(props.types), - [props.types], - ); - - const validate = useMemo( - () => (value: string | undefined, event: "blur" | "change") => { - if (!value || props.disableValidation) { - return; - } - - const newError = validator(value); - - if (!newError) { - return; - } else if (event === "blur") { - return newError; - } - }, - [props.disableValidation], - ); - - const setValue = useCallback( - (value: string) => { - value = value.replaceAll(/[^0-9+]/g, ""); - if (value.length > 12 && value.startsWith("+910")) { - value = "+91" + value.slice(4); - } - - const error = validate(value, "change"); - field.handleChange(value); - - setError(error); - }, - [field, validate, error], - ); - useEffect(() => { - if (props.onError) { - props.onError(error); - } - }, [error]); - const handleCountryChange = (value: CountryData): void => { - setCountry(value); - setValue(conditionPhoneCode(value.code)); - }; - - useEffect(() => { - if (field.value && field.value.length > 0) { - if (field.value.startsWith("1800")) { - setCountry({ flag: "๐Ÿ“ž", name: "Support", code: "1800" }); - return; - } - if (field.value === "+") { - setCountry({ flag: "๐ŸŒ", name: "Other", code: "+" }); - return; - } - setCountry(phoneCodes[getCountryCode(field.value)!]); - } - }, [field.value]); - - return ( - - )), - }} - > -
- - {({ open }: { open: boolean }) => { - return ( - <> - -
- {country?.flag} - -
-
- setValue(e.target.value)} - disabled={field.disabled} - onBlur={() => setError(validate(field.value, "blur"))} - ref={ref} - /> - - {({ close }) => ( - - )} - - - ); - }} -
-
-
- ); - }, -); -const PhoneNumberTypesHelp = (props: { types: PhoneNumberType[] }) => { - const { t } = useTranslation(); - - return ( -
- -
- Supports only{" "} - - {humanizeStrings(props.types.map((item) => t(item)))} - {" "} - numbers. -
-
- ); -}; -const conditionPhoneCode = (code: string) => { - code = code.split(" ")[0]; - return code.startsWith("+") ? code : "+" + code; -}; - -const formatPhoneNumber = ( - value: string | undefined, - types: PhoneNumberType[], -) => { - if (!value) { - return "+91 "; - } - - if (PhoneNumberValidator(types)(value) !== undefined || value.length < 13) { - return value; - } - - const phoneNumber = parsePhoneNumber(value); - return phoneNumber ? formatPhoneNumberUtil(phoneNumber) : value; -}; - -const CountryCodesList = ({ - handleCountryChange, - onClose, -}: { - handleCountryChange: (value: CountryData) => void; - onClose: () => void; -}) => { - const [searchValue, setSearchValue] = useState(""); - - return ( -
-
- - setSearchValue(e.target.value)} - /> -
- -
    - {Object.entries(phoneCodes) - .filter(([country, { flag, name, code }]) => { - if (searchValue === "") { - return true; - } - return ( - name.toLowerCase().includes(searchValue.toLowerCase()) || - code.includes(searchValue) || - country.toLowerCase().includes(searchValue.toLowerCase()) || - flag.includes(searchValue) - ); - }) - .map(([country, { flag, name, code }]) => ( -
  • { - handleCountryChange({ flag, name, code }); - onClose(); - }} - > - {flag} - {name} - - {" "} - ({conditionPhoneCode(code)}) - -
  • - ))} -
  • { - handleCountryChange({ flag: "๐Ÿ“ž", name: "Support", code: "1800" }); - onClose(); - }} - > - ๐Ÿ“ž - Support - (1800) -
  • -
  • { - handleCountryChange({ flag: "๐ŸŒ", name: "Other", code: "+" }); - onClose(); - }} - > - ๐ŸŒ - Other - (+) -
  • -
-
- ); -}; -export default PhoneNumberFormField; diff --git a/src/components/Form/FormFields/Utils.ts b/src/components/Form/FormFields/Utils.ts index 1e88bcbd6a0..c40ef3c7b2f 100644 --- a/src/components/Form/FormFields/Utils.ts +++ b/src/components/Form/FormFields/Utils.ts @@ -1,7 +1,5 @@ import { FocusEvent } from "react"; -import { FieldError } from "@/components/Form/FieldValidators"; - export type FieldChangeEvent = { name: string; value: T }; export type FieldChangeEventHandler = (event: FieldChangeEvent) => void; @@ -30,7 +28,7 @@ export type FormFieldBaseProps = { id?: string; onChange: FieldChangeEventHandler; value?: T; - error?: FieldError; + error?: string | undefined; onFocus?: (event: FocusEvent) => void; onBlur?: (event: FocusEvent) => void; }; diff --git a/src/components/Form/Utils.ts b/src/components/Form/Utils.ts index 21dc9d8b8aa..d69db98713b 100644 --- a/src/components/Form/Utils.ts +++ b/src/components/Form/Utils.ts @@ -1,14 +1,12 @@ -import { FieldError } from "@/components/Form/FieldValidators"; - export type FormDetails = { [key: string]: any }; export type FormErrors = Partial< - Record + Record >; export type FormState = { form: T; errors: FormErrors }; export type FormAction = | { type: "set_form"; form: T } | { type: "set_errors"; errors: FormErrors } - | { type: "set_field"; name: keyof T; value: any; error: FieldError } + | { type: "set_field"; name: keyof T; value: any; error: string | undefined } | { type: "set_state"; state: FormState }; export type FormReducer = ( prevState: FormState, diff --git a/src/components/Patient/PatientIndex.tsx b/src/components/Patient/PatientIndex.tsx index 035898415da..090f8866c00 100644 --- a/src/components/Patient/PatientIndex.tsx +++ b/src/components/Patient/PatientIndex.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { parsePhoneNumberWithError } from "libphonenumber-js"; import { navigate } from "raviger"; import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -35,7 +36,6 @@ import { GENDER_TYPES } from "@/common/constants"; import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; -import { parsePhoneNumber } from "@/Utils/utils"; import { PartialPatientModel } from "@/types/emr/newPatient"; export default function PatientIndex({ facilityId }: { facilityId: string }) { @@ -100,7 +100,7 @@ export default function PatientIndex({ facilityId }: { facilityId: string }) { queryKey: ["patient-search", facilityId, phoneNumber], queryFn: query.debounced(routes.searchPatient, { body: { - phone_number: parsePhoneNumber(phoneNumber) || "", + phone_number: parsePhoneNumberWithError(phoneNumber).number.toString(), }, }), enabled: !!phoneNumber, diff --git a/src/components/Patient/PatientRegistration.tsx b/src/components/Patient/PatientRegistration.tsx index 302395c1d63..937e0982aff 100644 --- a/src/components/Patient/PatientRegistration.tsx +++ b/src/components/Patient/PatientRegistration.tsx @@ -1,5 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { parsePhoneNumberWithError } from "libphonenumber-js"; import { navigate, useQueryParams } from "raviger"; import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; @@ -49,7 +50,6 @@ import { PLUGIN_Component } from "@/PluginEngine"; import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; -import { parsePhoneNumber } from "@/Utils/utils"; import OrganizationSelector from "@/pages/Organization/components/OrganizationSelector"; import { PatientModel } from "@/types/emr/patient"; import { Organization } from "@/types/organization/organization"; @@ -229,10 +229,14 @@ export default function PatientRegistration( queryKey: ["patients", "phone-number", debouncedNumber], queryFn: query(routes.searchPatient, { body: { - phone_number: parsePhoneNumber(debouncedNumber || "") || "", + phone_number: parsePhoneNumberWithError( + debouncedNumber || "", + ).number.toString(), }, }), - enabled: !!parsePhoneNumber(debouncedNumber || ""), + enabled: !!parsePhoneNumberWithError( + debouncedNumber || "", + ).number.toString(), }); const duplicatePatients = useMemo(() => { @@ -759,7 +763,7 @@ export default function PatientRegistration( {!patientPhoneSearch.isLoading && !!duplicatePatients?.length && - !!parsePhoneNumber(debouncedNumber || "") && + !!parsePhoneNumberWithError(debouncedNumber || "").number.toString() && !suppressDuplicateWarning && ( { - const phoneNumber = parsePhoneNumber(val); - if ( - !phoneNumber || - !PhoneNumberValidator()(phoneNumber) === undefined || - !phonePreg(String(phoneNumber)) - ) { - return false; - } - return true; - }, - { - message: t("invalid_phone_number"), - }, - ), + .refine(isValidPhoneNumber, t("invalid_phone_number")) + .transform((value) => parsePhoneNumberWithError(value).number.toString()), priority: z.number().default(1), }); @@ -139,7 +126,7 @@ export default function ResourceCreate(props: ResourceProps) { reason: data.reason, referring_facility_contact_name: data.referring_facility_contact_name, referring_facility_contact_number: - parsePhoneNumber(data.referring_facility_contact_number) ?? "", + data.referring_facility_contact_number, related_patient: related_patient, priority: data.priority, }; @@ -417,11 +404,9 @@ export default function ResourceCreate(props: ResourceProps) { {t("contact_phone")} - field.onChange(value.value)} + onValueChange={field.onChange} /> diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index b1123f55d08..0785112d32d 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -1,3 +1,4 @@ +import { parsePhoneNumberWithError } from "libphonenumber-js"; import { navigate } from "raviger"; import { useTranslation } from "react-i18next"; @@ -14,12 +15,7 @@ import useAuthUser from "@/hooks/useAuthUser"; import useSlug from "@/hooks/useSlug"; import useWindowDimensions from "@/hooks/useWindowDimensions"; -import { - formatName, - formatPhoneNumber, - isUserOnline, - relativeTime, -} from "@/Utils/utils"; +import { formatName, isUserOnline, relativeTime } from "@/Utils/utils"; import { UserBase } from "@/types/user/user"; const GetDetailsButton = (username: string) => { @@ -215,7 +211,7 @@ const UserListRow = ({ user }: { user: UserBase }) => { {user.user_type} - {formatPhoneNumber(user.phone_number)} + {parsePhoneNumberWithError(user.phone_number).formatInternational()} {GetDetailsButton(user.username)} diff --git a/src/components/ui/input-phone.tsx b/src/components/ui/input-phone.tsx new file mode 100644 index 00000000000..940d3a80702 --- /dev/null +++ b/src/components/ui/input-phone.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; + +import { Input } from "@/components/ui/input"; + +const PhoneInput = React.forwardRef< + HTMLInputElement, + React.ComponentProps<"input"> & { + onValueChange?: (value: string) => void; + } +>(({ className, ...props }, ref) => { + const { t } = useTranslation(); + return ( + { + let value = e.target.value.replace(/[^\d+]/g, ""); + if (value && !value.startsWith("+")) { + value = "+" + value; + } + props.onValueChange?.(value); + props.onChange?.(e); + }} + /> + ); +}); + +PhoneInput.displayName = "PhoneInput"; + +export { PhoneInput }; diff --git a/src/pages/PublicAppointments/auth/PatientLogin.tsx b/src/pages/PublicAppointments/auth/PatientLogin.tsx index 2a077f2a862..812f2ce8f89 100644 --- a/src/pages/PublicAppointments/auth/PatientLogin.tsx +++ b/src/pages/PublicAppointments/auth/PatientLogin.tsx @@ -1,6 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; import dayjs from "dayjs"; +import { isValidPhoneNumber } from "libphonenumber-js"; import { navigate } from "raviger"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -22,10 +23,9 @@ import { InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp"; +import { PhoneInput } from "@/components/ui/input-phone"; import CircularProgress from "@/components/Common/CircularProgress"; -import { PhoneNumberValidator } from "@/components/Form/FieldValidators"; -import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField"; import useAppHistory from "@/hooks/useAppHistory"; import { useAuthContext } from "@/hooks/useAuthUser"; @@ -34,7 +34,6 @@ import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import request from "@/Utils/request/request"; import { HTTPError } from "@/Utils/request/types"; -import { parsePhoneNumber } from "@/Utils/utils"; import { TokenData } from "@/types/auth/otpToken"; const FormSchema = z.object({ @@ -75,16 +74,7 @@ export default function PatientLogin({ ); } const validate = (phoneNumber: string) => { - let errors = ""; - - const parsedPhoneNumber = parsePhoneNumber(phoneNumber); - if ( - !parsedPhoneNumber || - !(PhoneNumberValidator(["mobile"])(parsedPhoneNumber ?? "") === undefined) - ) { - errors = t("invalid_phone"); - } - return errors; + return isValidPhoneNumber(phoneNumber) ? "" : t("invalid_phone"); }; const { mutate: sendOTP, isPending: isSendOTPLoading } = useMutation({ @@ -168,15 +158,18 @@ export default function PatientLogin({ className="flex mt-2 flex-col gap-4 shadow border p-8 rounded-lg" >
- setPhoneNumber(e.value)} - value={phoneNumber} - error={error} - /> + + {t("phone_number")} + + { + setPhoneNumber(value); + setError(""); + }} + /> + + {error && {error}} +