mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 03:05:42 +00:00
Refactor phone input handling across onboarding and settings forms
- Replaced PhoneInput component with ControlledPhoneField for better integration with react-hook-form. - Updated validation for phone numbers using isValidPhone function to ensure proper formatting. - Removed country code handling from forms, simplifying phone number management. - Introduced new phone field component with consistent styling and behavior. - Added phone number validation on the backend using class-validator. - Removed unused phone utility functions and cleaned up related code.
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"handlebars": "^4.7.9",
|
"handlebars": "^4.7.9",
|
||||||
|
"libphonenumber-js": "^1.13.6",
|
||||||
"minio": "7.1.3",
|
"minio": "7.1.3",
|
||||||
"pg": "^8.13.0",
|
"pg": "^8.13.0",
|
||||||
"puppeteer": "^24.2.0",
|
"puppeteer": "^24.2.0",
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
registerDecorator,
|
||||||
|
ValidationArguments,
|
||||||
|
ValidationOptions,
|
||||||
|
ValidatorConstraint,
|
||||||
|
ValidatorConstraintInterface,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { isValidPhoneNumber, parsePhoneNumberFromString } from 'libphonenumber-js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Country-aware phone validation. The value is expected as a full international
|
||||||
|
* number (E.164, e.g. "+251911223344"), so the country is derived from the
|
||||||
|
* value itself — no separate country field needed.
|
||||||
|
*/
|
||||||
|
@ValidatorConstraint({ name: 'IsValidPhone', async: false })
|
||||||
|
export class IsValidPhoneConstraint implements ValidatorConstraintInterface {
|
||||||
|
validate(value: unknown): boolean {
|
||||||
|
// Empty is allowed here; pair with @IsOptional / @IsNotEmpty as needed.
|
||||||
|
if (value === undefined || value === null || value === '') return true;
|
||||||
|
if (typeof value !== 'string') return false;
|
||||||
|
return isValidPhoneNumber(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMessage(args: ValidationArguments): string {
|
||||||
|
return `${args.property} must be a valid international phone number (E.164, e.g. +251911223344)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Class-validator decorator wrapping the country-aware phone constraint. */
|
||||||
|
export function IsValidPhone(validationOptions?: ValidationOptions) {
|
||||||
|
return function (object: object, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
target: object.constructor,
|
||||||
|
propertyName,
|
||||||
|
options: validationOptions,
|
||||||
|
constraints: [],
|
||||||
|
validator: IsValidPhoneConstraint,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a phone string to canonical E.164. Returns the canonical form when
|
||||||
|
* parseable, otherwise the trimmed original (tolerant — never throws), or the
|
||||||
|
* value unchanged when empty/nullish.
|
||||||
|
*/
|
||||||
|
export function normalizeE164(
|
||||||
|
value: string | null | undefined,
|
||||||
|
): string | null | undefined {
|
||||||
|
if (value === undefined || value === null || value === '') return value;
|
||||||
|
const parsed = parsePhoneNumberFromString(value);
|
||||||
|
return parsed?.isValid() ? parsed.number : value.trim();
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { ExternalProfileRepository } from "./external-profile.repository";
|
|||||||
import { CompanyDashboardRepository } from "./company-dashboard.repository";
|
import { CompanyDashboardRepository } from "./company-dashboard.repository";
|
||||||
import { MinioService } from "../minio/minio.service";
|
import { MinioService } from "../minio/minio.service";
|
||||||
import { ETradeService } from "./services/etrade.service";
|
import { ETradeService } from "./services/etrade.service";
|
||||||
|
import { normalizeE164 } from "../../common/validators/is-phone-number.validator";
|
||||||
import { CreateCompanyDto } from "./dto/create-company.dto";
|
import { CreateCompanyDto } from "./dto/create-company.dto";
|
||||||
import { UpdateCompanyDto } from "./dto/update-company.dto";
|
import { UpdateCompanyDto } from "./dto/update-company.dto";
|
||||||
import { CreateExternalProfileDto } from "./dto/create-external-profile.dto";
|
import { CreateExternalProfileDto } from "./dto/create-external-profile.dto";
|
||||||
@@ -86,7 +87,7 @@ export class CompaniesService {
|
|||||||
fanNumber: dto.fanNumber ?? null,
|
fanNumber: dto.fanNumber ?? null,
|
||||||
country: dto.companyLocation ?? "Ethiopia",
|
country: dto.companyLocation ?? "Ethiopia",
|
||||||
address: dto.companyAddress ?? null,
|
address: dto.companyAddress ?? null,
|
||||||
phone: dto.companyPhone ?? null,
|
phone: normalizeE164(dto.companyPhone) ?? null,
|
||||||
email: dto.companyEmail ?? null,
|
email: dto.companyEmail ?? null,
|
||||||
attributes: dto.attributes ?? null,
|
attributes: dto.attributes ?? null,
|
||||||
});
|
});
|
||||||
@@ -109,7 +110,7 @@ export class CompaniesService {
|
|||||||
firstName: identity.firstName,
|
firstName: identity.firstName,
|
||||||
lastName: identity.lastName,
|
lastName: identity.lastName,
|
||||||
email: identity.email,
|
email: identity.email,
|
||||||
phone: identity.phone,
|
phone: normalizeE164(identity.phone) ?? identity.phone,
|
||||||
jobTitle: dto.jobTitle ?? null,
|
jobTitle: dto.jobTitle ?? null,
|
||||||
isPrimaryContact: dto.isPrimaryContact ?? true,
|
isPrimaryContact: dto.isPrimaryContact ?? true,
|
||||||
activeProfileType,
|
activeProfileType,
|
||||||
@@ -209,7 +210,7 @@ export class CompaniesService {
|
|||||||
firstName: identity.firstName,
|
firstName: identity.firstName,
|
||||||
lastName: identity.lastName,
|
lastName: identity.lastName,
|
||||||
email: identity.email,
|
email: identity.email,
|
||||||
phone: identity.phone,
|
phone: normalizeE164(identity.phone) ?? identity.phone,
|
||||||
isPrimaryContact: true,
|
isPrimaryContact: true,
|
||||||
activeProfileType,
|
activeProfileType,
|
||||||
onboardingStep: "company",
|
onboardingStep: "company",
|
||||||
@@ -475,7 +476,8 @@ export class CompaniesService {
|
|||||||
companyUpdates.nationality = dto.nationality;
|
companyUpdates.nationality = dto.nationality;
|
||||||
if (dto.companyName !== undefined) companyUpdates.name = dto.companyName;
|
if (dto.companyName !== undefined) companyUpdates.name = dto.companyName;
|
||||||
if (dto.companyEmail !== undefined) companyUpdates.email = dto.companyEmail;
|
if (dto.companyEmail !== undefined) companyUpdates.email = dto.companyEmail;
|
||||||
if (dto.companyPhone !== undefined) companyUpdates.phone = dto.companyPhone;
|
if (dto.companyPhone !== undefined)
|
||||||
|
companyUpdates.phone = normalizeE164(dto.companyPhone);
|
||||||
if (dto.companyLocation !== undefined)
|
if (dto.companyLocation !== undefined)
|
||||||
companyUpdates.country = dto.companyLocation;
|
companyUpdates.country = dto.companyLocation;
|
||||||
if (dto.companyAddress !== undefined)
|
if (dto.companyAddress !== undefined)
|
||||||
@@ -503,15 +505,16 @@ export class CompaniesService {
|
|||||||
if (dto.contactPersonEmail !== undefined)
|
if (dto.contactPersonEmail !== undefined)
|
||||||
attrUpdates.contactPersonEmail = dto.contactPersonEmail;
|
attrUpdates.contactPersonEmail = dto.contactPersonEmail;
|
||||||
if (dto.contactPersonPhone !== undefined)
|
if (dto.contactPersonPhone !== undefined)
|
||||||
attrUpdates.contactPersonPhone = dto.contactPersonPhone;
|
attrUpdates.contactPersonPhone = normalizeE164(dto.contactPersonPhone);
|
||||||
if (dto.generalManagerName !== undefined)
|
if (dto.generalManagerName !== undefined)
|
||||||
attrUpdates.generalManagerName = dto.generalManagerName;
|
attrUpdates.generalManagerName = dto.generalManagerName;
|
||||||
if (dto.generalManagerEmail !== undefined)
|
if (dto.generalManagerEmail !== undefined)
|
||||||
attrUpdates.generalManagerEmail = dto.generalManagerEmail;
|
attrUpdates.generalManagerEmail = dto.generalManagerEmail;
|
||||||
if (dto.generalManagerPhone !== undefined)
|
if (dto.generalManagerPhone !== undefined)
|
||||||
attrUpdates.generalManagerPhone = dto.generalManagerPhone;
|
attrUpdates.generalManagerPhone = normalizeE164(dto.generalManagerPhone);
|
||||||
if (dto.poaName !== undefined) attrUpdates.poaName = dto.poaName;
|
if (dto.poaName !== undefined) attrUpdates.poaName = dto.poaName;
|
||||||
if (dto.poaPhone !== undefined) attrUpdates.poaPhone = dto.poaPhone;
|
if (dto.poaPhone !== undefined)
|
||||||
|
attrUpdates.poaPhone = normalizeE164(dto.poaPhone);
|
||||||
if (dto.poaEmail !== undefined) attrUpdates.poaEmail = dto.poaEmail;
|
if (dto.poaEmail !== undefined) attrUpdates.poaEmail = dto.poaEmail;
|
||||||
if (dto.poaLocation !== undefined)
|
if (dto.poaLocation !== undefined)
|
||||||
attrUpdates.poaLocation = dto.poaLocation;
|
attrUpdates.poaLocation = dto.poaLocation;
|
||||||
@@ -540,7 +543,7 @@ export class CompaniesService {
|
|||||||
if (dto.houseNo !== undefined)
|
if (dto.houseNo !== undefined)
|
||||||
companyUpdates.houseNo = dto.houseNo;
|
companyUpdates.houseNo = dto.houseNo;
|
||||||
if (dto.etradePhone !== undefined)
|
if (dto.etradePhone !== undefined)
|
||||||
companyUpdates.etradePhone = dto.etradePhone;
|
companyUpdates.etradePhone = normalizeE164(dto.etradePhone);
|
||||||
|
|
||||||
companyUpdates.attributes = attrUpdates;
|
companyUpdates.attributes = attrUpdates;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IsString, IsNotEmpty, IsOptional, IsEmail, MaxLength, IsBoolean, IsEnum
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { CompanyType } from '../entities/company.entity';
|
import { CompanyType } from '../entities/company.entity';
|
||||||
import { ProfileType } from '../entities/company-profile.entity';
|
import { ProfileType } from '../entities/company-profile.entity';
|
||||||
|
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||||
|
|
||||||
export class CompanyProfileInputDto {
|
export class CompanyProfileInputDto {
|
||||||
@IsEnum(ProfileType)
|
@IsEnum(ProfileType)
|
||||||
@@ -30,6 +31,7 @@ export class CreateCompanyWithProfileDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(20)
|
@MaxLength(20)
|
||||||
|
@IsValidPhone()
|
||||||
companyPhone?: string;
|
companyPhone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IsString, IsNotEmpty, IsOptional, IsEnum, MaxLength, Length, Matches, IsEmail } from 'class-validator';
|
import { IsString, IsNotEmpty, IsOptional, IsEnum, MaxLength, Length, Matches, IsEmail } from 'class-validator';
|
||||||
import { CompanyType, CompanyStatus } from '../entities/company.entity';
|
import { CompanyType, CompanyStatus } from '../entities/company.entity';
|
||||||
|
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||||
|
|
||||||
export class CreateCompanyDto {
|
export class CreateCompanyDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -37,6 +38,7 @@ export class CreateCompanyDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(20)
|
@MaxLength(20)
|
||||||
|
@IsValidPhone()
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IsString, IsNotEmpty, IsOptional, IsEmail, MaxLength, IsBoolean, IsUUID } from 'class-validator';
|
import { IsString, IsNotEmpty, IsOptional, IsEmail, MaxLength, IsBoolean, IsUUID } from 'class-validator';
|
||||||
|
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||||
|
|
||||||
export class CreateExternalProfileDto {
|
export class CreateExternalProfileDto {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
@@ -26,6 +27,7 @@ export class CreateExternalProfileDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(20)
|
@MaxLength(20)
|
||||||
|
@IsValidPhone()
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IsString, IsOptional, IsEmail, MaxLength, Length, Matches, IsEnum } from 'class-validator';
|
import { IsString, IsOptional, IsEmail, MaxLength, Length, Matches, IsEnum } from 'class-validator';
|
||||||
import { CompanyNationality } from '../entities/company.entity';
|
import { CompanyNationality } from '../entities/company.entity';
|
||||||
|
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||||
|
|
||||||
export class UpdateProfileDto {
|
export class UpdateProfileDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -19,6 +20,7 @@ export class UpdateProfileDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(20)
|
@MaxLength(20)
|
||||||
|
@IsValidPhone()
|
||||||
companyPhone?: string;
|
companyPhone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -60,6 +62,7 @@ export class UpdateProfileDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsValidPhone()
|
||||||
contactPersonPhone?: string;
|
contactPersonPhone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -72,6 +75,7 @@ export class UpdateProfileDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsValidPhone()
|
||||||
generalManagerPhone?: string;
|
generalManagerPhone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -80,6 +84,7 @@ export class UpdateProfileDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsValidPhone()
|
||||||
poaPhone?: string;
|
poaPhone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -151,5 +156,6 @@ export class UpdateProfileDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(20)
|
@MaxLength(20)
|
||||||
|
@IsValidPhone()
|
||||||
etradePhone?: string;
|
etradePhone?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"react-dom": "19.2.6",
|
"react-dom": "19.2.6",
|
||||||
"react-hook-form": "^7.76.0",
|
"react-hook-form": "^7.76.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"react-phone-number-input": "^3.4.17",
|
||||||
"react-router-dom": "^6.27.0",
|
"react-router-dom": "^6.27.0",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
|
|||||||
119
apps/edr-freight-web/portal/src/components/PhoneField.tsx
Normal file
119
apps/edr-freight-web/portal/src/components/PhoneField.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { Input } from "@mantine/core";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
type Control,
|
||||||
|
type FieldValues,
|
||||||
|
type Path,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import RPNInput, { isValidPhoneNumber } from "react-phone-number-input";
|
||||||
|
import "react-phone-number-input/style.css";
|
||||||
|
import "./phone-field.css";
|
||||||
|
|
||||||
|
/** Re-exported for zod `.refine()` checks on phone fields. */
|
||||||
|
export const isValidPhone = (value?: string | null): boolean =>
|
||||||
|
!!value && isValidPhoneNumber(value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text input rendered inside react-phone-number-input, styled to match the
|
||||||
|
* portal's Mantine fields (44px height, 10px radius, edr border). Must forward
|
||||||
|
* the ref and accept native input props for the library to drive it.
|
||||||
|
*/
|
||||||
|
const StyledInput = forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||||
|
function StyledInput(props, ref) {
|
||||||
|
return <input {...props} ref={ref} className="edr-phone-input" />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface PhoneFieldProps {
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange: (value: string | undefined) => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Professional phone input: searchable country selector (all countries, default
|
||||||
|
* Ethiopia), live formatting, emits a single E.164 value (e.g. +251912345678).
|
||||||
|
* Visually aligned with the portal's Mantine form fields.
|
||||||
|
*/
|
||||||
|
export function PhoneField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
error,
|
||||||
|
required,
|
||||||
|
disabled,
|
||||||
|
placeholder = "912 345 678",
|
||||||
|
}: PhoneFieldProps) {
|
||||||
|
return (
|
||||||
|
<Input.Wrapper
|
||||||
|
label={label}
|
||||||
|
required={required}
|
||||||
|
error={error}
|
||||||
|
styles={{
|
||||||
|
label: { fontWeight: 600, fontSize: 13, color: "#10202F", marginBottom: 6 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={`edr-phone-wrapper${error ? " edr-phone-wrapper--error" : ""}`}>
|
||||||
|
<RPNInput
|
||||||
|
international
|
||||||
|
defaultCountry="ET"
|
||||||
|
countryCallingCodeEditable={false}
|
||||||
|
addInternationalOption
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
|
inputComponent={StyledInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Input.Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ControlledPhoneFieldProps<T extends FieldValues> {
|
||||||
|
control: Control<T>;
|
||||||
|
name: Path<T>;
|
||||||
|
label?: string;
|
||||||
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RHF Controller wrapper so forms drop in one line. */
|
||||||
|
export function ControlledPhoneField<T extends FieldValues>({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
}: ControlledPhoneFieldProps<T>) {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<PhoneField
|
||||||
|
label={label}
|
||||||
|
required={required}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={(field.value as string) ?? ""}
|
||||||
|
onChange={(v) => field.onChange(v ?? "")}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhoneField;
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { Group, Stack, Text, TextInput, type TextInputProps } from "@mantine/core";
|
|
||||||
|
|
||||||
type InputPassthrough = Partial<TextInputProps>;
|
|
||||||
|
|
||||||
interface PhoneInputProps {
|
|
||||||
disabled?: boolean;
|
|
||||||
countryCode?: InputPassthrough;
|
|
||||||
phone?: InputPassthrough;
|
|
||||||
countryCodeError?: { message?: string };
|
|
||||||
phoneError?: { message?: string };
|
|
||||||
label?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PhoneInput({
|
|
||||||
disabled,
|
|
||||||
countryCode: countryCodeProps,
|
|
||||||
phone: phoneProps,
|
|
||||||
countryCodeError,
|
|
||||||
phoneError,
|
|
||||||
label = "Phone Number",
|
|
||||||
}: PhoneInputProps) {
|
|
||||||
const errorMsg = countryCodeError?.message ?? phoneError?.message;
|
|
||||||
return (
|
|
||||||
<Stack gap={6}>
|
|
||||||
<Text size="sm" fw={500} c="edr-text">{label}</Text>
|
|
||||||
<Group gap={8} wrap="nowrap" align="flex-start">
|
|
||||||
<TextInput
|
|
||||||
w={80}
|
|
||||||
disabled={disabled}
|
|
||||||
error={Boolean(countryCodeError)}
|
|
||||||
styles={{ input: { textAlign: "center" } }}
|
|
||||||
{...countryCodeProps}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
placeholder="912345678"
|
|
||||||
disabled={disabled}
|
|
||||||
error={Boolean(phoneError)}
|
|
||||||
{...phoneProps}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
{errorMsg && (
|
|
||||||
<Text size="xs" c="red.6">{errorMsg}</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
82
apps/edr-freight-web/portal/src/components/phone-field.css
Normal file
82
apps/edr-freight-web/portal/src/components/phone-field.css
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/* Align react-phone-number-input with the portal's Mantine field styling:
|
||||||
|
44px height, 10px radius, edr border, brand-green focus ring. */
|
||||||
|
|
||||||
|
.edr-phone-wrapper .PhoneInput {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Country selector — a compact pill matching the input height/radius. */
|
||||||
|
.edr-phone-wrapper .PhoneInputCountry {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid #e6ecf2;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition:
|
||||||
|
border-color 120ms ease,
|
||||||
|
box-shadow 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-phone-wrapper .PhoneInputCountryIcon {
|
||||||
|
width: 22px;
|
||||||
|
height: 16px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-phone-wrapper .PhoneInputCountrySelectArrow {
|
||||||
|
color: #6b7c8e;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The number input itself. */
|
||||||
|
.edr-phone-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #e6ecf2;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #10202f;
|
||||||
|
background: #fff;
|
||||||
|
outline: none;
|
||||||
|
transition:
|
||||||
|
border-color 120ms ease,
|
||||||
|
box-shadow 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-phone-input::placeholder {
|
||||||
|
color: #9aa8b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-phone-input:focus {
|
||||||
|
border-color: #0ea371;
|
||||||
|
box-shadow: 0 0 0 3px rgba(14, 163, 113, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-phone-wrapper .PhoneInputCountry:focus-within {
|
||||||
|
border-color: #0ea371;
|
||||||
|
box-shadow: 0 0 0 3px rgba(14, 163, 113, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-phone-input:disabled,
|
||||||
|
.edr-phone-wrapper .PhoneInputCountrySelect:disabled + .PhoneInputCountryIcon {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error state mirrors Mantine's invalid styling. */
|
||||||
|
.edr-phone-wrapper--error .edr-phone-input,
|
||||||
|
.edr-phone-wrapper--error .PhoneInputCountry {
|
||||||
|
border-color: #e03131;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-phone-wrapper--error .edr-phone-input:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(224, 49, 49, 0.12);
|
||||||
|
}
|
||||||
@@ -34,10 +34,9 @@ import type { AuthUser } from "@/types/auth";
|
|||||||
import type { CreateCompanyPayload } from "@/services/companies.service";
|
import type { CreateCompanyPayload } from "@/services/companies.service";
|
||||||
import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile";
|
import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile";
|
||||||
import type { CompanyRegistrationData } from "@edr/types";
|
import type { CompanyRegistrationData } from "@edr/types";
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
import { SmartFileInput } from "@edr/ui-common";
|
import { SmartFileInput } from "@edr/ui-common";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { splitPhone } from "@/utils/phone";
|
|
||||||
import RoleLicenseStep, {
|
import RoleLicenseStep, {
|
||||||
type RoleLicenseProfile,
|
type RoleLicenseProfile,
|
||||||
} from "@/components/onboarding/RoleLicenseStep";
|
} from "@/components/onboarding/RoleLicenseStep";
|
||||||
@@ -54,8 +53,10 @@ type CompanyStep =
|
|||||||
const onboardingSchema = z.object({
|
const onboardingSchema = z.object({
|
||||||
companyName: z.string().min(1, "Company name is required"),
|
companyName: z.string().min(1, "Company name is required"),
|
||||||
companyEmail: z.string().email("Invalid email address"),
|
companyEmail: z.string().email("Invalid email address"),
|
||||||
companyPhone: z.string().min(1, "Company phone is required"),
|
companyPhone: z
|
||||||
companyPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "Company phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
companyLocation: z.string().min(1, "Location is required"),
|
companyLocation: z.string().min(1, "Location is required"),
|
||||||
// Derived from the eTrade address parts (kebele/woreda/zone/region); no
|
// Derived from the eTrade address parts (kebele/woreda/zone/region); no
|
||||||
// standalone input — the granular fields live in the registration section.
|
// standalone input — the granular fields live in the registration section.
|
||||||
@@ -85,15 +86,21 @@ const onboardingSchema = z.object({
|
|||||||
.email("Invalid email address")
|
.email("Invalid email address")
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
contactPersonPhone: z.string().min(1, "Contact person phone is required"),
|
contactPersonPhone: z
|
||||||
contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "Contact person phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
generalManagerName: z.string().min(1, "GM name is required"),
|
generalManagerName: z.string().min(1, "GM name is required"),
|
||||||
generalManagerEmail: z.string().email("Invalid GM email"),
|
generalManagerEmail: z.string().email("Invalid GM email"),
|
||||||
generalManagerPhone: z.string().min(1, "GM phone is required"),
|
generalManagerPhone: z
|
||||||
generalManagerPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "GM phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
poaName: z.string().optional(),
|
poaName: z.string().optional(),
|
||||||
poaPhone: z.string().optional(),
|
poaPhone: z
|
||||||
poaPhoneCountryCode: z.string().optional(),
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((v) => !v || isValidPhone(v), "Enter a valid phone number"),
|
||||||
poaAddress: z.string().optional(),
|
poaAddress: z.string().optional(),
|
||||||
poaEmail: z.string().optional(),
|
poaEmail: z.string().optional(),
|
||||||
poaLocation: z.string().optional(),
|
poaLocation: z.string().optional(),
|
||||||
@@ -106,7 +113,6 @@ const stepFields: Record<CompanyStep, (keyof FormData)[]> = {
|
|||||||
"companyName",
|
"companyName",
|
||||||
"companyEmail",
|
"companyEmail",
|
||||||
"companyPhone",
|
"companyPhone",
|
||||||
"companyPhoneCountryCode",
|
|
||||||
"companyLocation",
|
"companyLocation",
|
||||||
"companyAddress",
|
"companyAddress",
|
||||||
"tinNumber",
|
"tinNumber",
|
||||||
@@ -129,14 +135,12 @@ const stepFields: Record<CompanyStep, (keyof FormData)[]> = {
|
|||||||
"generalManagerName",
|
"generalManagerName",
|
||||||
"generalManagerEmail",
|
"generalManagerEmail",
|
||||||
"generalManagerPhone",
|
"generalManagerPhone",
|
||||||
"generalManagerPhoneCountryCode",
|
|
||||||
],
|
],
|
||||||
contact: [
|
contact: [
|
||||||
"contactPersonName",
|
"contactPersonName",
|
||||||
"contactPersonPosition",
|
"contactPersonPosition",
|
||||||
"contactPersonEmail",
|
"contactPersonEmail",
|
||||||
"contactPersonPhone",
|
"contactPersonPhone",
|
||||||
"contactPersonPhoneCountryCode",
|
|
||||||
],
|
],
|
||||||
poa: [],
|
poa: [],
|
||||||
documents: [],
|
documents: [],
|
||||||
@@ -147,7 +151,7 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
|||||||
return {
|
return {
|
||||||
companyName: data.companyName,
|
companyName: data.companyName,
|
||||||
companyEmail: data.companyEmail,
|
companyEmail: data.companyEmail,
|
||||||
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
|
companyPhone: data.companyPhone,
|
||||||
companyLocation: data.companyLocation,
|
companyLocation: data.companyLocation,
|
||||||
companyAddress: data.companyAddress,
|
companyAddress: data.companyAddress,
|
||||||
tin: data.tinNumber,
|
tin: data.tinNumber,
|
||||||
@@ -157,15 +161,12 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
|||||||
contactPersonName: data.contactPersonName,
|
contactPersonName: data.contactPersonName,
|
||||||
contactPersonPosition: data.contactPersonPosition || undefined,
|
contactPersonPosition: data.contactPersonPosition || undefined,
|
||||||
contactPersonEmail: data.contactPersonEmail || undefined,
|
contactPersonEmail: data.contactPersonEmail || undefined,
|
||||||
contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
|
contactPersonPhone: data.contactPersonPhone,
|
||||||
generalManagerName: data.generalManagerName,
|
generalManagerName: data.generalManagerName,
|
||||||
generalManagerEmail: data.generalManagerEmail,
|
generalManagerEmail: data.generalManagerEmail,
|
||||||
generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
|
generalManagerPhone: data.generalManagerPhone,
|
||||||
poaName: data.poaName || undefined,
|
poaName: data.poaName || undefined,
|
||||||
poaPhone:
|
poaPhone: data.poaPhone || undefined,
|
||||||
data.poaPhone && data.poaPhoneCountryCode
|
|
||||||
? `${data.poaPhoneCountryCode}${data.poaPhone}`
|
|
||||||
: undefined,
|
|
||||||
poaAddress: data.poaAddress || undefined,
|
poaAddress: data.poaAddress || undefined,
|
||||||
poaEmail: data.poaEmail || undefined,
|
poaEmail: data.poaEmail || undefined,
|
||||||
poaLocation: data.poaLocation || undefined,
|
poaLocation: data.poaLocation || undefined,
|
||||||
@@ -180,7 +181,7 @@ function stepPayload(step: CompanyStep, d: FormData): Partial<UpdateProfilePaylo
|
|||||||
return {
|
return {
|
||||||
companyName: d.companyName,
|
companyName: d.companyName,
|
||||||
companyEmail: d.companyEmail,
|
companyEmail: d.companyEmail,
|
||||||
companyPhone: `${d.companyPhoneCountryCode}${d.companyPhone}`,
|
companyPhone: d.companyPhone,
|
||||||
companyLocation: d.companyLocation,
|
companyLocation: d.companyLocation,
|
||||||
companyAddress: d.companyAddress,
|
companyAddress: d.companyAddress,
|
||||||
tin: d.tinNumber,
|
tin: d.tinNumber,
|
||||||
@@ -203,22 +204,19 @@ function stepPayload(step: CompanyStep, d: FormData): Partial<UpdateProfilePaylo
|
|||||||
return {
|
return {
|
||||||
generalManagerName: d.generalManagerName,
|
generalManagerName: d.generalManagerName,
|
||||||
generalManagerEmail: d.generalManagerEmail,
|
generalManagerEmail: d.generalManagerEmail,
|
||||||
generalManagerPhone: `${d.generalManagerPhoneCountryCode}${d.generalManagerPhone}`,
|
generalManagerPhone: d.generalManagerPhone,
|
||||||
};
|
};
|
||||||
case "contact":
|
case "contact":
|
||||||
return {
|
return {
|
||||||
contactPersonName: d.contactPersonName,
|
contactPersonName: d.contactPersonName,
|
||||||
contactPersonPosition: d.contactPersonPosition || undefined,
|
contactPersonPosition: d.contactPersonPosition || undefined,
|
||||||
contactPersonEmail: d.contactPersonEmail || undefined,
|
contactPersonEmail: d.contactPersonEmail || undefined,
|
||||||
contactPersonPhone: `${d.contactPersonPhoneCountryCode}${d.contactPersonPhone}`,
|
contactPersonPhone: d.contactPersonPhone,
|
||||||
};
|
};
|
||||||
case "poa":
|
case "poa":
|
||||||
return {
|
return {
|
||||||
poaName: d.poaName || undefined,
|
poaName: d.poaName || undefined,
|
||||||
poaPhone:
|
poaPhone: d.poaPhone || undefined,
|
||||||
d.poaPhone && d.poaPhoneCountryCode
|
|
||||||
? `${d.poaPhoneCountryCode}${d.poaPhone}`
|
|
||||||
: undefined,
|
|
||||||
poaEmail: d.poaEmail || undefined,
|
poaEmail: d.poaEmail || undefined,
|
||||||
poaLocation: d.poaLocation || undefined,
|
poaLocation: d.poaLocation || undefined,
|
||||||
poaAddress: d.poaAddress || undefined,
|
poaAddress: d.poaAddress || undefined,
|
||||||
@@ -228,19 +226,14 @@ function stepPayload(step: CompanyStep, d: FormData): Partial<UpdateProfilePaylo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seed the form from previously-saved profile data (splitting combined phones). */
|
/** Seed the form from previously-saved profile data. */
|
||||||
function toFormValues(p: ProfileResponse): FormData {
|
function toFormValues(p: ProfileResponse): FormData {
|
||||||
const companyPhone = splitPhone(p.companyPhone);
|
|
||||||
const contactPhone = splitPhone(p.contactPersonPhone);
|
|
||||||
const gmPhone = splitPhone(p.generalManagerPhone);
|
|
||||||
const poaPhone = splitPhone(p.poaPhone);
|
|
||||||
// The draft placeholder TIN ("D…") shouldn't show as a real value.
|
// The draft placeholder TIN ("D…") shouldn't show as a real value.
|
||||||
const tin = p.tinNumber && !p.tinNumber.startsWith("D") ? p.tinNumber : "";
|
const tin = p.tinNumber && !p.tinNumber.startsWith("D") ? p.tinNumber : "";
|
||||||
return {
|
return {
|
||||||
companyName: p.companyName ?? "",
|
companyName: p.companyName ?? "",
|
||||||
companyEmail: p.companyEmail ?? "",
|
companyEmail: p.companyEmail ?? "",
|
||||||
companyPhone: companyPhone.number,
|
companyPhone: p.companyPhone ?? "",
|
||||||
companyPhoneCountryCode: companyPhone.countryCode,
|
|
||||||
companyLocation: p.companyLocation ?? "",
|
companyLocation: p.companyLocation ?? "",
|
||||||
companyAddress: p.companyAddress ?? "",
|
companyAddress: p.companyAddress ?? "",
|
||||||
tinNumber: tin,
|
tinNumber: tin,
|
||||||
@@ -261,15 +254,12 @@ function toFormValues(p: ProfileResponse): FormData {
|
|||||||
contactPersonName: p.contactPersonName ?? "",
|
contactPersonName: p.contactPersonName ?? "",
|
||||||
contactPersonPosition: p.contactPersonPosition ?? "",
|
contactPersonPosition: p.contactPersonPosition ?? "",
|
||||||
contactPersonEmail: p.contactPersonEmail ?? "",
|
contactPersonEmail: p.contactPersonEmail ?? "",
|
||||||
contactPersonPhone: contactPhone.number,
|
contactPersonPhone: p.contactPersonPhone ?? "",
|
||||||
contactPersonPhoneCountryCode: contactPhone.countryCode,
|
|
||||||
generalManagerName: p.generalManagerName ?? "",
|
generalManagerName: p.generalManagerName ?? "",
|
||||||
generalManagerEmail: p.generalManagerEmail ?? "",
|
generalManagerEmail: p.generalManagerEmail ?? "",
|
||||||
generalManagerPhone: gmPhone.number,
|
generalManagerPhone: p.generalManagerPhone ?? "",
|
||||||
generalManagerPhoneCountryCode: gmPhone.countryCode,
|
|
||||||
poaName: p.poaName ?? "",
|
poaName: p.poaName ?? "",
|
||||||
poaPhone: poaPhone.number,
|
poaPhone: p.poaPhone ?? "",
|
||||||
poaPhoneCountryCode: poaPhone.countryCode,
|
|
||||||
poaAddress: p.poaAddress ?? "",
|
poaAddress: p.poaAddress ?? "",
|
||||||
poaEmail: p.poaEmail ?? "",
|
poaEmail: p.poaEmail ?? "",
|
||||||
poaLocation: p.poaLocation ?? "",
|
poaLocation: p.poaLocation ?? "",
|
||||||
@@ -357,6 +347,7 @@ export default function CompanyProfileForm({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
trigger,
|
trigger,
|
||||||
watch,
|
watch,
|
||||||
@@ -368,7 +359,6 @@ export default function CompanyProfileForm({
|
|||||||
companyName: "",
|
companyName: "",
|
||||||
companyEmail: "",
|
companyEmail: "",
|
||||||
companyPhone: "",
|
companyPhone: "",
|
||||||
companyPhoneCountryCode: "+251",
|
|
||||||
companyLocation: "",
|
companyLocation: "",
|
||||||
companyAddress: "",
|
companyAddress: "",
|
||||||
tinNumber: "",
|
tinNumber: "",
|
||||||
@@ -390,14 +380,11 @@ export default function CompanyProfileForm({
|
|||||||
contactPersonPosition: "",
|
contactPersonPosition: "",
|
||||||
contactPersonEmail: "",
|
contactPersonEmail: "",
|
||||||
contactPersonPhone: "",
|
contactPersonPhone: "",
|
||||||
contactPersonPhoneCountryCode: "+251",
|
|
||||||
generalManagerName: "",
|
generalManagerName: "",
|
||||||
generalManagerEmail: "",
|
generalManagerEmail: "",
|
||||||
generalManagerPhone: "",
|
generalManagerPhone: "",
|
||||||
generalManagerPhoneCountryCode: "+251",
|
|
||||||
poaName: "",
|
poaName: "",
|
||||||
poaPhone: "",
|
poaPhone: "",
|
||||||
poaPhoneCountryCode: "+251",
|
|
||||||
poaAddress: "",
|
poaAddress: "",
|
||||||
poaEmail: "",
|
poaEmail: "",
|
||||||
poaLocation: "",
|
poaLocation: "",
|
||||||
@@ -447,9 +434,7 @@ export default function CompanyProfileForm({
|
|||||||
// Pre-fill the company contact phone from eTrade's mobile number.
|
// Pre-fill the company contact phone from eTrade's mobile number.
|
||||||
const mobile = data.mobilePhone || data.regularPhone;
|
const mobile = data.mobilePhone || data.regularPhone;
|
||||||
if (mobile) {
|
if (mobile) {
|
||||||
const { number, countryCode } = splitPhone(mobile);
|
setValue("companyPhone", mobile ?? "", { shouldValidate: true });
|
||||||
setValue("companyPhone", number);
|
|
||||||
setValue("companyPhoneCountryCode", countryCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setEtradeOwner({
|
setEtradeOwner({
|
||||||
@@ -462,9 +447,9 @@ export default function CompanyProfileForm({
|
|||||||
const useOwnerAsManager = () => {
|
const useOwnerAsManager = () => {
|
||||||
if (!etradeOwner) return;
|
if (!etradeOwner) return;
|
||||||
setValue("generalManagerName", etradeOwner.name);
|
setValue("generalManagerName", etradeOwner.name);
|
||||||
const { number, countryCode } = splitPhone(etradeOwner.phone);
|
setValue("generalManagerPhone", etradeOwner.phone ?? "", {
|
||||||
setValue("generalManagerPhone", number);
|
shouldValidate: true,
|
||||||
setValue("generalManagerPhoneCountryCode", countryCode);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Copy the General Manager into the Contact Person fields (toggleable). */
|
/** Copy the General Manager into the Contact Person fields (toggleable). */
|
||||||
@@ -474,10 +459,6 @@ export default function CompanyProfileForm({
|
|||||||
setValue("contactPersonName", watch("generalManagerName"));
|
setValue("contactPersonName", watch("generalManagerName"));
|
||||||
setValue("contactPersonEmail", watch("generalManagerEmail"));
|
setValue("contactPersonEmail", watch("generalManagerEmail"));
|
||||||
setValue("contactPersonPhone", watch("generalManagerPhone"));
|
setValue("contactPersonPhone", watch("generalManagerPhone"));
|
||||||
setValue(
|
|
||||||
"contactPersonPhoneCountryCode",
|
|
||||||
watch("generalManagerPhoneCountryCode"),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Copy the Contact Person into the PoA fields (toggleable, still editable). */
|
/** Copy the Contact Person into the PoA fields (toggleable, still editable). */
|
||||||
@@ -487,7 +468,6 @@ export default function CompanyProfileForm({
|
|||||||
setValue("poaName", watch("contactPersonName"));
|
setValue("poaName", watch("contactPersonName"));
|
||||||
setValue("poaEmail", watch("contactPersonEmail"));
|
setValue("poaEmail", watch("contactPersonEmail"));
|
||||||
setValue("poaPhone", watch("contactPersonPhone"));
|
setValue("poaPhone", watch("contactPersonPhone"));
|
||||||
setValue("poaPhoneCountryCode", watch("contactPersonPhoneCountryCode"));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasDocuments = Boolean(uploadSetting?.fields?.length);
|
const hasDocuments = Boolean(uploadSetting?.fields?.length);
|
||||||
@@ -663,15 +643,11 @@ export default function CompanyProfileForm({
|
|||||||
error={errors.companyEmail?.message}
|
error={errors.companyEmail?.message}
|
||||||
{...register("companyEmail")}
|
{...register("companyEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("companyPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{
|
name="companyPhone"
|
||||||
...register("companyPhone"),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
countryCodeError={errors.companyPhoneCountryCode}
|
|
||||||
phoneError={errors.companyPhone}
|
|
||||||
label="Company Phone"
|
label="Company Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -828,17 +804,11 @@ export default function CompanyProfileForm({
|
|||||||
error={errors.generalManagerEmail?.message}
|
error={errors.generalManagerEmail?.message}
|
||||||
{...register("generalManagerEmail")}
|
{...register("generalManagerEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{
|
control={control}
|
||||||
...register("generalManagerPhoneCountryCode"),
|
name="generalManagerPhone"
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register("generalManagerPhone"),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
countryCodeError={errors.generalManagerPhoneCountryCode}
|
|
||||||
phoneError={errors.generalManagerPhone}
|
|
||||||
label="Phone"
|
label="Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
@@ -877,15 +847,11 @@ export default function CompanyProfileForm({
|
|||||||
error={errors.contactPersonEmail?.message}
|
error={errors.contactPersonEmail?.message}
|
||||||
{...register("contactPersonEmail")}
|
{...register("contactPersonEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("contactPersonPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{
|
name="contactPersonPhone"
|
||||||
...register("contactPersonPhone"),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
countryCodeError={errors.contactPersonPhoneCountryCode}
|
|
||||||
phoneError={errors.contactPersonPhone}
|
|
||||||
label="Phone"
|
label="Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
@@ -917,11 +883,9 @@ export default function CompanyProfileForm({
|
|||||||
error={errors.poaEmail?.message}
|
error={errors.poaEmail?.message}
|
||||||
{...register("poaEmail")}
|
{...register("poaEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("poaPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("poaPhone"), placeholder: "912345678" }}
|
name="poaPhone"
|
||||||
countryCodeError={errors.poaPhoneCountryCode}
|
|
||||||
phoneError={errors.poaPhone}
|
|
||||||
label="PoA Phone"
|
label="PoA Phone"
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import type { AuthUser } from "@/types/auth";
|
import type { AuthUser } from "@/types/auth";
|
||||||
import type { CreateCompanyPayload } from "@/services/companies.service";
|
import type { CreateCompanyPayload } from "@/services/companies.service";
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
import { SmartFileInput } from "@edr/ui-common";
|
import { SmartFileInput } from "@edr/ui-common";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
|
|
||||||
@@ -25,21 +25,25 @@ type DjiboutiStep = "company" | "representative" | "documents" | "confirm";
|
|||||||
const djiboutiSchema = z.object({
|
const djiboutiSchema = z.object({
|
||||||
companyName: z.string().min(1, "Company name is required"),
|
companyName: z.string().min(1, "Company name is required"),
|
||||||
companyEmail: z.string().email("Invalid email address"),
|
companyEmail: z.string().email("Invalid email address"),
|
||||||
companyPhone: z.string().min(1, "Company phone is required"),
|
companyPhone: z
|
||||||
companyPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "Company phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
companyLocation: z.string().min(1, "Location / Country is required"),
|
companyLocation: z.string().min(1, "Location / Country is required"),
|
||||||
companyAddress: z.string().min(1, "Address is required"),
|
companyAddress: z.string().min(1, "Address is required"),
|
||||||
repName: z.string().min(1, "Representative name is required"),
|
repName: z.string().min(1, "Representative name is required"),
|
||||||
repEmail: z.string().email("Invalid representative email"),
|
repEmail: z.string().email("Invalid representative email"),
|
||||||
repPhone: z.string().min(1, "Representative phone is required"),
|
repPhone: z
|
||||||
repPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "Representative phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof djiboutiSchema>;
|
type FormData = z.infer<typeof djiboutiSchema>;
|
||||||
|
|
||||||
const stepFields: Record<DjiboutiStep, (keyof FormData)[]> = {
|
const stepFields: Record<DjiboutiStep, (keyof FormData)[]> = {
|
||||||
company: ["companyName", "companyEmail", "companyPhone", "companyPhoneCountryCode", "companyLocation", "companyAddress"],
|
company: ["companyName", "companyEmail", "companyPhone", "companyLocation", "companyAddress"],
|
||||||
representative: ["repName", "repEmail", "repPhone", "repPhoneCountryCode"],
|
representative: ["repName", "repEmail", "repPhone"],
|
||||||
documents: [],
|
documents: [],
|
||||||
confirm: [],
|
confirm: [],
|
||||||
};
|
};
|
||||||
@@ -48,7 +52,7 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
|||||||
return {
|
return {
|
||||||
companyName: data.companyName,
|
companyName: data.companyName,
|
||||||
companyEmail: data.companyEmail,
|
companyEmail: data.companyEmail,
|
||||||
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
|
companyPhone: data.companyPhone,
|
||||||
companyLocation: data.companyLocation,
|
companyLocation: data.companyLocation,
|
||||||
companyAddress: data.companyAddress,
|
companyAddress: data.companyAddress,
|
||||||
tin: "",
|
tin: "",
|
||||||
@@ -57,7 +61,7 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
|||||||
attributes: {
|
attributes: {
|
||||||
repName: data.repName,
|
repName: data.repName,
|
||||||
repEmail: data.repEmail,
|
repEmail: data.repEmail,
|
||||||
repPhone: `${data.repPhoneCountryCode}${data.repPhone}`,
|
repPhone: data.repPhone,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -88,11 +92,11 @@ export default function DjiboutiAgentForm({
|
|||||||
api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }),
|
api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { register, handleSubmit, trigger, watch, formState: { errors } } = useForm<FormData>({
|
const { register, control, handleSubmit, trigger, watch, formState: { errors } } = useForm<FormData>({
|
||||||
resolver: zodResolver(djiboutiSchema),
|
resolver: zodResolver(djiboutiSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
companyName: "", companyEmail: "", companyPhone: "", companyPhoneCountryCode: "+253",
|
companyName: "", companyEmail: "", companyPhone: "",
|
||||||
companyLocation: "", companyAddress: "", repName: "", repEmail: "", repPhone: "", repPhoneCountryCode: "+253",
|
companyLocation: "", companyAddress: "", repName: "", repEmail: "", repPhone: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -195,12 +199,11 @@ export default function DjiboutiAgentForm({
|
|||||||
error={errors.companyEmail?.message}
|
error={errors.companyEmail?.message}
|
||||||
{...register("companyEmail")}
|
{...register("companyEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("companyPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("companyPhone"), placeholder: "12345678" }}
|
name="companyPhone"
|
||||||
countryCodeError={errors.companyPhoneCountryCode}
|
|
||||||
phoneError={errors.companyPhone}
|
|
||||||
label="Company Phone"
|
label="Company Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
@@ -239,12 +242,11 @@ export default function DjiboutiAgentForm({
|
|||||||
error={errors.repEmail?.message}
|
error={errors.repEmail?.message}
|
||||||
{...register("repEmail")}
|
{...register("repEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("repPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("repPhone"), placeholder: "12345678" }}
|
name="repPhone"
|
||||||
countryCodeError={errors.repPhoneCountryCode}
|
|
||||||
phoneError={errors.repPhone}
|
|
||||||
label="Representative Phone"
|
label="Representative Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
@@ -280,7 +282,7 @@ export default function DjiboutiAgentForm({
|
|||||||
<ReviewRow label="Address" value={formValues.companyAddress} />
|
<ReviewRow label="Address" value={formValues.companyAddress} />
|
||||||
<ReviewRow label="Rep. name" value={formValues.repName} />
|
<ReviewRow label="Rep. name" value={formValues.repName} />
|
||||||
<ReviewRow label="Rep. email" value={formValues.repEmail} />
|
<ReviewRow label="Rep. email" value={formValues.repEmail} />
|
||||||
<ReviewRow label="Rep. phone" value={`${formValues.repPhoneCountryCode}${formValues.repPhone}`} />
|
<ReviewRow label="Rep. phone" value={formValues.repPhone} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ import { z } from "zod";
|
|||||||
import type { AuthUser } from "@/types/auth";
|
import type { AuthUser } from "@/types/auth";
|
||||||
import type { CreateCompanyPayload } from "@/services/companies.service";
|
import type { CreateCompanyPayload } from "@/services/companies.service";
|
||||||
import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile";
|
import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile";
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
import { SmartFileInput } from "@edr/ui-common";
|
import { SmartFileInput } from "@edr/ui-common";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { splitPhone } from "@/utils/phone";
|
|
||||||
import RoleLicenseStep, {
|
import RoleLicenseStep, {
|
||||||
type RoleLicenseProfile,
|
type RoleLicenseProfile,
|
||||||
} from "@/components/onboarding/RoleLicenseStep";
|
} from "@/components/onboarding/RoleLicenseStep";
|
||||||
@@ -32,23 +31,31 @@ type ForwarderStep = "company" | "personnel" | "poa" | "documents" | "additional
|
|||||||
const forwarderSchema = z.object({
|
const forwarderSchema = z.object({
|
||||||
companyName: z.string().min(1, "Company name is required"),
|
companyName: z.string().min(1, "Company name is required"),
|
||||||
companyEmail: z.string().email("Invalid email address"),
|
companyEmail: z.string().email("Invalid email address"),
|
||||||
companyPhone: z.string().min(1, "Company phone is required"),
|
companyPhone: z
|
||||||
companyPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "Company phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
companyLocation: z.string().min(1, "Location is required"),
|
companyLocation: z.string().min(1, "Location is required"),
|
||||||
companyAddress: z.string().min(1, "Address is required"),
|
companyAddress: z.string().min(1, "Address is required"),
|
||||||
tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
|
tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
|
||||||
vatNumber: z.string().min(1, "VAT number is required").length(10, "VAT number must be exactly 10 digits"),
|
vatNumber: z.string().min(1, "VAT number is required").length(10, "VAT number must be exactly 10 digits"),
|
||||||
fanNumber: z.string().length(16, "FAN must be exactly 16 digits"),
|
fanNumber: z.string().length(16, "FAN must be exactly 16 digits"),
|
||||||
contactPersonName: z.string().min(1, "Contact person name is required"),
|
contactPersonName: z.string().min(1, "Contact person name is required"),
|
||||||
contactPersonPhone: z.string().min(1, "Contact person phone is required"),
|
contactPersonPhone: z
|
||||||
contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "Contact person phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
generalManagerName: z.string().min(1, "GM name is required"),
|
generalManagerName: z.string().min(1, "GM name is required"),
|
||||||
generalManagerEmail: z.string().email("Invalid GM email"),
|
generalManagerEmail: z.string().email("Invalid GM email"),
|
||||||
generalManagerPhone: z.string().min(1, "GM phone is required"),
|
generalManagerPhone: z
|
||||||
generalManagerPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "GM phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
poaName: z.string().optional(),
|
poaName: z.string().optional(),
|
||||||
poaPhone: z.string().optional(),
|
poaPhone: z
|
||||||
poaPhoneCountryCode: z.string().optional(),
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((v) => !v || isValidPhone(v), "Enter a valid phone number"),
|
||||||
poaAddress: z.string().optional(),
|
poaAddress: z.string().optional(),
|
||||||
poaEmail: z.string().optional(),
|
poaEmail: z.string().optional(),
|
||||||
poaLocation: z.string().optional(),
|
poaLocation: z.string().optional(),
|
||||||
@@ -57,8 +64,8 @@ const forwarderSchema = z.object({
|
|||||||
type FormData = z.infer<typeof forwarderSchema>;
|
type FormData = z.infer<typeof forwarderSchema>;
|
||||||
|
|
||||||
const stepFields: Record<ForwarderStep, (keyof FormData)[]> = {
|
const stepFields: Record<ForwarderStep, (keyof FormData)[]> = {
|
||||||
company: ["companyName", "companyEmail", "companyPhone", "companyPhoneCountryCode", "companyLocation", "companyAddress", "tinNumber", "vatNumber", "fanNumber"],
|
company: ["companyName", "companyEmail", "companyPhone", "companyLocation", "companyAddress", "tinNumber", "vatNumber", "fanNumber"],
|
||||||
personnel: ["contactPersonName", "contactPersonPhone", "contactPersonPhoneCountryCode", "generalManagerName", "generalManagerEmail", "generalManagerPhone", "generalManagerPhoneCountryCode"],
|
personnel: ["contactPersonName", "contactPersonPhone", "generalManagerName", "generalManagerEmail", "generalManagerPhone"],
|
||||||
poa: [],
|
poa: [],
|
||||||
documents: [],
|
documents: [],
|
||||||
additional: [],
|
additional: [],
|
||||||
@@ -68,7 +75,7 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
|||||||
return {
|
return {
|
||||||
companyName: data.companyName,
|
companyName: data.companyName,
|
||||||
companyEmail: data.companyEmail,
|
companyEmail: data.companyEmail,
|
||||||
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
|
companyPhone: data.companyPhone,
|
||||||
companyLocation: data.companyLocation,
|
companyLocation: data.companyLocation,
|
||||||
companyAddress: data.companyAddress,
|
companyAddress: data.companyAddress,
|
||||||
tin: data.tinNumber,
|
tin: data.tinNumber,
|
||||||
@@ -76,12 +83,12 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
|||||||
fanNumber: data.fanNumber,
|
fanNumber: data.fanNumber,
|
||||||
attributes: {
|
attributes: {
|
||||||
contactPersonName: data.contactPersonName,
|
contactPersonName: data.contactPersonName,
|
||||||
contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
|
contactPersonPhone: data.contactPersonPhone,
|
||||||
generalManagerName: data.generalManagerName,
|
generalManagerName: data.generalManagerName,
|
||||||
generalManagerEmail: data.generalManagerEmail,
|
generalManagerEmail: data.generalManagerEmail,
|
||||||
generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
|
generalManagerPhone: data.generalManagerPhone,
|
||||||
poaName: data.poaName || undefined,
|
poaName: data.poaName || undefined,
|
||||||
poaPhone: data.poaPhone && data.poaPhoneCountryCode ? `${data.poaPhoneCountryCode}${data.poaPhone}` : undefined,
|
poaPhone: data.poaPhone || undefined,
|
||||||
poaAddress: data.poaAddress || undefined,
|
poaAddress: data.poaAddress || undefined,
|
||||||
poaEmail: data.poaEmail || undefined,
|
poaEmail: data.poaEmail || undefined,
|
||||||
poaLocation: data.poaLocation || undefined,
|
poaLocation: data.poaLocation || undefined,
|
||||||
@@ -96,7 +103,7 @@ function stepPayload(step: ForwarderStep, d: FormData): Partial<UpdateProfilePay
|
|||||||
return {
|
return {
|
||||||
companyName: d.companyName,
|
companyName: d.companyName,
|
||||||
companyEmail: d.companyEmail,
|
companyEmail: d.companyEmail,
|
||||||
companyPhone: `${d.companyPhoneCountryCode}${d.companyPhone}`,
|
companyPhone: d.companyPhone,
|
||||||
companyLocation: d.companyLocation,
|
companyLocation: d.companyLocation,
|
||||||
companyAddress: d.companyAddress,
|
companyAddress: d.companyAddress,
|
||||||
tin: d.tinNumber,
|
tin: d.tinNumber,
|
||||||
@@ -106,18 +113,15 @@ function stepPayload(step: ForwarderStep, d: FormData): Partial<UpdateProfilePay
|
|||||||
case "personnel":
|
case "personnel":
|
||||||
return {
|
return {
|
||||||
contactPersonName: d.contactPersonName,
|
contactPersonName: d.contactPersonName,
|
||||||
contactPersonPhone: `${d.contactPersonPhoneCountryCode}${d.contactPersonPhone}`,
|
contactPersonPhone: d.contactPersonPhone,
|
||||||
generalManagerName: d.generalManagerName,
|
generalManagerName: d.generalManagerName,
|
||||||
generalManagerEmail: d.generalManagerEmail,
|
generalManagerEmail: d.generalManagerEmail,
|
||||||
generalManagerPhone: `${d.generalManagerPhoneCountryCode}${d.generalManagerPhone}`,
|
generalManagerPhone: d.generalManagerPhone,
|
||||||
};
|
};
|
||||||
case "poa":
|
case "poa":
|
||||||
return {
|
return {
|
||||||
poaName: d.poaName || undefined,
|
poaName: d.poaName || undefined,
|
||||||
poaPhone:
|
poaPhone: d.poaPhone || undefined,
|
||||||
d.poaPhone && d.poaPhoneCountryCode
|
|
||||||
? `${d.poaPhoneCountryCode}${d.poaPhone}`
|
|
||||||
: undefined,
|
|
||||||
poaEmail: d.poaEmail || undefined,
|
poaEmail: d.poaEmail || undefined,
|
||||||
poaLocation: d.poaLocation || undefined,
|
poaLocation: d.poaLocation || undefined,
|
||||||
poaAddress: d.poaAddress || undefined,
|
poaAddress: d.poaAddress || undefined,
|
||||||
@@ -127,33 +131,25 @@ function stepPayload(step: ForwarderStep, d: FormData): Partial<UpdateProfilePay
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seed the form from previously-saved profile data (splitting combined phones). */
|
/** Seed the form from previously-saved profile data. */
|
||||||
function toFormValues(p: ProfileResponse): FormData {
|
function toFormValues(p: ProfileResponse): FormData {
|
||||||
const companyPhone = splitPhone(p.companyPhone);
|
|
||||||
const contactPhone = splitPhone(p.contactPersonPhone);
|
|
||||||
const gmPhone = splitPhone(p.generalManagerPhone);
|
|
||||||
const poaPhone = splitPhone(p.poaPhone);
|
|
||||||
const tin = p.tinNumber && !p.tinNumber.startsWith("D") ? p.tinNumber : "";
|
const tin = p.tinNumber && !p.tinNumber.startsWith("D") ? p.tinNumber : "";
|
||||||
return {
|
return {
|
||||||
companyName: p.companyName ?? "",
|
companyName: p.companyName ?? "",
|
||||||
companyEmail: p.companyEmail ?? "",
|
companyEmail: p.companyEmail ?? "",
|
||||||
companyPhone: companyPhone.number,
|
companyPhone: p.companyPhone ?? "",
|
||||||
companyPhoneCountryCode: companyPhone.countryCode,
|
|
||||||
companyLocation: p.companyLocation ?? "",
|
companyLocation: p.companyLocation ?? "",
|
||||||
companyAddress: p.companyAddress ?? "",
|
companyAddress: p.companyAddress ?? "",
|
||||||
tinNumber: tin,
|
tinNumber: tin,
|
||||||
vatNumber: p.vatNumber ?? "",
|
vatNumber: p.vatNumber ?? "",
|
||||||
fanNumber: p.fanNumber ?? "",
|
fanNumber: p.fanNumber ?? "",
|
||||||
contactPersonName: p.contactPersonName ?? "",
|
contactPersonName: p.contactPersonName ?? "",
|
||||||
contactPersonPhone: contactPhone.number,
|
contactPersonPhone: p.contactPersonPhone ?? "",
|
||||||
contactPersonPhoneCountryCode: contactPhone.countryCode,
|
|
||||||
generalManagerName: p.generalManagerName ?? "",
|
generalManagerName: p.generalManagerName ?? "",
|
||||||
generalManagerEmail: p.generalManagerEmail ?? "",
|
generalManagerEmail: p.generalManagerEmail ?? "",
|
||||||
generalManagerPhone: gmPhone.number,
|
generalManagerPhone: p.generalManagerPhone ?? "",
|
||||||
generalManagerPhoneCountryCode: gmPhone.countryCode,
|
|
||||||
poaName: p.poaName ?? "",
|
poaName: p.poaName ?? "",
|
||||||
poaPhone: poaPhone.number,
|
poaPhone: p.poaPhone ?? "",
|
||||||
poaPhoneCountryCode: poaPhone.countryCode,
|
|
||||||
poaAddress: p.poaAddress ?? "",
|
poaAddress: p.poaAddress ?? "",
|
||||||
poaEmail: p.poaEmail ?? "",
|
poaEmail: p.poaEmail ?? "",
|
||||||
poaLocation: p.poaLocation ?? "",
|
poaLocation: p.poaLocation ?? "",
|
||||||
@@ -233,14 +229,14 @@ export default function ForwarderForm({
|
|||||||
api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }),
|
api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { register, handleSubmit, trigger, watch, formState: { errors } } = useForm<FormData>({
|
const { register, control, handleSubmit, trigger, watch, formState: { errors } } = useForm<FormData>({
|
||||||
resolver: zodResolver(forwarderSchema),
|
resolver: zodResolver(forwarderSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
companyName: "", companyEmail: "", companyPhone: "", companyPhoneCountryCode: "+251",
|
companyName: "", companyEmail: "", companyPhone: "",
|
||||||
companyLocation: "", companyAddress: "", tinNumber: "", vatNumber: "", fanNumber: "",
|
companyLocation: "", companyAddress: "", tinNumber: "", vatNumber: "", fanNumber: "",
|
||||||
contactPersonName: "", contactPersonPhone: "", contactPersonPhoneCountryCode: "+251",
|
contactPersonName: "", contactPersonPhone: "",
|
||||||
generalManagerName: "", generalManagerEmail: "", generalManagerPhone: "", generalManagerPhoneCountryCode: "+251",
|
generalManagerName: "", generalManagerEmail: "", generalManagerPhone: "",
|
||||||
poaName: "", poaPhone: "", poaPhoneCountryCode: "+251", poaAddress: "", poaEmail: "", poaLocation: "",
|
poaName: "", poaPhone: "", poaAddress: "", poaEmail: "", poaLocation: "",
|
||||||
},
|
},
|
||||||
// Rehydrate from previously-saved data (RHF re-syncs when `values` change).
|
// Rehydrate from previously-saved data (RHF re-syncs when `values` change).
|
||||||
values: rehydrate ? toFormValues(rehydrate) : undefined,
|
values: rehydrate ? toFormValues(rehydrate) : undefined,
|
||||||
@@ -383,12 +379,11 @@ export default function ForwarderForm({
|
|||||||
error={errors.companyEmail?.message}
|
error={errors.companyEmail?.message}
|
||||||
{...register("companyEmail")}
|
{...register("companyEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("companyPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("companyPhone"), placeholder: "912345678" }}
|
name="companyPhone"
|
||||||
countryCodeError={errors.companyPhoneCountryCode}
|
|
||||||
phoneError={errors.companyPhone}
|
|
||||||
label="Company Phone"
|
label="Company Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
@@ -441,12 +436,11 @@ export default function ForwarderForm({
|
|||||||
error={errors.contactPersonName?.message}
|
error={errors.contactPersonName?.message}
|
||||||
{...register("contactPersonName")}
|
{...register("contactPersonName")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("contactPersonPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("contactPersonPhone"), placeholder: "912345678" }}
|
name="contactPersonPhone"
|
||||||
countryCodeError={errors.contactPersonPhoneCountryCode}
|
|
||||||
phoneError={errors.contactPersonPhone}
|
|
||||||
label="Phone"
|
label="Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
@@ -467,12 +461,11 @@ export default function ForwarderForm({
|
|||||||
error={errors.generalManagerEmail?.message}
|
error={errors.generalManagerEmail?.message}
|
||||||
{...register("generalManagerEmail")}
|
{...register("generalManagerEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("generalManagerPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("generalManagerPhone"), placeholder: "912345678" }}
|
name="generalManagerPhone"
|
||||||
countryCodeError={errors.generalManagerPhoneCountryCode}
|
|
||||||
phoneError={errors.generalManagerPhone}
|
|
||||||
label="Phone"
|
label="Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
@@ -497,11 +490,9 @@ export default function ForwarderForm({
|
|||||||
error={errors.poaEmail?.message}
|
error={errors.poaEmail?.message}
|
||||||
{...register("poaEmail")}
|
{...register("poaEmail")}
|
||||||
/>
|
/>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("poaPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("poaPhone"), placeholder: "912345678" }}
|
name="poaPhone"
|
||||||
countryCodeError={errors.poaPhoneCountryCode}
|
|
||||||
phoneError={errors.poaPhone}
|
|
||||||
label="PoA Phone"
|
label="PoA Phone"
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useState } from "react";
|
||||||
import { ChevronDown, Eye, EyeOff, Mail, Smartphone } from "lucide-react";
|
import { ChevronDown, Eye, EyeOff, Mail, Smartphone } from "lucide-react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import RPNInput from "react-phone-number-input";
|
||||||
|
import "react-phone-number-input/style.css";
|
||||||
|
|
||||||
import useAuth from "@/hooks/useAuth";
|
import useAuth from "@/hooks/useAuth";
|
||||||
import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
|
import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
|
||||||
|
import "@/components/phone-field.css";
|
||||||
|
|
||||||
const EDR_LOGO = "/assets/edr-logo.png";
|
const EDR_LOGO = "/assets/edr-logo.png";
|
||||||
|
|
||||||
@@ -25,7 +28,6 @@ export default function LoginPage() {
|
|||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const [method, setMethod] = useState<LoginMethod>("email");
|
const [method, setMethod] = useState<LoginMethod>("email");
|
||||||
const [identifier, setIdentifier] = useState("");
|
const [identifier, setIdentifier] = useState("");
|
||||||
const [countryCode] = useState("+251");
|
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -38,11 +40,9 @@ export default function LoginPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const loginId =
|
// In phone mode the identifier is already a canonical E.164 string
|
||||||
method === "email"
|
// (e.g. +251912345678) from the phone field; email mode passes through.
|
||||||
? identifier
|
const result = await login({ email: identifier, password });
|
||||||
: `${countryCode}${identifier.startsWith("0") ? identifier.slice(1) : identifier}`;
|
|
||||||
const result = await login({ email: loginId, password });
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const from = (location.state as { from?: { pathname: string } } | null)?.from
|
const from = (location.state as { from?: { pathname: string } } | null)?.from
|
||||||
?.pathname;
|
?.pathname;
|
||||||
@@ -79,7 +79,10 @@ export default function LoginPage() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
value={method}
|
value={method}
|
||||||
onChange={(event) => setMethod(event.target.value as LoginMethod)}
|
onChange={(event) => {
|
||||||
|
setMethod(event.target.value as LoginMethod);
|
||||||
|
setIdentifier("");
|
||||||
|
}}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={`${fieldClass} appearance-none pr-10`}
|
className={`${fieldClass} appearance-none pr-10`}
|
||||||
>
|
>
|
||||||
@@ -97,13 +100,29 @@ export default function LoginPage() {
|
|||||||
<label className="text-sm font-medium text-gray-800">
|
<label className="text-sm font-medium text-gray-800">
|
||||||
{currentMethod.label} <span className="text-red-500">*</span>
|
{currentMethod.label} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
{method === "phone" ? (
|
||||||
value={identifier}
|
<div className="edr-phone-wrapper">
|
||||||
onChange={(event) => setIdentifier(event.target.value)}
|
<RPNInput
|
||||||
placeholder={currentMethod.placeholder}
|
international
|
||||||
disabled={loading}
|
defaultCountry="ET"
|
||||||
className={fieldClass}
|
countryCallingCodeEditable={false}
|
||||||
/>
|
addInternationalOption
|
||||||
|
placeholder="912 345 678"
|
||||||
|
disabled={loading}
|
||||||
|
value={identifier || undefined}
|
||||||
|
onChange={(v) => setIdentifier(v ?? "")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={identifier}
|
||||||
|
onChange={(event) => setIdentifier(event.target.value)}
|
||||||
|
placeholder={currentMethod.placeholder}
|
||||||
|
disabled={loading}
|
||||||
|
className={fieldClass}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { ArrowRight, Check, Eye, EyeOff, X } from "lucide-react";
|
import { ArrowRight, Check, Eye, EyeOff, X } from "lucide-react";
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import RPNInput from "react-phone-number-input";
|
||||||
|
import "react-phone-number-input/style.css";
|
||||||
|
|
||||||
import { userType } from "@/enums/userType";
|
import { userType } from "@/enums/userType";
|
||||||
import useAuth from "@/hooks/useAuth";
|
import useAuth from "@/hooks/useAuth";
|
||||||
import type { SignupPayload } from "@/types/auth";
|
import type { SignupPayload } from "@/types/auth";
|
||||||
import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
|
import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
|
||||||
|
import { isValidPhone } from "@/components/PhoneField";
|
||||||
|
import "@/components/phone-field.css";
|
||||||
|
|
||||||
const EDR_LOGO = "/assets/edr-logo.png";
|
const EDR_LOGO = "/assets/edr-logo.png";
|
||||||
|
|
||||||
@@ -20,22 +24,13 @@ const passwordRequirements = [
|
|||||||
{ label: "One special character", test: (v: string) => /[^A-Za-z0-9]/.test(v) },
|
{ label: "One special character", test: (v: string) => /[^A-Za-z0-9]/.test(v) },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const ETHIOPIA_COUNTRY_CODE = "+251";
|
|
||||||
|
|
||||||
const isValidEthiopianMobile = (value: string) => {
|
|
||||||
const digits = value.replace(/\D/g, "");
|
|
||||||
const normalized = digits.startsWith("0") ? digits.slice(1) : digits;
|
|
||||||
return /^9\d{8}$/.test(normalized);
|
|
||||||
};
|
|
||||||
|
|
||||||
const userSchema = z
|
const userSchema = z
|
||||||
.object({
|
.object({
|
||||||
email: z.string().email("Invalid email address"),
|
email: z.string().email("Invalid email address"),
|
||||||
countryCode: z.literal(ETHIOPIA_COUNTRY_CODE),
|
|
||||||
phone: z
|
phone: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Phone number is required")
|
.min(1, "Phone number is required")
|
||||||
.refine(isValidEthiopianMobile, "Enter a valid mobile number (e.g. 0912345678)"),
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
userType: z.string(),
|
userType: z.string(),
|
||||||
firstName: z.object({ en: z.string().min(2, "Name is required"), am: z.string().nullable() }),
|
firstName: z.object({ en: z.string().min(2, "Name is required"), am: z.string().nullable() }),
|
||||||
lastName: z.object({ en: z.string().min(2, "Name is required"), am: z.string().nullable() }),
|
lastName: z.object({ en: z.string().min(2, "Name is required"), am: z.string().nullable() }),
|
||||||
@@ -70,12 +65,12 @@ export default function SignupPage() {
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(userSchema),
|
resolver: zodResolver(userSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
countryCode: ETHIOPIA_COUNTRY_CODE,
|
|
||||||
phone: "",
|
phone: "",
|
||||||
userType: userType.individual,
|
userType: userType.individual,
|
||||||
firstName: { en: "", am: "" },
|
firstName: { en: "", am: "" },
|
||||||
@@ -89,12 +84,11 @@ export default function SignupPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const digits = data.phone.replace(/\D/g, "");
|
|
||||||
const normalizedPhone = digits.startsWith("0") ? digits.slice(1) : digits;
|
|
||||||
const payload: SignupPayload = {
|
const payload: SignupPayload = {
|
||||||
email: data.email,
|
email: data.email,
|
||||||
username: data.email,
|
username: data.email,
|
||||||
phoneNumber: `${data.countryCode}${normalizedPhone}`,
|
// Already a canonical E.164 string from the phone field (e.g. +251912345678).
|
||||||
|
phoneNumber: data.phone,
|
||||||
userType: data.userType,
|
userType: data.userType,
|
||||||
name: {
|
name: {
|
||||||
en: `${data.firstName.en} ${data.lastName.en}`,
|
en: `${data.firstName.en} ${data.lastName.en}`,
|
||||||
@@ -183,31 +177,30 @@ export default function SignupPage() {
|
|||||||
<label htmlFor="signup-phone" className="text-sm font-medium text-gray-800">
|
<label htmlFor="signup-phone" className="text-sm font-medium text-gray-800">
|
||||||
Phone <span className="text-red-500">*</span>
|
Phone <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="hidden" {...register("countryCode")} />
|
<Controller
|
||||||
<div
|
control={control}
|
||||||
className={`flex overflow-hidden rounded-xl border bg-white shadow-sm transition-all duration-200 hover:border-gray-300 focus-within:border-primary focus-within:ring-4 focus-within:ring-primary/10 ${
|
name="phone"
|
||||||
errors.phone ? "border-red-300 focus-within:border-red-400 focus-within:ring-red-100" : "border-gray-200/90"
|
render={({ field }) => (
|
||||||
}`}
|
<div
|
||||||
>
|
className={`edr-phone-wrapper${
|
||||||
<span className="flex h-11 shrink-0 items-center border-r border-gray-200/90 bg-gray-50 px-3 text-sm font-medium text-gray-600">
|
errors.phone ? " edr-phone-wrapper--error" : ""
|
||||||
{ETHIOPIA_COUNTRY_CODE}
|
}`}
|
||||||
</span>
|
>
|
||||||
<input
|
<RPNInput
|
||||||
id="signup-phone"
|
international
|
||||||
type="tel"
|
defaultCountry="ET"
|
||||||
inputMode="numeric"
|
countryCallingCodeEditable={false}
|
||||||
autoComplete="tel-national"
|
addInternationalOption
|
||||||
placeholder="0912345678"
|
id="signup-phone"
|
||||||
maxLength={10}
|
placeholder="912 345 678"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="h-11 min-w-0 flex-1 border-0 bg-transparent px-4 text-sm text-gray-900 outline-none placeholder:text-gray-400"
|
value={field.value || undefined}
|
||||||
{...register("phone", {
|
onChange={(v) => field.onChange(v ?? "")}
|
||||||
onChange: (event) => {
|
onBlur={field.onBlur}
|
||||||
event.target.value = event.target.value.replace(/\D/g, "").slice(0, 10);
|
/>
|
||||||
},
|
</div>
|
||||||
})}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{errorText(errors.phone?.message)}
|
{errorText(errors.phone?.message)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -37,9 +37,8 @@ const schema = z.object({
|
|||||||
|
|
||||||
phoneNumber: z
|
phoneNumber: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Phone number is required"),
|
.min(1, "Phone number is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
phoneCountryCode: z.string().min(1),
|
|
||||||
|
|
||||||
// COMPANY
|
// COMPANY
|
||||||
companyName: z
|
companyName: z
|
||||||
@@ -52,9 +51,8 @@ const schema = z.object({
|
|||||||
|
|
||||||
companyPhone: z
|
companyPhone: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Company phone is required"),
|
.min(1, "Company phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
companyPhoneCountryCode: z.string().min(1),
|
|
||||||
|
|
||||||
companyLocation: z
|
companyLocation: z
|
||||||
.string()
|
.string()
|
||||||
@@ -75,9 +73,8 @@ const schema = z.object({
|
|||||||
|
|
||||||
representativePhone: z
|
representativePhone: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Representative phone is required"),
|
.min(1, "Representative phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
representativePhoneCountryCode: z.string().min(1),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof schema>;
|
type FormData = z.infer<typeof schema>;
|
||||||
@@ -91,14 +88,12 @@ const stepFields: Record<
|
|||||||
"lastName",
|
"lastName",
|
||||||
"email",
|
"email",
|
||||||
"phoneNumber",
|
"phoneNumber",
|
||||||
"phoneCountryCode",
|
|
||||||
],
|
],
|
||||||
|
|
||||||
company: [
|
company: [
|
||||||
"companyName",
|
"companyName",
|
||||||
"companyEmail",
|
"companyEmail",
|
||||||
"companyPhone",
|
"companyPhone",
|
||||||
"companyPhoneCountryCode",
|
|
||||||
"companyLocation",
|
"companyLocation",
|
||||||
"companyAddress",
|
"companyAddress",
|
||||||
],
|
],
|
||||||
@@ -107,7 +102,6 @@ const stepFields: Record<
|
|||||||
"representativeName",
|
"representativeName",
|
||||||
"representativeEmail",
|
"representativeEmail",
|
||||||
"representativePhone",
|
"representativePhone",
|
||||||
"representativePhoneCountryCode",
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,6 +111,7 @@ export default function DjiboutiForwardingAgentForm() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
trigger,
|
trigger,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
@@ -124,10 +119,9 @@ export default function DjiboutiForwardingAgentForm() {
|
|||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
phoneCountryCode: "+253",
|
phoneNumber: "",
|
||||||
companyPhoneCountryCode: "+253",
|
companyPhone: "",
|
||||||
representativePhoneCountryCode:
|
representativePhone: "",
|
||||||
"+253",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,23 +274,11 @@ export default function DjiboutiForwardingAgentForm() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="phoneNumber"
|
||||||
label="Phone Number"
|
label="Phone Number"
|
||||||
countryCode={{
|
required
|
||||||
...register(
|
|
||||||
"phoneCountryCode"
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register(
|
|
||||||
"phoneNumber"
|
|
||||||
),
|
|
||||||
placeholder: "77123456",
|
|
||||||
}}
|
|
||||||
countryCodeError={
|
|
||||||
errors.phoneCountryCode
|
|
||||||
}
|
|
||||||
phoneError={errors.phoneNumber}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -347,25 +329,11 @@ export default function DjiboutiForwardingAgentForm() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="companyPhone"
|
||||||
label="Company Phone"
|
label="Company Phone"
|
||||||
countryCode={{
|
required
|
||||||
...register(
|
|
||||||
"companyPhoneCountryCode"
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register(
|
|
||||||
"companyPhone"
|
|
||||||
),
|
|
||||||
placeholder: "77123456",
|
|
||||||
}}
|
|
||||||
countryCodeError={
|
|
||||||
errors.companyPhoneCountryCode
|
|
||||||
}
|
|
||||||
phoneError={
|
|
||||||
errors.companyPhone
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -472,25 +440,11 @@ export default function DjiboutiForwardingAgentForm() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="representativePhone"
|
||||||
label="Representative Phone"
|
label="Representative Phone"
|
||||||
countryCode={{
|
required
|
||||||
...register(
|
|
||||||
"representativePhoneCountryCode"
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register(
|
|
||||||
"representativePhone"
|
|
||||||
),
|
|
||||||
placeholder: "77123456",
|
|
||||||
}}
|
|
||||||
countryCodeError={
|
|
||||||
errors.representativePhoneCountryCode
|
|
||||||
}
|
|
||||||
phoneError={
|
|
||||||
errors.representativePhone
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -34,14 +34,18 @@ const onboardingSchema = z.object({
|
|||||||
firstName: z.string().min(1, "First name is required"),
|
firstName: z.string().min(1, "First name is required"),
|
||||||
lastName: z.string().min(1, "Last name is required"),
|
lastName: z.string().min(1, "Last name is required"),
|
||||||
email: z.string().email("Invalid email address"),
|
email: z.string().email("Invalid email address"),
|
||||||
phoneNumber: z.string().min(1, "Phone number is required"),
|
phoneNumber: z
|
||||||
phoneCountryCode: z.string().min(1),
|
.string()
|
||||||
|
.min(1, "Phone number is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
|
|
||||||
// COMPANY
|
// COMPANY
|
||||||
companyName: z.string().min(1, "Company name is required"),
|
companyName: z.string().min(1, "Company name is required"),
|
||||||
companyEmail: z.string().email("Invalid email address"),
|
companyEmail: z.string().email("Invalid email address"),
|
||||||
companyPhone: z.string().min(1, "Company phone is required"),
|
companyPhone: z
|
||||||
companyPhoneCountryCode: z.string().min(1),
|
.string()
|
||||||
|
.min(1, "Company phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
companyLocation: z.string().min(1, "Location is required"),
|
companyLocation: z.string().min(1, "Location is required"),
|
||||||
companyAddress: z.string().min(1, "Address is required"),
|
companyAddress: z.string().min(1, "Address is required"),
|
||||||
|
|
||||||
@@ -63,9 +67,8 @@ const onboardingSchema = z.object({
|
|||||||
|
|
||||||
contactPersonPhone: z
|
contactPersonPhone: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Contact person phone is required"),
|
.min(1, "Contact person phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
contactPersonPhoneCountryCode: z.string().min(1),
|
|
||||||
|
|
||||||
// GENERAL MANAGER
|
// GENERAL MANAGER
|
||||||
generalManagerName: z
|
generalManagerName: z
|
||||||
@@ -78,14 +81,15 @@ const onboardingSchema = z.object({
|
|||||||
|
|
||||||
generalManagerPhone: z
|
generalManagerPhone: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "General manager phone is required"),
|
.min(1, "General manager phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
generalManagerPhoneCountryCode: z.string().min(1),
|
|
||||||
|
|
||||||
// OPTIONAL POA
|
// OPTIONAL POA
|
||||||
poaName: z.string().optional(),
|
poaName: z.string().optional(),
|
||||||
poaPhone: z.string().optional(),
|
poaPhone: z
|
||||||
poaPhoneCountryCode: z.string().optional(),
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((v) => !v || isValidPhone(v), "Enter a valid phone number"),
|
||||||
poaAddress: z.string().optional(),
|
poaAddress: z.string().optional(),
|
||||||
poaEmail: z.string().optional(),
|
poaEmail: z.string().optional(),
|
||||||
poaLocation: z.string().optional(),
|
poaLocation: z.string().optional(),
|
||||||
@@ -102,14 +106,12 @@ const stepFields: Record<
|
|||||||
"lastName",
|
"lastName",
|
||||||
"email",
|
"email",
|
||||||
"phoneNumber",
|
"phoneNumber",
|
||||||
"phoneCountryCode",
|
|
||||||
],
|
],
|
||||||
|
|
||||||
company: [
|
company: [
|
||||||
"companyName",
|
"companyName",
|
||||||
"companyEmail",
|
"companyEmail",
|
||||||
"companyPhone",
|
"companyPhone",
|
||||||
"companyPhoneCountryCode",
|
|
||||||
"companyLocation",
|
"companyLocation",
|
||||||
"companyAddress",
|
"companyAddress",
|
||||||
"tinNumber",
|
"tinNumber",
|
||||||
@@ -120,11 +122,9 @@ const stepFields: Record<
|
|||||||
personnel: [
|
personnel: [
|
||||||
"contactPersonName",
|
"contactPersonName",
|
||||||
"contactPersonPhone",
|
"contactPersonPhone",
|
||||||
"contactPersonPhoneCountryCode",
|
|
||||||
"generalManagerName",
|
"generalManagerName",
|
||||||
"generalManagerEmail",
|
"generalManagerEmail",
|
||||||
"generalManagerPhone",
|
"generalManagerPhone",
|
||||||
"generalManagerPhoneCountryCode",
|
|
||||||
],
|
],
|
||||||
|
|
||||||
poa: [],
|
poa: [],
|
||||||
@@ -136,6 +136,7 @@ export default function ImportExportOnBoarding() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
trigger,
|
trigger,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
@@ -143,11 +144,11 @@ export default function ImportExportOnBoarding() {
|
|||||||
resolver: zodResolver(onboardingSchema),
|
resolver: zodResolver(onboardingSchema),
|
||||||
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
phoneCountryCode: "+251",
|
phoneNumber: "",
|
||||||
companyPhoneCountryCode: "+251",
|
companyPhone: "",
|
||||||
contactPersonPhoneCountryCode: "+251",
|
contactPersonPhone: "",
|
||||||
generalManagerPhoneCountryCode: "+251",
|
generalManagerPhone: "",
|
||||||
poaPhoneCountryCode: "+251",
|
poaPhone: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -303,23 +304,11 @@ export default function ImportExportOnBoarding() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="phoneNumber"
|
||||||
label="Phone Number"
|
label="Phone Number"
|
||||||
countryCode={{
|
required
|
||||||
...register(
|
|
||||||
"phoneCountryCode"
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register(
|
|
||||||
"phoneNumber"
|
|
||||||
),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
countryCodeError={
|
|
||||||
errors.phoneCountryCode
|
|
||||||
}
|
|
||||||
phoneError={errors.phoneNumber}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -370,25 +359,11 @@ export default function ImportExportOnBoarding() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="companyPhone"
|
||||||
label="Company Phone"
|
label="Company Phone"
|
||||||
countryCode={{
|
required
|
||||||
...register(
|
|
||||||
"companyPhoneCountryCode"
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register(
|
|
||||||
"companyPhone"
|
|
||||||
),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
countryCodeError={
|
|
||||||
errors.companyPhoneCountryCode
|
|
||||||
}
|
|
||||||
phoneError={
|
|
||||||
errors.companyPhone
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -535,25 +510,11 @@ export default function ImportExportOnBoarding() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="contactPersonPhone"
|
||||||
label="Contact Person Phone"
|
label="Contact Person Phone"
|
||||||
countryCode={{
|
required
|
||||||
...register(
|
|
||||||
"contactPersonPhoneCountryCode"
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register(
|
|
||||||
"contactPersonPhone"
|
|
||||||
),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
countryCodeError={
|
|
||||||
errors.contactPersonPhoneCountryCode
|
|
||||||
}
|
|
||||||
phoneError={
|
|
||||||
errors.contactPersonPhone
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -614,25 +575,11 @@ export default function ImportExportOnBoarding() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="generalManagerPhone"
|
||||||
label="General Manager Phone"
|
label="General Manager Phone"
|
||||||
countryCode={{
|
required
|
||||||
...register(
|
|
||||||
"generalManagerPhoneCountryCode"
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register(
|
|
||||||
"generalManagerPhone"
|
|
||||||
),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
countryCodeError={
|
|
||||||
errors.generalManagerPhoneCountryCode
|
|
||||||
}
|
|
||||||
phoneError={
|
|
||||||
errors.generalManagerPhone
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -669,17 +616,10 @@ export default function ImportExportOnBoarding() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="poaPhone"
|
||||||
label="PoA Phone"
|
label="PoA Phone"
|
||||||
countryCode={{
|
|
||||||
...register(
|
|
||||||
"poaPhoneCountryCode"
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
phone={{
|
|
||||||
...register("poaPhone"),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -29,8 +29,10 @@ const schema = z.object({
|
|||||||
firstName: z.string().min(1),
|
firstName: z.string().min(1),
|
||||||
lastName: z.string().min(1),
|
lastName: z.string().min(1),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
phoneNumber: z.string().min(1),
|
phoneNumber: z
|
||||||
phoneCountryCode: z.string().min(1),
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
|
|
||||||
// TRANSPORT
|
// TRANSPORT
|
||||||
fanNumber: z.string().min(1),
|
fanNumber: z.string().min(1),
|
||||||
@@ -60,7 +62,6 @@ const stepFields: Record<Step, (keyof FormData)[]> = {
|
|||||||
"lastName",
|
"lastName",
|
||||||
"email",
|
"email",
|
||||||
"phoneNumber",
|
"phoneNumber",
|
||||||
"phoneCountryCode",
|
|
||||||
],
|
],
|
||||||
transport: [
|
transport: [
|
||||||
"fanNumber",
|
"fanNumber",
|
||||||
@@ -78,6 +79,7 @@ export default function TransporterOnboarding() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
trigger,
|
trigger,
|
||||||
watch,
|
watch,
|
||||||
@@ -85,7 +87,7 @@ export default function TransporterOnboarding() {
|
|||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
phoneCountryCode: "+251",
|
phoneNumber: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,12 +163,11 @@ export default function TransporterOnboarding() {
|
|||||||
<FieldError errors={[errors.email]} />
|
<FieldError errors={[errors.email]} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
|
control={control}
|
||||||
|
name="phoneNumber"
|
||||||
label="Phone Number"
|
label="Phone Number"
|
||||||
countryCode={{ ...register("phoneCountryCode") }}
|
required
|
||||||
phone={{ ...register("phoneNumber") }}
|
|
||||||
countryCodeError={errors.phoneCountryCode}
|
|
||||||
phoneError={errors.phoneNumber}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
import type { ProfileResponse } from "@/types/profile";
|
import type { ProfileResponse } from "@/types/profile";
|
||||||
import type {
|
import type {
|
||||||
CreateCompanyPayload,
|
CreateCompanyPayload,
|
||||||
@@ -27,8 +27,10 @@ import OnboardingRoleSelect from "./OnboardingRoleSelect";
|
|||||||
export const COMPANY_PROFILE_SCHEMA = z.object({
|
export const COMPANY_PROFILE_SCHEMA = z.object({
|
||||||
companyName: z.string().min(1, "Company name is required"),
|
companyName: z.string().min(1, "Company name is required"),
|
||||||
companyEmail: z.string().email("Invalid email address"),
|
companyEmail: z.string().email("Invalid email address"),
|
||||||
companyPhone: z.string().min(1, "Company phone is required"),
|
companyPhone: z
|
||||||
companyPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "Company phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
companyLocation: z.string().min(1, "Location is required"),
|
companyLocation: z.string().min(1, "Location is required"),
|
||||||
companyAddress: z.string().min(1, "Address is required"),
|
companyAddress: z.string().min(1, "Address is required"),
|
||||||
tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
|
tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
|
||||||
@@ -37,13 +39,6 @@ export const COMPANY_PROFILE_SCHEMA = z.object({
|
|||||||
|
|
||||||
export type CompanyProfileFormData = z.infer<typeof COMPANY_PROFILE_SCHEMA>;
|
export type CompanyProfileFormData = z.infer<typeof COMPANY_PROFILE_SCHEMA>;
|
||||||
|
|
||||||
export function splitPhone(fullPhone?: string | null) {
|
|
||||||
if (!fullPhone) return { code: "+251", number: "" };
|
|
||||||
const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
|
|
||||||
if (match) return { code: match[1], number: match[2] };
|
|
||||||
return { code: "+251", number: fullPhone };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TabCompanyProfileProps {
|
interface TabCompanyProfileProps {
|
||||||
profile?: ProfileResponse;
|
profile?: ProfileResponse;
|
||||||
mode?: "edit" | "create";
|
mode?: "edit" | "create";
|
||||||
@@ -61,12 +56,10 @@ export default function TabCompanyProfile({
|
|||||||
|
|
||||||
const defaultValues = useMemo((): CompanyProfileFormData => {
|
const defaultValues = useMemo((): CompanyProfileFormData => {
|
||||||
if (profile) {
|
if (profile) {
|
||||||
const phone = splitPhone(profile.companyPhone);
|
|
||||||
return {
|
return {
|
||||||
companyName: profile.companyName,
|
companyName: profile.companyName,
|
||||||
companyEmail: profile.companyEmail ?? "",
|
companyEmail: profile.companyEmail ?? "",
|
||||||
companyPhone: phone.number,
|
companyPhone: profile.companyPhone ?? "",
|
||||||
companyPhoneCountryCode: phone.code,
|
|
||||||
companyLocation: profile.companyLocation,
|
companyLocation: profile.companyLocation,
|
||||||
companyAddress: profile.companyAddress ?? "",
|
companyAddress: profile.companyAddress ?? "",
|
||||||
tinNumber: profile.tinNumber,
|
tinNumber: profile.tinNumber,
|
||||||
@@ -77,7 +70,6 @@ export default function TabCompanyProfile({
|
|||||||
companyName: "",
|
companyName: "",
|
||||||
companyEmail: "",
|
companyEmail: "",
|
||||||
companyPhone: "",
|
companyPhone: "",
|
||||||
companyPhoneCountryCode: "+251",
|
|
||||||
companyLocation: "",
|
companyLocation: "",
|
||||||
companyAddress: "",
|
companyAddress: "",
|
||||||
tinNumber: "",
|
tinNumber: "",
|
||||||
@@ -87,6 +79,7 @@ export default function TabCompanyProfile({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
@@ -100,7 +93,7 @@ export default function TabCompanyProfile({
|
|||||||
const base = {
|
const base = {
|
||||||
companyName: data.companyName,
|
companyName: data.companyName,
|
||||||
companyEmail: data.companyEmail,
|
companyEmail: data.companyEmail,
|
||||||
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
|
companyPhone: data.companyPhone,
|
||||||
companyLocation: data.companyLocation,
|
companyLocation: data.companyLocation,
|
||||||
companyAddress: data.companyAddress,
|
companyAddress: data.companyAddress,
|
||||||
tin: data.tinNumber,
|
tin: data.tinNumber,
|
||||||
@@ -185,15 +178,11 @@ export default function TabCompanyProfile({
|
|||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={6}>
|
<Grid.Col span={6}>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("companyPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{
|
name="companyPhone"
|
||||||
...register("companyPhone"),
|
|
||||||
placeholder: "912345678",
|
|
||||||
}}
|
|
||||||
countryCodeError={errors.companyPhoneCountryCode}
|
|
||||||
phoneError={errors.companyPhone}
|
|
||||||
label="Company Phone"
|
label="Company Phone"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -14,24 +14,19 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
import type { ProfileResponse } from "@/types/profile";
|
import type { ProfileResponse } from "@/types/profile";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
contactPersonName: z.string().min(1, "Contact person name is required"),
|
contactPersonName: z.string().min(1, "Contact person name is required"),
|
||||||
contactPersonPhone: z.string().min(1, "Contact person phone is required"),
|
contactPersonPhone: z
|
||||||
contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "Contact person phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof schema>;
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
function splitPhone(fullPhone?: string | null) {
|
|
||||||
if (!fullPhone) return { code: "+251", number: "" };
|
|
||||||
const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
|
|
||||||
if (match) return { code: match[1], number: match[2] };
|
|
||||||
return { code: "+251", number: fullPhone };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TabContactPersonProps {
|
interface TabContactPersonProps {
|
||||||
profile: ProfileResponse;
|
profile: ProfileResponse;
|
||||||
mode?: "edit" | "onboarding";
|
mode?: "edit" | "onboarding";
|
||||||
@@ -42,16 +37,15 @@ export default function TabContactPerson({ profile, mode = "edit", onContinue }:
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const defaultValues = useMemo((): FormData => {
|
const defaultValues = useMemo((): FormData => {
|
||||||
const phone = splitPhone(profile.contactPersonPhone);
|
|
||||||
return {
|
return {
|
||||||
contactPersonName: profile.contactPersonName ?? "",
|
contactPersonName: profile.contactPersonName ?? "",
|
||||||
contactPersonPhone: phone.number,
|
contactPersonPhone: profile.contactPersonPhone ?? "",
|
||||||
contactPersonPhoneCountryCode: phone.code,
|
|
||||||
};
|
};
|
||||||
}, [profile]);
|
}, [profile]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
@@ -64,7 +58,7 @@ export default function TabContactPerson({ profile, mode = "edit", onContinue }:
|
|||||||
mutationFn: (data: FormData) =>
|
mutationFn: (data: FormData) =>
|
||||||
api.companies.updateProfile.call({
|
api.companies.updateProfile.call({
|
||||||
contactPersonName: data.contactPersonName,
|
contactPersonName: data.contactPersonName,
|
||||||
contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
|
contactPersonPhone: data.contactPersonPhone,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
|
queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
|
||||||
@@ -93,12 +87,11 @@ export default function TabContactPerson({ profile, mode = "edit", onContinue }:
|
|||||||
{...register("contactPersonName")}
|
{...register("contactPersonName")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("contactPersonPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("contactPersonPhone"), placeholder: "912345678" }}
|
name="contactPersonPhone"
|
||||||
countryCodeError={errors.contactPersonPhoneCountryCode}
|
|
||||||
phoneError={errors.contactPersonPhone}
|
|
||||||
label="Phone Number"
|
label="Phone Number"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -15,25 +15,20 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
import type { ProfileResponse } from "@/types/profile";
|
import type { ProfileResponse } from "@/types/profile";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
generalManagerName: z.string().min(1, "GM name is required"),
|
generalManagerName: z.string().min(1, "GM name is required"),
|
||||||
generalManagerEmail: z.string().email("Invalid GM email"),
|
generalManagerEmail: z.string().email("Invalid GM email"),
|
||||||
generalManagerPhone: z.string().min(1, "GM phone is required"),
|
generalManagerPhone: z
|
||||||
generalManagerPhoneCountryCode: z.string().min(1, "Country code is required"),
|
.string()
|
||||||
|
.min(1, "GM phone is required")
|
||||||
|
.refine(isValidPhone, "Enter a valid phone number"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof schema>;
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
function splitPhone(fullPhone?: string | null) {
|
|
||||||
if (!fullPhone) return { code: "+251", number: "" };
|
|
||||||
const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
|
|
||||||
if (match) return { code: match[1], number: match[2] };
|
|
||||||
return { code: "+251", number: fullPhone };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TabGeneralManagerProps {
|
interface TabGeneralManagerProps {
|
||||||
profile: ProfileResponse;
|
profile: ProfileResponse;
|
||||||
mode?: "edit" | "onboarding";
|
mode?: "edit" | "onboarding";
|
||||||
@@ -44,17 +39,16 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const defaultValues = useMemo((): FormData => {
|
const defaultValues = useMemo((): FormData => {
|
||||||
const phone = splitPhone(profile.generalManagerPhone);
|
|
||||||
return {
|
return {
|
||||||
generalManagerName: profile.generalManagerName ?? "",
|
generalManagerName: profile.generalManagerName ?? "",
|
||||||
generalManagerEmail: profile.generalManagerEmail ?? "",
|
generalManagerEmail: profile.generalManagerEmail ?? "",
|
||||||
generalManagerPhone: phone.number,
|
generalManagerPhone: profile.generalManagerPhone ?? "",
|
||||||
generalManagerPhoneCountryCode: phone.code,
|
|
||||||
};
|
};
|
||||||
}, [profile]);
|
}, [profile]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
@@ -68,7 +62,7 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
|
|||||||
api.companies.updateProfile.call({
|
api.companies.updateProfile.call({
|
||||||
generalManagerName: data.generalManagerName,
|
generalManagerName: data.generalManagerName,
|
||||||
generalManagerEmail: data.generalManagerEmail,
|
generalManagerEmail: data.generalManagerEmail,
|
||||||
generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
|
generalManagerPhone: data.generalManagerPhone,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
|
queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
|
||||||
@@ -108,12 +102,11 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
|
|||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={6}>
|
<Grid.Col span={6}>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("generalManagerPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("generalManagerPhone"), placeholder: "912345678" }}
|
name="generalManagerPhone"
|
||||||
countryCodeError={errors.generalManagerPhoneCountryCode}
|
|
||||||
phoneError={errors.generalManagerPhone}
|
|
||||||
label="Phone Number"
|
label="Phone Number"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -15,27 +15,22 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
|
||||||
import type { ProfileResponse } from "@/types/profile";
|
import type { ProfileResponse } from "@/types/profile";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
poaName: z.string().optional(),
|
poaName: z.string().optional(),
|
||||||
poaEmail: z.string().optional(),
|
poaEmail: z.string().optional(),
|
||||||
poaPhone: z.string().optional(),
|
poaPhone: z
|
||||||
poaPhoneCountryCode: z.string().optional(),
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((v) => !v || isValidPhone(v), "Enter a valid phone number"),
|
||||||
poaLocation: z.string().optional(),
|
poaLocation: z.string().optional(),
|
||||||
poaAddress: z.string().optional(),
|
poaAddress: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof schema>;
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
function splitPhone(fullPhone?: string | null) {
|
|
||||||
if (!fullPhone) return { code: "+251", number: "" };
|
|
||||||
const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
|
|
||||||
if (match) return { code: match[1], number: match[2] };
|
|
||||||
return { code: "+251", number: fullPhone };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TabPowerOfAttorneyProps {
|
interface TabPowerOfAttorneyProps {
|
||||||
profile: ProfileResponse;
|
profile: ProfileResponse;
|
||||||
mode?: "edit" | "onboarding";
|
mode?: "edit" | "onboarding";
|
||||||
@@ -50,12 +45,10 @@ export default function TabPowerOfAttorney({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const defaultValues = useMemo((): FormData => {
|
const defaultValues = useMemo((): FormData => {
|
||||||
const phone = splitPhone(profile.poaPhone);
|
|
||||||
return {
|
return {
|
||||||
poaName: profile.poaName ?? "",
|
poaName: profile.poaName ?? "",
|
||||||
poaEmail: profile.poaEmail ?? "",
|
poaEmail: profile.poaEmail ?? "",
|
||||||
poaPhone: phone.number,
|
poaPhone: profile.poaPhone ?? "",
|
||||||
poaPhoneCountryCode: profile.poaPhone ? phone.code : "+251",
|
|
||||||
poaLocation: profile.poaLocation ?? "",
|
poaLocation: profile.poaLocation ?? "",
|
||||||
poaAddress: profile.poaAddress ?? "",
|
poaAddress: profile.poaAddress ?? "",
|
||||||
};
|
};
|
||||||
@@ -63,6 +56,7 @@ export default function TabPowerOfAttorney({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
@@ -75,10 +69,7 @@ export default function TabPowerOfAttorney({
|
|||||||
mutationFn: (data: FormData) =>
|
mutationFn: (data: FormData) =>
|
||||||
api.companies.updateProfile.call({
|
api.companies.updateProfile.call({
|
||||||
poaName: data.poaName || undefined,
|
poaName: data.poaName || undefined,
|
||||||
poaPhone:
|
poaPhone: data.poaPhone || undefined,
|
||||||
data.poaPhone && data.poaPhoneCountryCode
|
|
||||||
? `${data.poaPhoneCountryCode}${data.poaPhone}`
|
|
||||||
: undefined,
|
|
||||||
poaEmail: data.poaEmail || undefined,
|
poaEmail: data.poaEmail || undefined,
|
||||||
poaLocation: data.poaLocation || undefined,
|
poaLocation: data.poaLocation || undefined,
|
||||||
poaAddress: data.poaAddress || undefined,
|
poaAddress: data.poaAddress || undefined,
|
||||||
@@ -124,12 +115,10 @@ export default function TabPowerOfAttorney({
|
|||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={6}>
|
<Grid.Col span={6}>
|
||||||
<PhoneInput
|
<ControlledPhoneField
|
||||||
countryCode={{ ...register("poaPhoneCountryCode") }}
|
control={control}
|
||||||
phone={{ ...register("poaPhone"), placeholder: "912345678" }}
|
name="poaPhone"
|
||||||
label="PoA Phone"
|
label="PoA Phone"
|
||||||
countryCodeError={errors.poaPhoneCountryCode}
|
|
||||||
phoneError={errors.poaPhone}
|
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Phone numbers are stored combined as `{countryCode}{number}`
|
|
||||||
* (e.g. "+251912345678"). These helpers split a stored value back into the two
|
|
||||||
* fields the onboarding forms use, and combine them on the way out.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const DEFAULT_COUNTRY_CODE = "+251";
|
|
||||||
|
|
||||||
/** Split a stored phone into { countryCode, number } for form rehydration. */
|
|
||||||
export function splitPhone(
|
|
||||||
value: string | null | undefined,
|
|
||||||
defaultCode = DEFAULT_COUNTRY_CODE,
|
|
||||||
): { countryCode: string; number: string } {
|
|
||||||
if (!value) return { countryCode: defaultCode, number: "" };
|
|
||||||
const trimmed = value.trim();
|
|
||||||
// Ethiopian (+251) is the common case; otherwise take the leading "+NNN".
|
|
||||||
const match = trimmed.match(/^(\+\d{1,4})(.*)$/);
|
|
||||||
if (match) return { countryCode: match[1], number: match[2] };
|
|
||||||
return { countryCode: defaultCode, number: trimmed };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Combine a country code + number into the stored phone form. */
|
|
||||||
export function combinePhone(countryCode: string, number: string): string {
|
|
||||||
return `${countryCode}${number}`;
|
|
||||||
}
|
|
||||||
50
pnpm-lock.yaml
generated
50
pnpm-lock.yaml
generated
@@ -107,6 +107,9 @@ importers:
|
|||||||
handlebars:
|
handlebars:
|
||||||
specifier: ^4.7.9
|
specifier: ^4.7.9
|
||||||
version: 4.7.9
|
version: 4.7.9
|
||||||
|
libphonenumber-js:
|
||||||
|
specifier: ^1.13.6
|
||||||
|
version: 1.13.6
|
||||||
minio:
|
minio:
|
||||||
specifier: 7.1.3
|
specifier: 7.1.3
|
||||||
version: 7.1.3
|
version: 7.1.3
|
||||||
@@ -349,6 +352,9 @@ importers:
|
|||||||
react-hot-toast:
|
react-hot-toast:
|
||||||
specifier: ^2.6.0
|
specifier: ^2.6.0
|
||||||
version: 2.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
version: 2.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
react-phone-number-input:
|
||||||
|
specifier: ^3.4.17
|
||||||
|
version: 3.4.17(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.27.0
|
specifier: ^6.27.0
|
||||||
version: 6.30.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
version: 6.30.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
@@ -5915,6 +5921,9 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
|
classnames@2.5.1:
|
||||||
|
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||||
|
|
||||||
cli-cursor@3.1.0:
|
cli-cursor@3.1.0:
|
||||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6200,6 +6209,9 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
country-flag-icons@1.6.17:
|
||||||
|
resolution: {integrity: sha512-Nmik0289ZVZSI3c7mJR/amg6DyY7Z59b0sTFSKayeX72mHfPzCPJygwJs2pYgQULzuAyWeCUgwAJ+Dq8OR+JFw==}
|
||||||
|
|
||||||
crc-32@1.2.2:
|
crc-32@1.2.2:
|
||||||
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
|
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@@ -7946,6 +7958,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
|
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
input-format@0.3.14:
|
||||||
|
resolution: {integrity: sha512-gHMrgrbCgmT4uK5Um5eVDUohuV9lcs95ZUUN9Px2Y0VIfjTzT2wF8Q3Z4fwLFm7c5Z2OXCm53FHoovj6SlOKdg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18.1.0'
|
||||||
|
react-dom: '>=18.1.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
internal-ip@1.2.0:
|
internal-ip@1.2.0:
|
||||||
resolution: {integrity: sha512-DzGfTasXPmwizQP4XV2rR6r2vp8TjlOpMnJqG9Iy2i1pl1lkZdZj5rSpIc7YFGX2nS46PPgAGEyT+Q5hE2FB2g==}
|
resolution: {integrity: sha512-DzGfTasXPmwizQP4XV2rR6r2vp8TjlOpMnJqG9Iy2i1pl1lkZdZj5rSpIc7YFGX2nS46PPgAGEyT+Q5hE2FB2g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -10490,6 +10513,12 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-phone-number-input@3.4.17:
|
||||||
|
resolution: {integrity: sha512-1wcjhBAWHgEBAGLi5/XbeZI7Q3aEHNb2z/dHY6R2Gz70TQvu0ZoOT28NTdwtZf4lyRKXWufnTzVhLPBUD8LfmQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8'
|
||||||
|
react-dom: '>=16.8'
|
||||||
|
|
||||||
react-redux@9.3.0:
|
react-redux@9.3.0:
|
||||||
resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
|
resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -19678,6 +19707,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
|
classnames@2.5.1: {}
|
||||||
|
|
||||||
cli-cursor@3.1.0:
|
cli-cursor@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
restore-cursor: 3.1.0
|
restore-cursor: 3.1.0
|
||||||
@@ -19944,6 +19975,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
country-flag-icons@1.6.17: {}
|
||||||
|
|
||||||
crc-32@1.2.2: {}
|
crc-32@1.2.2: {}
|
||||||
|
|
||||||
crc32-stream@4.0.3:
|
crc32-stream@4.0.3:
|
||||||
@@ -22131,6 +22164,13 @@ snapshots:
|
|||||||
|
|
||||||
ini@4.1.1: {}
|
ini@4.1.1: {}
|
||||||
|
|
||||||
|
input-format@0.3.14(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||||
|
dependencies:
|
||||||
|
prop-types: 15.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.6
|
||||||
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
|
||||||
internal-ip@1.2.0:
|
internal-ip@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
meow: 3.7.0
|
meow: 3.7.0
|
||||||
@@ -25075,6 +25115,16 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.31
|
'@types/react': 18.3.31
|
||||||
|
|
||||||
|
react-phone-number-input@3.4.17(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||||
|
dependencies:
|
||||||
|
classnames: 2.5.1
|
||||||
|
country-flag-icons: 1.6.17
|
||||||
|
input-format: 0.3.14(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
libphonenumber-js: 1.13.6
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 19.2.6
|
||||||
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
|
||||||
react-redux@9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1):
|
react-redux@9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/use-sync-external-store': 0.0.6
|
'@types/use-sync-external-store': 0.0.6
|
||||||
|
|||||||
Reference in New Issue
Block a user