diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index a4ecf321e..580e77e0c 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -30,6 +30,7 @@ import { } from "./seed/edr-freight.seed"; import { EdrOrgSeeder } from "./seed/edr-org.seeder"; import { DemoUsersSeeder } from "./seed/demo-users.seeder"; +import { FileUploadSettingsSeeder } from "./seed/file-upload-settings.seeder"; @Module({ imports: [ @@ -71,18 +72,20 @@ import { DemoUsersSeeder } from "./seed/demo-users.seeder"; BackofficeModule, DemoPermissionsModule, ], - providers: [EdrOrgSeeder, DemoUsersSeeder], + providers: [EdrOrgSeeder, DemoUsersSeeder, FileUploadSettingsSeeder], }) export class AppModule implements OnApplicationBootstrap { constructor( private readonly seeder: DataSeeder, private readonly edrOrgSeeder: EdrOrgSeeder, private readonly demoUsersSeeder: DemoUsersSeeder, + private readonly fileUploadSettingsSeeder: FileUploadSettingsSeeder, ) { } async onApplicationBootstrap() { await this.seeder.run(); await this.edrOrgSeeder.run(); await this.demoUsersSeeder.run(); + await this.fileUploadSettingsSeeder.run(); } } diff --git a/apps/edr-freight-api/src/modules/companies/companies.controller.ts b/apps/edr-freight-api/src/modules/companies/companies.controller.ts index 5c646279e..b1481d0d4 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -1,6 +1,8 @@ -import { Controller, Get, Post, Patch, Delete, Body, Param, Query, ParseUUIDPipe, HttpCode, HttpStatus } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Controller, Get, Post, Patch, Delete, Body, Param, Query, ParseUUIDPipe, HttpCode, HttpStatus, UseInterceptors, UploadedFiles } from '@nestjs/common'; +import { AnyFilesInterceptor } from '@nestjs/platform-express'; +import { ApiOperation, ApiTags, ApiConsumes } from '@nestjs/swagger'; import { CurrentUser } from '@edr/api-common'; +import { FilesService } from '../files/files.service'; import { CompaniesService } from './companies.service'; import { CreateCompanyDto } from './dto/create-company.dto'; import { UpdateCompanyDto } from './dto/update-company.dto'; @@ -11,6 +13,8 @@ import { ResponseCompanyDto } from './dto/response-company.dto'; import { ResponseExternalProfileDto } from './dto/response-external-profile.dto'; import { ResponseFFClientDto } from './dto/response-ff-client.dto'; import { CompanyInfoResponseDto } from './dto/company-info-response.dto'; +import { UpdateProfileDto } from './dto/update-profile.dto'; +import { ProfileResponseDto } from './dto/profile-response.dto'; interface CurrentIamUser { id: string; @@ -22,7 +26,10 @@ interface CurrentIamUser { @ApiTags('Companies') @Controller('companies') export class CompaniesController { - constructor(private readonly companiesService: CompaniesService) {} + constructor( + private readonly companiesService: CompaniesService, + private readonly filesService: FilesService, + ) {} @Get('getInfo') @ApiOperation({ summary: 'Get company info for the current user' }) @@ -31,6 +38,22 @@ export class CompaniesController { return new CompanyInfoResponseDto(profile, company); } + @Get('profile') + @ApiOperation({ summary: 'Get flattened profile for the settings page' }) + async getProfile(@CurrentUser() user: CurrentIamUser): Promise { + const { profile, company } = await this.companiesService.getCompanyInfoByUserId(user.id); + return new ProfileResponseDto(profile, company); + } + + @Patch('profile') + @ApiOperation({ summary: 'Update profile (flattened settings page)' }) + async updateProfile( + @CurrentUser() user: CurrentIamUser, + @Body() dto: UpdateProfileDto, + ): Promise { + return this.companiesService.updateProfile(user.id, dto); + } + @Post('create') @ApiOperation({ summary: 'Create a company with its associated external profile (onboarding)' }) async createWithProfile( @@ -105,6 +128,17 @@ export class CompaniesController { await this.companiesService.deleteCompany(id); } + @Post(':companyId/documents') + @UseInterceptors(AnyFilesInterceptor()) + @ApiConsumes('multipart/form-data') + @ApiOperation({ summary: 'Upload documents for a company (onboarding)' }) + async uploadDocuments( + @Param('companyId', ParseUUIDPipe) companyId: string, + @UploadedFiles() files: Array, + ) { + return this.filesService.uploadMany(companyId, 'companies', files); + } + @Post(':companyId/profiles') @ApiOperation({ summary: 'Add a profile (employee) to a company' }) async createProfile( diff --git a/apps/edr-freight-api/src/modules/companies/companies.module.ts b/apps/edr-freight-api/src/modules/companies/companies.module.ts index 8fac2f901..d18573460 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.module.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { FilesModule } from '../files/files.module'; import { CompaniesController } from './companies.controller'; import { CompaniesService } from './companies.service'; import { CompaniesRepository } from './companies.repository'; @@ -10,7 +11,7 @@ import { ExternalProfile } from './entities/external-profile.entity'; import { FFClient } from './entities/ff-client.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Company, ExternalProfile, FFClient])], + imports: [TypeOrmModule.forFeature([Company, ExternalProfile, FFClient]), FilesModule], controllers: [CompaniesController], providers: [CompaniesService, CompaniesRepository, ExternalProfileRepository, FFClientRepository], exports: [CompaniesService], diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index 03c104798..f383b9e55 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -7,6 +7,8 @@ import { UpdateCompanyDto } from './dto/update-company.dto'; import { CreateExternalProfileDto } from './dto/create-external-profile.dto'; import { CreateFFClientDto } from './dto/create-ff-client.dto'; import { CreateCompanyWithProfileDto } from './dto/create-company-with-profile.dto'; +import { UpdateProfileDto } from './dto/update-profile.dto'; +import { ProfileResponseDto } from './dto/profile-response.dto'; import { Company } from './entities/company.entity'; import { ExternalProfile } from './entities/external-profile.entity'; import { FFClient } from './entities/ff-client.entity'; @@ -103,6 +105,42 @@ export class CompaniesService { return updated; } + async updateProfile(userId: string, dto: UpdateProfileDto): Promise { + const { profile, company } = await this.getCompanyInfoByUserId(userId); + + const companyUpdates: Record = {}; + const attrUpdates: Record = { ...(company.attributes ?? {}) }; + + if (dto.companyName !== undefined) companyUpdates.name = dto.companyName; + if (dto.companyEmail !== undefined) companyUpdates.email = dto.companyEmail; + if (dto.companyPhone !== undefined) companyUpdates.phone = dto.companyPhone; + if (dto.companyLocation !== undefined) companyUpdates.country = dto.companyLocation; + if (dto.companyAddress !== undefined) companyUpdates.address = dto.companyAddress; + if (dto.tin !== undefined) companyUpdates.tin = dto.tin; + if (dto.vatNumber !== undefined) companyUpdates.vatNumber = dto.vatNumber; + if (dto.fanNumber !== undefined) { + companyUpdates.businessLicense = dto.fanNumber; + companyUpdates.fanNumber = dto.fanNumber; + } + + if (dto.contactPersonName !== undefined) attrUpdates.contactPersonName = dto.contactPersonName; + if (dto.contactPersonPhone !== undefined) attrUpdates.contactPersonPhone = dto.contactPersonPhone; + if (dto.generalManagerName !== undefined) attrUpdates.generalManagerName = dto.generalManagerName; + if (dto.generalManagerEmail !== undefined) attrUpdates.generalManagerEmail = dto.generalManagerEmail; + if (dto.generalManagerPhone !== undefined) attrUpdates.generalManagerPhone = dto.generalManagerPhone; + if (dto.poaName !== undefined) attrUpdates.poaName = dto.poaName; + if (dto.poaPhone !== undefined) attrUpdates.poaPhone = dto.poaPhone; + if (dto.poaEmail !== undefined) attrUpdates.poaEmail = dto.poaEmail; + if (dto.poaLocation !== undefined) attrUpdates.poaLocation = dto.poaLocation; + if (dto.poaAddress !== undefined) attrUpdates.poaAddress = dto.poaAddress; + + companyUpdates.attributes = attrUpdates; + + const updated = await this.companiesRepo.update(company.id, companyUpdates); + if (!updated) throw new NotFoundException(`Company ${company.id} not found`); + return new ProfileResponseDto(profile, updated); + } + async deleteCompany(id: string): Promise { await this.findCompanyById(id); await this.companiesRepo.softDelete(id); diff --git a/apps/edr-freight-api/src/modules/companies/dto/profile-response.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/profile-response.dto.ts new file mode 100644 index 000000000..ee8ede34f --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/profile-response.dto.ts @@ -0,0 +1,53 @@ +import { Company } from '../entities/company.entity'; +import { ExternalProfile } from '../entities/external-profile.entity'; + +export class ProfileResponseDto { + companyId: string; + companyName: string; + companyEmail: string | null; + companyPhone: string | null; + companyLocation: string; + companyAddress: string | null; + tinNumber: string; + vatNumber: string | null; + fanNumber: string | null; + + contactPersonName: string | null; + contactPersonPhone: string | null; + generalManagerName: string | null; + generalManagerEmail: string | null; + generalManagerPhone: string | null; + + poaName: string | null; + poaPhone: string | null; + poaEmail: string | null; + poaLocation: string | null; + poaAddress: string | null; + + profileId: string; + + constructor(profile: ExternalProfile, company: Company) { + this.companyId = company.id; + this.companyName = company.name; + this.companyEmail = company.email ?? null; + this.companyPhone = company.phone ?? null; + this.companyLocation = company.country; + this.companyAddress = company.address ?? null; + this.tinNumber = company.tin; + this.vatNumber = company.vatNumber ?? null; + this.fanNumber = company.fanNumber ?? null; + this.profileId = profile.id; + + const attrs = company.attributes ?? {}; + this.contactPersonName = attrs.contactPersonName ?? null; + this.contactPersonPhone = attrs.contactPersonPhone ?? null; + this.generalManagerName = attrs.generalManagerName ?? null; + this.generalManagerEmail = attrs.generalManagerEmail ?? null; + this.generalManagerPhone = attrs.generalManagerPhone ?? null; + this.poaName = attrs.poaName ?? null; + this.poaPhone = attrs.poaPhone ?? null; + this.poaEmail = attrs.poaEmail ?? null; + this.poaLocation = attrs.poaLocation ?? null; + this.poaAddress = attrs.poaAddress ?? null; + } +} diff --git a/apps/edr-freight-api/src/modules/companies/dto/update-profile.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/update-profile.dto.ts new file mode 100644 index 000000000..0acdf60a1 --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/update-profile.dto.ts @@ -0,0 +1,83 @@ +import { IsString, IsOptional, IsEmail, MaxLength, Length, Matches } from 'class-validator'; + +export class UpdateProfileDto { + @IsOptional() + @IsString() + @MaxLength(200) + companyName?: string; + + @IsOptional() + @IsEmail() + @MaxLength(150) + companyEmail?: string; + + @IsOptional() + @IsString() + @MaxLength(20) + companyPhone?: string; + + @IsOptional() + @IsString() + @MaxLength(32) + companyLocation?: string; + + @IsOptional() + @IsString() + companyAddress?: string; + + @IsOptional() + @IsString() + @Length(10, 10) + @Matches(/^\d+$/, { message: 'TIN must contain only digits' }) + tin?: string; + + @IsOptional() + @IsString() + @MaxLength(50) + vatNumber?: string; + + @IsOptional() + @IsString() + @MaxLength(16) + fanNumber?: string; + + @IsOptional() + @IsString() + contactPersonName?: string; + + @IsOptional() + @IsString() + contactPersonPhone?: string; + + @IsOptional() + @IsString() + generalManagerName?: string; + + @IsOptional() + @IsEmail() + generalManagerEmail?: string; + + @IsOptional() + @IsString() + generalManagerPhone?: string; + + @IsOptional() + @IsString() + poaName?: string; + + @IsOptional() + @IsString() + poaPhone?: string; + + @IsOptional() + @IsEmail() + poaEmail?: string; + + @IsOptional() + @IsString() + poaLocation?: string; + + @IsOptional() + @IsString() + poaAddress?: string; +} diff --git a/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts b/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts new file mode 100644 index 000000000..2ef1f79f0 --- /dev/null +++ b/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts @@ -0,0 +1,125 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { DataSource } from "typeorm"; + +import { FileUploadField } from "../modules/file-upload-settings/entities/file-upload-field.entity"; +import { FileUploadSetting } from "../modules/file-upload-settings/entities/file-upload-setting.entity"; + +const COMPANY_ONBOARDING_DOCUMENTS = [ + { + code: "company_onboarding_documents_customer", + label: "Customer onboarding documents", + entity: "customer", + }, + { + code: "company_onboarding_documents_forwarder", + label: "Forwarder onboarding documents", + entity: "other", + }, + { + code: "company_onboarding_documents_transporter", + label: "Transporter onboarding documents", + entity: "other", + }, + { + code: "company_onboarding_documents_forwarder_dj", + label: "Djibouti forwarder onboarding documents", + entity: "other", + }, +] as const; + +const COMPANY_ONBOARDING_DESCRIPTION = + "Required documents for external company onboarding. The same set applies to customers, forwarders, transporters, and brokers."; + +const COMPANY_ONBOARDING_FIELDS = [ + { + fileKey: "business_license", + fileLabel: "Business License / Trade License", + helpText: "Verified against the government trade system during registration.", + isRequired: true, + isMultiple: false, + maxFiles: 1, + allowedExtensions: ["pdf", "jpg", "jpeg", "png"], + maxSizeMb: 10, + displayOrder: 1, + }, + { + fileKey: "tin_certificate", + fileLabel: "TIN Certificate", + helpText: "Verified against the TIN registry during registration.", + isRequired: true, + isMultiple: false, + maxFiles: 1, + allowedExtensions: ["pdf", "jpg", "jpeg", "png"], + maxSizeMb: 10, + displayOrder: 2, + }, + { + fileKey: "national_id_passport", + fileLabel: "National ID / Passport", + helpText: "Verified against the National ID API during registration.", + isRequired: true, + isMultiple: false, + maxFiles: 1, + allowedExtensions: ["pdf", "jpg", "jpeg", "png"], + maxSizeMb: 10, + displayOrder: 3, + }, +] as const; + +@Injectable() +export class FileUploadSettingsSeeder { + private readonly logger = new Logger(FileUploadSettingsSeeder.name); + + constructor(private readonly dataSource: DataSource) {} + + async run() { + await this.dataSource.transaction(async (manager) => { + const settingRepository = manager.getRepository(FileUploadSetting); + const fieldRepository = manager.getRepository(FileUploadField); + + for (const documentSetting of COMPANY_ONBOARDING_DOCUMENTS) { + await settingRepository.upsert( + { + code: documentSetting.code, + label: documentSetting.label, + description: COMPANY_ONBOARDING_DESCRIPTION, + entity: documentSetting.entity, + }, + { + conflictPaths: { code: true }, + }, + ); + + const setting = await settingRepository.findOne({ + where: { code: documentSetting.code }, + select: { id: true, code: true }, + }); + + if (!setting) { + throw new Error(`file_upload_setting_seed_failed:${documentSetting.code}`); + } + + await fieldRepository.delete({ settingId: setting.id }); + + await fieldRepository.insert( + COMPANY_ONBOARDING_FIELDS.map((field, index) => ({ + settingId: setting.id, + fileKey: field.fileKey, + fileLabel: field.fileLabel, + helpText: field.helpText, + isRequired: field.isRequired, + isMultiple: field.isMultiple, + maxFiles: field.maxFiles, + allowedExtensions: [...field.allowedExtensions], + maxSizeMb: field.maxSizeMb, + displayOrder: field.displayOrder ?? index + 1, + })), + ); + } + }); + + this.logger.log( + "Ensured company onboarding file upload settings for external companies", + ); + } +} diff --git a/apps/edr-freight-web/backoffice/src/hooks/rule-engine/useRuleEngine.ts b/apps/edr-freight-web/backoffice/src/hooks/rule-engine/useRuleEngine.ts index 858975a41..a8f2536b9 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/rule-engine/useRuleEngine.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/rule-engine/useRuleEngine.ts @@ -76,9 +76,7 @@ export const useContainerTypeOptions = ( enabled = true, ) => useQuery({ - queryKey: QUERY_KEYS.RULE_ENGINE.selectOptions("container-types", { - includeNone, - }), + queryKey: api.ruleEngine.list.queryKey(), queryFn: () => api.ruleEngine.list.call({ resource: "container-types", diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx index cab3bfd1f..147668d91 100644 --- a/apps/edr-freight-web/portal/src/App.tsx +++ b/apps/edr-freight-web/portal/src/App.tsx @@ -14,11 +14,13 @@ import { Home, Loader2, User, + Settings, } from "lucide-react"; import useAuth from "./hooks/useAuth"; import ProfilePage from "./pages/ProfilePage"; +import SettingsPage from "./pages/SettingsPage"; import MyPortalPage from "./pages/MyPortalPage"; import EDRFreightLandingPage from "./pages/EDRFreightLandingPage"; import SignupPage from "./pages/accounts/SignupPage"; @@ -40,12 +42,14 @@ const sidebarItems: SidebarItem[] = [ { label: "Tracking", href: "/tracking", icon: }, { label: "Billing", href: "/billing", icon: }, { label: "Profile", href: "/profile", icon: }, + { label: "Settings", href: "/settings", icon: }, ]; const App = () => { const navigate = useNavigate(); const location = useLocation(); const { user, isPending, logout, customer, customerQuery } = useAuth(); + useEffect(() => { if (isPending) return; const isInProtectedRoutes = sidebarItems.find((item) => @@ -107,6 +111,7 @@ const App = () => { } /> } /> } /> + } /> } /> diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts index 032f06fee..b2dd0f254 100644 --- a/apps/edr-freight-web/portal/src/constants/URLS.ts +++ b/apps/edr-freight-web/portal/src/constants/URLS.ts @@ -82,6 +82,8 @@ export const URL_CONSTANTS = { COMPANIES_API: { GET_INFO: "/api/companies/getInfo", CREATE: "/api/companies/create", + PROFILE: "/api/companies/profile", + DOCUMENTS: (id: string) => `/api/companies/${id}/documents`, }, BOOKINGS: { diff --git a/apps/edr-freight-web/portal/src/hooks/useAuth.ts b/apps/edr-freight-web/portal/src/hooks/useAuth.ts index 8d58d3ada..7f4c02585 100644 --- a/apps/edr-freight-web/portal/src/hooks/useAuth.ts +++ b/apps/edr-freight-web/portal/src/hooks/useAuth.ts @@ -29,7 +29,6 @@ const useAuth = () => { const authQuery = useQuery( api.auth.getMyInfo.queryOptions({ - enabled: !!getCookie("auth-token"), retry: false, staleTime: 10 * 60 * 1000, }), diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx index 440617f3e..6cb111bb7 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { ArrowRight, @@ -14,6 +14,8 @@ import { Plus, Receipt, Truck, + UploadCloud, + X, } from "lucide-react"; import { @@ -48,6 +50,7 @@ export default function MyPortalPage() { const outstandingInvoices = myInvoices.filter( (inv) => inv.status === "Sent" || inv.status === "Overdue", ); + const [dismissed, setDismissed] = useState(false); const totalOutstanding = outstandingInvoices .filter((inv) => inv.currency === "USD") .reduce((sum, inv) => sum + inv.amount, 0); @@ -61,6 +64,34 @@ export default function MyPortalPage() { return (
+ {/* Documents banner */} + {!me.documentsComplete && !dismissed && ( +
+ +
+

Upload your documents

+

+ To enable all account features, please upload your Business + License, TIN Certificate, and National ID / Passport. +

+ + Upload now + +
+ +
+ )} + {/* Welcome banner */}
diff --git a/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx b/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx index e0d364bc1..9c4354723 100644 --- a/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx +++ b/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx @@ -1,135 +1,46 @@ -import { useMemo } from "react"; -import { - User, - Building2, - Phone, - Mail, - MapPin, - ShieldCheck, - Briefcase, - UserCheck, - Building, - Globe, - Fingerprint, - FileCheck, - Settings2, - ExternalLink, -} from "lucide-react"; -import useAuth from "@/hooks/useAuth"; -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - CardAction, - Badge, - Separator, - SmartFileInput, - Button, -} from "@edr/ui-common"; -import type { IFileUploadSetting } from "@edr/types/freight"; -import { cn } from "@/lib/utils"; +import { User, Building2, Phone, Mail, MapPin, ShieldCheck, Briefcase, UserCheck, Fingerprint, FileCheck, Globe, Building } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "@/services/api"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, Badge, Separator } from "@edr/ui-common"; + +function InfoItem({ icon, label, value }: { icon?: React.ReactNode; label: string; value?: string | null }) { + return ( +
+ {icon &&
{icon}
} +
+

{label}

+

{value || "—"}

+
+
+ ); +} export default function ProfilePage() { - const { user, customer, isPending } = useAuth(); - - const documentSettings = useMemo(() => ({ - id: "profile-docs", - code: "customer_documents", - label: "Customer Documents", - entity: "customer", - createdAt: new Date(), - updatedAt: new Date(), - fields: [ - { - id: "doc-tin", - settingId: "profile-docs", - fileKey: "tin_certificate", - fileLabel: "TIN Certificate", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: [".pdf", ".jpg", ".jpeg", ".png"], - maxSizeMb: 5, - order: 1, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: "doc-license", - settingId: "profile-docs", - fileKey: "business_license", - fileLabel: "Business/Investment License", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: [".pdf", ".jpg", ".jpeg", ".png"], - maxSizeMb: 5, - order: 2, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: "doc-reg", - settingId: "profile-docs", - fileKey: "registration_certificate", - fileLabel: "Business Registration Certificate", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: [".pdf", ".jpg", ".jpeg", ".png"], - maxSizeMb: 5, - order: 3, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: "doc-id", - settingId: "profile-docs", - fileKey: "national_id", - fileLabel: "National ID", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: [".pdf", ".jpg", ".jpeg", ".png"], - maxSizeMb: 5, - order: 4, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: "doc-poa", - settingId: "profile-docs", - fileKey: "power_of_attorney", - fileLabel: "Power of Attorney", - isRequired: false, - isMultiple: false, - maxFiles: 1, - allowedExtensions: [".pdf", ".jpg", ".jpeg", ".png"], - maxSizeMb: 5, - order: 5, - createdAt: new Date(), - updatedAt: new Date(), - }, - ], - }), []); + const { data: profile, isPending } = useQuery( + api.companies.getProfile.queryOptions(), + ); if (isPending) { return (
-
+
); } - const displayName = user?.name?.en || user?.username || user?.email || "User"; + if (!profile) { + return ( +
+

No company profile found.

+
+ ); + } return ( -
-
- {/* Header Section */} -
+
+
+
+ {/* Header */}
@@ -137,7 +48,7 @@ export default function ProfilePage() {

- {displayName} + {profile.companyName}

Verified @@ -145,182 +56,129 @@ export default function ProfilePage() {

- {customer?.companyName || "No Company Linked"} + {profile.companyName}

-
- -
-
- + -
- {/* Left Column - Personal & Company Info */} -
-
- {/* Personal Details Card */} +
+ {/* Left Column */} +
+
+ {/* Company Details */} + + + + + Company Details + + Business registration information + + + } label="Location" value={profile.companyLocation} /> + } label="Address" value={profile.companyAddress} /> + } label="TIN Number" value={profile.tinNumber} /> + } label="FAN Number" value={profile.fanNumber} /> + } label="Email" value={profile.companyEmail} /> + } label="Phone" value={profile.companyPhone} /> + + + + {/* Personal Details (from ExternalProfile) */} + + + + + Profile Details + + Your linked user profile + + + } label="Profile" value="Primary Contact" /> + + +
+ + {/* Personnel Card */} - - Personal Details + + Key Personnel - Your account contact information - - - + Management and contact persons - - } label="Email Address" value={user?.email} /> - } label="Phone Number" value={user?.phoneNumber} /> - } label="Username" value={user?.username} /> + +
+

+ Contact Person +

+
+ + +
+
+
+

+ General Manager +

+
+ + + +
+
- {/* Company Details Card */} - - - - - Company Details - - Business registration information - - - } label="Location" value={customer?.companyLocation} /> - } label="Address" value={customer?.companyAddress} /> - } label="TIN Number" value={customer?.tinNumber} /> - } label="FAN Number" value={customer?.fanNumber} /> + {/* Power of Attorney */} + {profile.poaName && ( + + + + + Power of Attorney + + Authorized representative details + + + + + + + + + )} +
+ + {/* Right Column */} +
+ +
+ +
+ +

Secure Account

+

+ Your information is protected by enterprise-grade security. + Contact support for verified information updates. +

+
- - {/* Personnel Card */} - - - - - Key Personnel - - Management and contact persons - - -
-

- Contact Person -

-
- - -
-
-
-

- General Manager -

-
- - - -
-
-
-
- - {/* Power of Attorney Section (Conditional) */} - {customer?.poaName && ( - - - - - Power of Attorney - - Authorized representative details - - - - - - - - - )} -
- - {/* Right Column - Documents */} -
- - - - - Documents - - Manage required business documents - - - - - - - -
- -
- -

Secure Account

-

- Your information is protected by enterprise-grade security. - Contact support for verified information updates. -

-
- -
-
-
); } - -function InfoItem({ - icon, - label, - value, -}: { - icon?: React.ReactNode; - label: string; - value?: string | null; -}) { - return ( -
- {icon && ( -
- {icon} -
- )} -
-

- {label} -

-

- {value || "—"} -

-
-
- ); -} diff --git a/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx b/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx new file mode 100644 index 000000000..faec1b5b2 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx @@ -0,0 +1,617 @@ +import { useState, useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Building2, + User, + Briefcase, + UserCheck, + FileCheck, + Loader2, + Save, + UploadCloud, + CheckCircle2, + XCircle, +} from "lucide-react"; +import { api } from "@/services/api"; +import { companiesService } from "@/services/companies.service"; +import PhoneInput from "@/components/auth/PhoneInput"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, + Button, + Input, + Field, + FieldLabel, + FieldError, + FieldGroup, + SmartFileInput, + Badge, +} from "@edr/ui-common"; +import { cn } from "@/lib/utils"; + +type SettingsTab = + | "company" + | "contact" + | "gm" + | "poa" + | "documents"; + +const settingsSchema = z.object({ + companyName: z.string().min(1, "Company name is required"), + companyEmail: z.string().email("Invalid email address"), + companyPhone: z.string().min(1, "Company phone is required"), + companyPhoneCountryCode: z.string().min(1, "Country code is required"), + companyLocation: z.string().min(1, "Location is required"), + companyAddress: z.string().min(1, "Address is required"), + tinNumber: z.string().length(10, "TIN must be exactly 10 digits"), + fanNumber: z.string().length(16, "FAN must be exactly 16 digits"), + contactPersonName: z.string().min(1, "Contact person name is required"), + contactPersonPhone: z.string().min(1, "Contact person phone is required"), + contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"), + generalManagerName: z.string().min(1, "GM name is required"), + generalManagerEmail: z.string().email("Invalid GM email"), + generalManagerPhone: z.string().min(1, "GM phone is required"), + generalManagerPhoneCountryCode: z.string().min(1, "Country code is required"), + poaName: z.string().optional(), + poaEmail: z.string().optional(), + poaPhone: z.string().optional(), + poaPhoneCountryCode: z.string().optional(), + poaLocation: z.string().optional(), + poaAddress: z.string().optional(), +}); + +type FormData = z.infer; + +const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [ + { id: "company", label: "Company Profile", icon: }, + { id: "contact", label: "Contact Person", icon: }, + { id: "gm", label: "General Manager", icon: }, + { id: "poa", label: "Power of Attorney", icon: }, + { id: "documents", label: "Documents", icon: }, +]; + +function splitPhone(fullPhone?: string | null): { code: string; number: string } { + 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 }; +} + +export default function SettingsPage() { + const queryClient = useQueryClient(); + const [searchParams, setSearchParams] = useSearchParams(); + const tab = (searchParams.get("tab") as SettingsTab) || "company"; + const setTab = (t: SettingsTab) => { + setSearchParams((prev) => { + const next = new URLSearchParams(prev); + next.set("tab", t); + return next; + }, { replace: true }); + }; + const [documentFiles, setDocumentFiles] = useState< + Record + >({}); + + const profileQuery = useQuery( + api.companies.getProfile.queryOptions(), + ); + + const docSettingQuery = useQuery( + api.fileUploadSettings.getByCode.queryOptions({ + input: { code: "customer_documents" }, + enabled: tab === "documents", + }), + ); + + const profile = profileQuery.data; + + const defaultValues = useMemo((): FormData => { + if (!profile) { + return { + companyName: "", + companyEmail: "", + companyPhone: "", + companyPhoneCountryCode: "+251", + companyLocation: "", + companyAddress: "", + tinNumber: "", + fanNumber: "", + contactPersonName: "", + contactPersonPhone: "", + contactPersonPhoneCountryCode: "+251", + generalManagerName: "", + generalManagerEmail: "", + generalManagerPhone: "", + generalManagerPhoneCountryCode: "+251", + poaName: "", + poaEmail: "", + poaPhone: "", + poaPhoneCountryCode: "+251", + poaLocation: "", + poaAddress: "", + }; + } + const contactPhone = splitPhone(profile.contactPersonPhone); + const gmPhone = splitPhone(profile.generalManagerPhone); + const poaPhone = splitPhone(profile.poaPhone); + return { + companyName: profile.companyName, + companyEmail: profile.companyEmail ?? "", + companyPhone: profile.companyPhone ?? "", + companyPhoneCountryCode: splitPhone(profile.companyPhone).code, + companyLocation: profile.companyLocation, + companyAddress: profile.companyAddress ?? "", + tinNumber: profile.tinNumber, + fanNumber: profile.fanNumber ?? "", + contactPersonName: profile.contactPersonName ?? "", + contactPersonPhone: contactPhone.number, + contactPersonPhoneCountryCode: contactPhone.code, + generalManagerName: profile.generalManagerName ?? "", + generalManagerEmail: profile.generalManagerEmail ?? "", + generalManagerPhone: gmPhone.number, + generalManagerPhoneCountryCode: gmPhone.code, + poaName: profile.poaName ?? "", + poaEmail: profile.poaEmail ?? "", + poaPhone: poaPhone.number, + poaPhoneCountryCode: poaPhone.code, + poaLocation: profile.poaLocation ?? "", + poaAddress: profile.poaAddress ?? "", + }; + }, [profile]); + + const { + register, + handleSubmit, + reset, + formState: { errors, isDirty }, + } = useForm({ + resolver: zodResolver(settingsSchema), + values: defaultValues, + }); + + const updateMutation = useMutation({ + mutationFn: (data: FormData) => + api.companies.updateProfile.call({ + companyName: data.companyName, + companyEmail: data.companyEmail, + companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`, + companyLocation: data.companyLocation, + companyAddress: data.companyAddress, + tin: data.tinNumber, + fanNumber: data.fanNumber, + contactPersonName: data.contactPersonName, + contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`, + generalManagerName: data.generalManagerName, + generalManagerEmail: data.generalManagerEmail, + generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`, + poaName: data.poaName || undefined, + poaPhone: + data.poaPhone && data.poaPhoneCountryCode + ? `${data.poaPhoneCountryCode}${data.poaPhone}` + : undefined, + poaEmail: data.poaEmail || undefined, + poaLocation: data.poaLocation || undefined, + poaAddress: data.poaAddress || undefined, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: api.companies.getProfile.queryKey(), + }); + }, + }); + + const docUploadMutation = useMutation({ + mutationFn: (files: Record) => + companiesService.uploadDocuments(profile!.companyId, files), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: api.companies.getProfile.queryKey(), + }); + }, + }); + + const isPending = profileQuery.isPending || updateMutation.isPending || docUploadMutation.isPending; + + if (profileQuery.isPending) { + return ( +
+
+
+ ); + } + + if (!profile) { + return ( +
+

No company profile found.

+
+ ); + } + + const onSubmit = (data: FormData) => { + updateMutation.mutate(data); + }; + + return ( +
+
+
+

+ Account Settings +

+

+ Manage your company profile, personnel, and documents +

+
+ + Verified + +
+ + {/* Tab Bar */} +
+ {TABS.map((t) => ( + + ))} +
+ +
+ + + + {tab === "company" && <> Company Profile} + {tab === "contact" && <> Contact Person} + {tab === "gm" && <> General Manager} + {tab === "poa" && <> Power of Attorney} + {tab === "documents" && <> Documents} + + + {tab === "company" && "Edit your company registration details"} + {tab === "contact" && "Manage the primary contact person for your account"} + {tab === "gm" && "Manage the general manager information"} + {tab === "poa" && "Power of Attorney details are optional"} + {tab === "documents" && "Upload and manage required business documents"} + + + + + + {/* Company Profile Tab */} + {tab === "company" && ( + <> + + Company Name + + + + +
+ + Company Email + + + + + +
+ +
+ + Location + + + + + + Address + + + +
+ +
+ + TIN Number (10 digits) + + + + + + FAN Number (16 digits) + + + +
+ + )} + + {/* Contact Person Tab */} + {tab === "contact" && ( + <> + + Full Name + + + + + + + )} + + {/* General Manager Tab */} + {tab === "gm" && ( + <> + + Full Name + + + + +
+ + Email Address + + + + + +
+ + )} + + {/* Power of Attorney Tab */} + {tab === "poa" && ( + <> +

+ Power of Attorney details are optional. Fill them in if you have + an authorized representative, or leave blank. +

+ + + PoA Full Name + + + + +
+ + PoA Email + + + + + +
+ +
+ + PoA Location + + + + + + PoA Address + + + +
+ + )} + + {/* Documents Tab */} + {tab === "documents" && ( + <> + {docSettingQuery.isLoading ? ( +
+ +
+ ) : !docSettingQuery.data ? ( +

+ No document requirements configured for your account. +

+ ) : ( + + )} + + {docSettingQuery.data && ( +
+
+ {docUploadMutation.isSuccess && ( + + + Documents uploaded successfully + + )} + {docUploadMutation.isError && ( + + + Upload failed + + )} +
+ +
+ )} + + )} +
+
+ + {tab !== "documents" && ( + +
+ {updateMutation.isSuccess && ( + + + Saved successfully + + )} + {updateMutation.isError && ( + + + Save failed + + )} +
+
+ + +
+
+ )} +
+
+
+ ); +} diff --git a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx index eb8d5a343..e86c30132 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx @@ -14,10 +14,8 @@ import { ChevronLeft, UploadCloud, } from "lucide-react"; -import type { OnboardingUserType } from "./types"; import type { AuthUser } from "@/types/auth"; import type { CreateCompanyPayload } from "@/services/companies.service"; -import type { FileUploadSetting } from "@/types/fileUploadSettings"; import PhoneInput from "@/components/auth/PhoneInput"; import { Button, @@ -30,7 +28,7 @@ import { } from "@edr/ui-common"; import { api } from "@/services/api"; -type CompanyStep = "company" | "personnel" | "poa" | "documents"; +type CompanyStep = "company" | "personnel" | "poa" | "documents" | "confirm"; const onboardingSchema = z.object({ companyName: z.string().min(1, "Company name is required"), @@ -85,6 +83,7 @@ const stepFields: Record = { ], poa: [], documents: [], + confirm: [], }; function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload { @@ -116,26 +115,34 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload { } export default function CompanyProfileForm({ - userType, + documentSettingCode, + documentFiles: controlledFiles, + onDocumentFilesChange, user, onSubmit, isPending, onBack, }: { - userType: OnboardingUserType; + documentSettingCode: string; + documentFiles?: Record; + onDocumentFilesChange?: ( + files: Record, + ) => void; user: AuthUser; onSubmit: (data: CreateCompanyPayload) => void; isPending: boolean; onBack: () => void; }) { const [step, setStep] = useState("company"); - const [documentFiles, setDocumentFiles] = useState< + const [internalFiles, setInternalFiles] = useState< Record >({}); + const documentFiles = controlledFiles ?? internalFiles; + const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles; - const { data: uploadSettings = [], isLoading: loadingDocuments } = useQuery( - api.fileUploadSettings.getByEntity.queryOptions({ - input: { entity: "customer" }, + const { data: uploadSetting, isLoading: loadingDocuments } = useQuery( + api.fileUploadSettings.getByCode.queryOptions({ + input: { code: documentSettingCode }, refetchOnMount: false, }), ); @@ -144,6 +151,7 @@ export default function CompanyProfileForm({ register, handleSubmit, trigger, + watch, formState: { errors }, } = useForm({ resolver: zodResolver(onboardingSchema), @@ -173,18 +181,20 @@ export default function CompanyProfileForm({ }, }); - const hasDocuments = uploadSettings.length > 0; + const formValues = watch(); + const hasDocuments = Boolean(uploadSetting?.fields?.length); + const totalSteps = 5; const nextStep = async () => { if (step === "poa") { - if (hasDocuments) { - setStep("documents"); - } else { - handleSubmit((data) => onSubmit(buildPayload(data, user)))(); - } + setStep("documents"); return; } if (step === "documents") { + setStep("confirm"); + return; + } + if (step === "confirm") { handleSubmit((data) => onSubmit(buildPayload(data, user)))(); return; } @@ -201,8 +211,10 @@ export default function CompanyProfileForm({ setStep("company"); } else if (step === "poa") { setStep("personnel"); - } else { + } else if (step === "documents") { setStep("poa"); + } else { + setStep("documents"); } }; @@ -228,31 +240,41 @@ export default function CompanyProfileForm({ } active={step === "personnel"} - completed={step === "poa"} + completed={ + step === "poa" || step === "documents" || step === "confirm" + } /> } active={step === "poa"} - completed={hasDocuments ? step === "documents" : step === "personnel"} + completed={step === "documents" || step === "confirm"} + /> + } + active={step === "documents"} + completed={step === "confirm"} + /> + } + active={step === "confirm"} + completed={false} /> - {hasDocuments && ( - } - active={step === "documents"} - completed={false} - /> - )}

- {step === "company" && `Step 1 of ${hasDocuments ? 4 : 3} — Company Information`} - {step === "personnel" && `Step 2 of ${hasDocuments ? 4 : 3} — Personnel Details`} - {step === "poa" && `Step 3 of ${hasDocuments ? 4 : 3} — Power of Attorney (Optional)`} - {step === "documents" && "Step 4 of 4 — Upload Documents (Optional)"} + {step === "company" && + `Step 1 of ${totalSteps} — Company Information`} + {step === "personnel" && + `Step 2 of ${totalSteps} — Personnel Details`} + {step === "poa" && + `Step 3 of ${totalSteps} — Power of Attorney (Optional)`} + {step === "documents" && + `Step 4 of ${totalSteps} — Upload Documents (Optional)`} + {step === "confirm" && `Step 5 of ${totalSteps} — Review & Confirm`}

onSubmit(buildPayload(data, user)))} + onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-4" > @@ -353,11 +375,6 @@ export default function CompanyProfileForm({ {step === "personnel" && ( <> -

- Personal details are pulled from your account. Contact and - management info is collected below. -

-

Contact Person @@ -500,62 +517,155 @@ export default function CompanyProfileForm({ {step === "documents" && ( <> -

- Upload required documents for your registration. You can skip - this step and upload later from your account settings. -

- {loadingDocuments ? (
- ) : uploadSettings.length === 0 ? ( + ) : !uploadSetting ? (

No document requirements found for your account type.

) : (
- {uploadSettings.map((setting) => ( - - ))} +
)} )} + + {step === "confirm" && ( +
+
+

+ Review your registration +

+

+ Confirm the company details below before saving. +

+
+ +
+ + + + + + + + + + + + + + + + + +
+
+ )}
- +
+ +
); } +function ReviewRow({ label, value }: { label: string; value?: string | null }) { + return ( +
+
+ {label} +
+
+ {value?.trim() ? value : "Not provided"} +
+
+ ); +} + function StepIcon({ icon, active, diff --git a/apps/edr-freight-web/portal/src/pages/accounts/DjiboutiAgentForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/DjiboutiAgentForm.tsx index 9422dd3e2..2730c878d 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/DjiboutiAgentForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/DjiboutiAgentForm.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; +import { useQuery } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { @@ -10,6 +11,7 @@ import { CheckCircle2, Loader2, ChevronLeft, + UploadCloud, } from "lucide-react"; import type { AuthUser } from "@/types/auth"; import type { CreateCompanyPayload } from "@/services/companies.service"; @@ -21,9 +23,11 @@ import { FieldLabel, FieldError, FieldGroup, + SmartFileInput, } from "@edr/ui-common"; +import { api } from "@/services/api"; -type DjiboutiStep = "company" | "representative"; +type DjiboutiStep = "company" | "representative" | "documents" | "confirm"; const djiboutiSchema = z.object({ companyName: z.string().min(1, "Company name is required"), @@ -40,9 +44,23 @@ const djiboutiSchema = z.object({ type FormData = z.infer; -const stepLabels: Record = { - company: "Step 1 of 2 — Company Information", - representative: "Step 2 of 2 — Representative Details", +const stepFields: Record = { + company: [ + "companyName", + "companyEmail", + "companyPhone", + "companyPhoneCountryCode", + "companyLocation", + "companyAddress", + ], + representative: [ + "repName", + "repEmail", + "repPhone", + "repPhoneCountryCode", + ], + documents: [], + confirm: [], }; function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload { @@ -64,22 +82,43 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload { } export default function DjiboutiAgentForm({ + documentSettingCode, + documentFiles: controlledFiles, + onDocumentFilesChange, user, onSubmit, isPending, onBack, }: { + documentSettingCode: string; + documentFiles?: Record; + onDocumentFilesChange?: ( + files: Record, + ) => void; user: AuthUser; onSubmit: (data: CreateCompanyPayload) => void; isPending: boolean; onBack: () => void; }) { const [step, setStep] = useState("company"); + const [internalFiles, setInternalFiles] = useState< + Record + >({}); + const documentFiles = controlledFiles ?? internalFiles; + const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles; + + const { data: uploadSetting, isLoading: loadingDocuments } = useQuery( + api.fileUploadSettings.getByCode.queryOptions({ + input: { code: documentSettingCode }, + refetchOnMount: false, + }), + ); const { register, handleSubmit, trigger, + watch, formState: { errors }, } = useForm({ resolver: zodResolver(djiboutiSchema), @@ -97,32 +136,42 @@ export default function DjiboutiAgentForm({ }, }); + const formValues = watch(); + const hasDocuments = Boolean(uploadSetting?.fields?.length); + const totalSteps = 4; + const nextStep = async () => { if (step === "representative") { + setStep("documents"); + return; + } + if (step === "documents") { + setStep("confirm"); + return; + } + if (step === "confirm") { handleSubmit((data) => onSubmit(buildPayload(data, user)))(); return; } - const fields: (keyof FormData)[] = - step === "company" - ? [ - "companyName", - "companyEmail", - "companyPhone", - "companyPhoneCountryCode", - "companyLocation", - "companyAddress", - ] - : ["repName", "repEmail", "repPhone", "repPhoneCountryCode"]; + const fields = stepFields[step]; const isValid = await trigger(fields); if (!isValid) return; setStep("representative"); }; + const skipDocuments = () => { + setStep("confirm"); + }; + const prevStep = () => { if (step === "company") { onBack(); - } else { + } else if (step === "representative") { setStep("company"); + } else if (step === "documents") { + setStep("representative"); + } else { + setStep("documents"); } }; @@ -143,21 +192,34 @@ export default function DjiboutiAgentForm({ } active={step === "company"} - completed={step === "representative"} + completed={step !== "company"} /> } active={step === "representative"} + completed={step === "documents" || step === "confirm"} + /> + } + active={step === "documents"} + completed={step === "confirm"} + /> + } + active={step === "confirm"} completed={false} />

- {stepLabels[step]} + {step === "company" && `Step 1 of ${totalSteps} — Company Information`} + {step === "representative" && `Step 2 of ${totalSteps} — Representative Details`} + {step === "documents" && `Step 3 of ${totalSteps} — Upload Documents (Optional)`} + {step === "confirm" && `Step 4 of ${totalSteps} — Review & Confirm`}

onSubmit(buildPayload(data, user)))} + onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-4" > @@ -262,35 +324,120 @@ export default function DjiboutiAgentForm({
)} + + {step === "documents" && ( + <> + {loadingDocuments ? ( +
+ +
+ ) : !uploadSetting ? ( +

+ No document requirements found for your account type. +

+ ) : ( +
+ +
+ )} + + )} + + {step === "confirm" && ( +
+
+

+ Review your registration +

+

+ Confirm the company details below before saving. +

+
+ +
+ + + + + + + + +
+
+ )}
- )} - + + +
); } +function ReviewRow({ label, value }: { label: string; value?: string | null }) { + return ( +
+
+ {label} +
+
+ {value?.trim() ? value : "Not provided"} +
+
+ ); +} + function StepIcon({ icon, active, diff --git a/apps/edr-freight-web/portal/src/pages/customers/on_boarding/CustomerOnboardingPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx similarity index 57% rename from apps/edr-freight-web/portal/src/pages/customers/on_boarding/CustomerOnboardingPage.tsx rename to apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx index 994dd751e..418b8bcc4 100644 --- a/apps/edr-freight-web/portal/src/pages/customers/on_boarding/CustomerOnboardingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useForm } from "react-hook-form"; +import { useQuery } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { @@ -11,11 +11,11 @@ import { FileText, CheckCircle2, Loader2, + ChevronLeft, + UploadCloud, } from "lucide-react"; -import useAuth from "@/hooks/useAuth"; -import { api } from "@/services/api"; -import type { CreateCustomerDto } from "@/types/customers"; -import AuthLayout from "@/components/auth/AuthLayout"; +import type { AuthUser } from "@/types/auth"; +import type { CreateCompanyPayload } from "@/services/companies.service"; import PhoneInput from "@/components/auth/PhoneInput"; import { Button, @@ -24,14 +24,13 @@ import { FieldLabel, FieldError, FieldGroup, + SmartFileInput, } from "@edr/ui-common"; -import TransporterOnboarding from "./TransportrOnBoarding"; -import DjiboutiForwardingAgentForm from "./DjiboutiFreightForwardingAgent"; -import ImportExportOnBoarding from "./ImportExportOnBoarding"; +import { api } from "@/services/api"; -type OnboardingStep = "company" | "personnel" | "poa"; +type ForwarderStep = "company" | "personnel" | "poa" | "documents" | "confirm"; -const onboardingSchema = z.object({ +const forwarderSchema = z.object({ companyName: z.string().min(1, "Company name is required"), companyEmail: z.string().email("Invalid email address"), companyPhone: z.string().min(1, "Company phone is required"), @@ -59,9 +58,9 @@ const onboardingSchema = z.object({ poaLocation: z.string().optional(), }); -type FormData = z.infer; +type FormData = z.infer; -const stepFields: Record = { +const stepFields: Record = { company: [ "companyName", "companyEmail", @@ -83,20 +82,79 @@ const stepFields: Record = { "generalManagerPhoneCountryCode", ], poa: [], + documents: [], + confirm: [], }; -export default function CustomerOnboardingPage() { - const queryClient = useQueryClient(); - const { user } = useAuth(); - const [step, setStep] = useState("company"); +function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload { + return { + companyName: data.companyName, + companyEmail: data.companyEmail, + companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`, + companyLocation: data.companyLocation, + companyAddress: data.companyAddress, + tin: data.tinNumber, + vatNumber: data.vatNumber, + fanNumber: data.fanNumber, + attributes: { + contactPersonName: data.contactPersonName, + contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`, + generalManagerName: data.generalManagerName, + generalManagerEmail: data.generalManagerEmail, + generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`, + poaName: data.poaName || undefined, + poaPhone: + data.poaPhone && data.poaPhoneCountryCode + ? `${data.poaPhoneCountryCode}${data.poaPhone}` + : undefined, + poaAddress: data.poaAddress || undefined, + poaEmail: data.poaEmail || undefined, + poaLocation: data.poaLocation || undefined, + }, + }; +} + +export default function ForwarderForm({ + documentSettingCode, + documentFiles: controlledFiles, + onDocumentFilesChange, + user, + onSubmit, + isPending, + onBack, +}: { + documentSettingCode: string; + documentFiles?: Record; + onDocumentFilesChange?: ( + files: Record, + ) => void; + user: AuthUser; + onSubmit: (data: CreateCompanyPayload) => void; + isPending: boolean; + onBack: () => void; +}) { + const [step, setStep] = useState("company"); + const [internalFiles, setInternalFiles] = useState< + Record + >({}); + const documentFiles = controlledFiles ?? internalFiles; + const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles; + + const { data: uploadSetting, isLoading: loadingDocuments } = useQuery( + api.fileUploadSettings.getByCode.queryOptions({ + input: { code: documentSettingCode }, + refetchOnMount: false, + }), + ); const { register, handleSubmit, trigger, + watch, formState: { errors }, } = useForm({ - resolver: zodResolver(onboardingSchema), + resolver: zodResolver(forwarderSchema), defaultValues: { companyName: "", companyEmail: "", @@ -123,20 +181,21 @@ export default function CustomerOnboardingPage() { }, }); - const createCustomerMutation = useMutation({ - mutationFn: (payload: CreateCustomerDto) => - api.customers.create.call(payload), - onSuccess: () => { - if (user) - queryClient.invalidateQueries({ - queryKey: api.customers.getByUserId.queryKey({ id: user.id }), - }); - }, - }); + const formValues = watch(); + const hasDocuments = Boolean(uploadSetting?.fields?.length); + const totalSteps = 5; const nextStep = async () => { if (step === "poa") { - handleSubmit(onSubmit)(); + setStep("documents"); + return; + } + if (step === "documents") { + setStep("confirm"); + return; + } + if (step === "confirm") { + handleSubmit((data) => onSubmit(buildPayload(data, user)))(); return; } const fields = stepFields[step]; @@ -145,69 +204,36 @@ export default function CustomerOnboardingPage() { setStep(step === "company" ? "personnel" : "poa"); }; - const prevStep = () => { - if (step === "personnel") setStep("company"); - else if (step === "poa") setStep("personnel"); + const skipDocuments = () => { + setStep("confirm"); }; - const onSubmit = async (data: FormData) => { - const nameParts = (user?.name?.en ?? "").split(" "); - const payload: CreateCustomerDto = { - userId: user!.id, - firstName: nameParts[0] || "", - lastName: nameParts.slice(-1)[0] || "", - email: user!.email, - phone: user!.phoneNumber, - companyName: data.companyName, - companyEmail: data.companyEmail, - companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`, - companyLocation: data.companyLocation, - companyAddress: data.companyAddress, - contactPersonName: data.contactPersonName, - contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`, - tinNumber: data.tinNumber, - vatNumber: data.vatNumber, - fanNumber: data.fanNumber, - generalManagerName: data.generalManagerName, - generalManagerEmail: data.generalManagerEmail, - generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`, - poaName: data.poaName || undefined, - poaPhone: - data.poaPhone && data.poaPhoneCountryCode - ? `${data.poaPhoneCountryCode}${data.poaPhone}` - : undefined, - poaAddress: data.poaAddress || undefined, - poaEmail: data.poaEmail || undefined, - poaLocation: data.poaLocation || undefined, - }; - createCustomerMutation.mutate(payload); + const prevStep = () => { + if (step === "company") { + onBack(); + } else if (step === "personnel") { + setStep("company"); + } else if (step === "poa") { + setStep("personnel"); + } else if (step === "documents") { + setStep("poa"); + } else { + setStep("documents"); + } }; return ( - + <> +
+ - - {/* */} - {/* */} - {/*
} active={step === "personnel"} - completed={step === "poa"} + completed={step === "poa" || step === "documents" || step === "confirm"} /> } active={step === "poa"} + completed={step === "documents" || step === "confirm"} + /> + } + active={step === "documents"} + completed={step === "confirm"} + /> + } + active={step === "confirm"} completed={false} />

- {step === "company" && "Step 1 of 3 — Company Information"} - {step === "personnel" && "Step 2 of 3 — Personnel Details"} - {step === "poa" && "Step 3 of 3 — Power of Attorney (Optional)"} + {step === "company" && + `Step 1 of ${totalSteps} — Company Information`} + {step === "personnel" && + `Step 2 of ${totalSteps} — Personnel Details`} + {step === "poa" && + `Step 3 of ${totalSteps} — Power of Attorney (Optional)`} + {step === "documents" && + `Step 4 of ${totalSteps} — Upload Documents (Optional)`} + {step === "confirm" && `Step 5 of ${totalSteps} — Review & Confirm`}

-
*/} +
- {/*
+ e.preventDefault()} + className="flex flex-col gap-4" + > {step === "company" && ( <> @@ -332,11 +377,6 @@ export default function CustomerOnboardingPage() { {step === "personnel" && ( <> -

- Personal details are pulled from your account. Contact and - management info is collected below. -

-

Contact Person @@ -418,88 +458,212 @@ export default function CustomerOnboardingPage() { {step === "poa" && ( <>

- Power of Attorney details are optional. Skip if not applicable. + Power of Attorney details are optional. Fill them in if you have + them, or skip to continue.

- + PoA Name +
- + PoA Email +
- + PoA Location + - + PoA Address +
)} + + {step === "documents" && ( + <> + {loadingDocuments ? ( +
+ +
+ ) : !uploadSetting ? ( +

+ No document requirements found for your account type. +

+ ) : ( +
+ +
+ )} + + )} + + {step === "confirm" && ( +
+
+

+ Review your registration +

+

+ Confirm the company details below before saving. +

+
+ +
+ + + + + + + + + + + + + + + + + +
+
+ )}
- - )} - + + +

- */} - + + + ); +} + +function ReviewRow({ label, value }: { label: string; value?: string | null }) { + return ( +
+
+ {label} +
+
+ {value?.trim() ? value : "Not provided"} +
+
); } diff --git a/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx index a53b37ee5..ec1d724f8 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { ArrowDownToLine, ArrowUpFromLine, @@ -10,9 +10,11 @@ import { } from "lucide-react"; import useAuth from "@/hooks/useAuth"; import { api } from "@/services/api"; +import { companiesService } from "@/services/companies.service"; import type { CreateCompanyPayload } from "@/services/companies.service"; import AuthLayout from "@/components/auth/AuthLayout"; import CompanyProfileForm from "./CompanyProfileForm"; +import ForwarderForm from "./ForwarderForm"; import DjiboutiAgentForm from "./DjiboutiAgentForm"; import TransporterForm from "./TransporterForm"; import type { OnboardingUserType } from "./types"; @@ -113,17 +115,21 @@ const PREFLIGHT_LEFT = { }, }; +const DOCUMENT_SETTING_CODE_MAP: Record = { + importer: "company_onboarding_documents_customer", + exporter: "company_onboarding_documents_customer", + "freight-forwarder-et": "company_onboarding_documents_forwarder", + "freight-forwarder-dj": "company_onboarding_documents_forwarder_dj", + transporter: "company_onboarding_documents_transporter", +}; + export default function OnboardingPage() { const queryClient = useQueryClient(); const { user } = useAuth(); const [userType, setUserType] = useState(null); - - useQuery( - api.fileUploadSettings.getByEntity.queryOptions({ - input: { entity: "customer" }, - refetchOnMount: false, - }), - ); + const [documentFiles, setDocumentFiles] = useState< + Record + >({}); const COMPANY_TYPE_MAP: Record = { importer: "customer", @@ -136,8 +142,14 @@ export default function OnboardingPage() { const createCompanyMutation = useMutation({ mutationFn: (payload: CreateCompanyPayload) => api.companies.create.call(payload), - onSuccess: () => { - queryClient.invalidateQueries({ + onSuccess: async (data) => { + const hasFiles = Object.values(documentFiles).some( + (f) => f !== null && (Array.isArray(f) ? f.length > 0 : true), + ); + if (hasFiles) { + await companiesService.uploadDocuments(data.company.id, documentFiles); + } + await queryClient.invalidateQueries({ queryKey: api.companies.getInfo.queryKey(), }); }, @@ -237,6 +249,9 @@ export default function OnboardingPage() { {userType === "transporter" ? ( ) : userType === "freight-forwarder-dj" ? ( + ) : userType === "freight-forwarder-et" ? ( + ) : ( { - if (data.truckType === "Casoni" && (!data.plateNumber2 || data.plateNumber2.trim().length === 0)) { + if ( + data.truckType === "Casoni" && + (!data.plateNumber2 || data.plateNumber2.trim().length === 0) + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["plateNumber2"], @@ -77,19 +89,42 @@ function buildPayload(data: FormData, user: AuthUser): CreateCompanyPayload { } export default function TransporterForm({ + documentSettingCode, + documentFiles: controlledFiles, + onDocumentFilesChange, user, onSubmit, isPending, onBack, }: { + documentSettingCode: string; + documentFiles?: Record; + onDocumentFilesChange?: ( + files: Record, + ) => void; user: AuthUser; onSubmit: (data: CreateCompanyPayload) => void; isPending: boolean; onBack: () => void; }) { + const [step, setStep] = useState("vehicle"); + const [internalFiles, setInternalFiles] = useState< + Record + >({}); + const documentFiles = controlledFiles ?? internalFiles; + const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles; + + const { data: uploadSetting, isLoading: loadingDocuments } = useQuery( + api.fileUploadSettings.getByCode.queryOptions({ + input: { code: documentSettingCode }, + refetchOnMount: false, + }), + ); + const { register, handleSubmit, + trigger, watch, control, formState: { errors }, @@ -108,187 +143,341 @@ export default function TransporterForm({ const truckType = watch("truckType"); const isCasoni = truckType === "Casoni"; + const formValues = watch(); + const hasDocuments = Boolean(uploadSetting?.fields?.length); + const totalSteps = 3; + + const nextStep = async () => { + if (step === "documents") { + setStep("confirm"); + return; + } + if (step === "confirm") { + handleSubmit((data) => onSubmit(buildPayload(data, user)))(); + return; + } + const fields: (keyof FormData)[] = [ + "tinNumber", + "fanNumber", + "truckType", + "plateNumber", + "vehicleModel", + "yearOfManufacturing", + ]; + const isValid = await trigger(fields); + if (!isValid) return; + setStep("documents"); + }; + + const skipDocuments = () => { + setStep("confirm"); + }; + + const prevStep = () => { + if (step === "vehicle") { + onBack(); + } else if (step === "documents") { + setStep("vehicle"); + } else { + setStep("documents"); + } + }; return ( <>
-
-
- -
+
+
+ } + active={step === "vehicle"} + completed={step !== "vehicle"} + /> + } + active={step === "documents"} + completed={step === "confirm"} + /> + } + active={step === "confirm"} + completed={false} + />

- Transporter Registration + {step === "vehicle" && `Step 1 of ${totalSteps} — Vehicle Information`} + {step === "documents" && `Step 2 of ${totalSteps} — Upload Documents (Optional)`} + {step === "confirm" && `Step 3 of ${totalSteps} — Review & Confirm`}

onSubmit(buildPayload(data, user)))} + onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-4" > - {/* Personal Info (read-only) */} -
-
- - Account Holder + {step === "vehicle" && ( + <> +
+ + TIN Number (10 digits) + + + + + + FAN Number (16 digits) + + + +
+ +
+ +

+ Vehicle / Truck Information +

+ + ( + + Truck Type + + + + )} + /> + +
+ + Plate Number{isCasoni ? " (Front)" : ""} + + + + + {isCasoni && ( + + Plate Number (Trailer) + + + + )} + + {!isCasoni && ( + + Vehicle Model + + + + )} +
+ +
+ {isCasoni && ( + + Vehicle Model + + + + )} + + + Year of Manufacturing + + + +
+ + )} + + {step === "documents" && ( + <> + {loadingDocuments ? ( +
+ +
+ ) : !uploadSetting ? ( +

+ No document requirements found for your account type. +

+ ) : ( +
+ +
+ )} + + )} + + {step === "confirm" && ( +
+
+

+ Review your registration +

+

+ Confirm the details below before saving. +

+
+ +
+ + + + + {formValues.plateNumber2 && ( + + )} + + +
-

- {user.name?.en} — {user.email} — {user.phoneNumber} -

-
- -
- - TIN Number (10 digits) - - - - - - FAN Number (16 digits) - - - -
- -
- -

- Vehicle / Truck Information -

- - ( - - Truck Type - - - - )} - /> - -
- - - Plate Number{isCasoni ? " (Front)" : ""} - - - - - - {isCasoni && ( - - Plate Number (Trailer) - - - - )} - - {!isCasoni && ( - - Vehicle Model - - - - )} -
- -
- {isCasoni && ( - - Vehicle Model - - - - )} - - - Year of Manufacturing - - - -
+ )}
- - )} - + + +
); } + +function ReviewRow({ label, value }: { label: string; value?: string | null }) { + return ( +
+
+ {label} +
+
+ {value?.trim() ? value : "Not provided"} +
+
+ ); +} + +function StepIcon({ + icon, + active, + completed, +}: { + icon: React.ReactNode; + active: boolean; + completed: boolean; +}) { + return ( +
+ {completed ? : icon} +
+ ); +} diff --git a/apps/edr-freight-web/portal/src/pages/customers/customers.mock.ts b/apps/edr-freight-web/portal/src/pages/customers/customers.mock.ts index 92ec6ff80..a1a9bf1c2 100644 --- a/apps/edr-freight-web/portal/src/pages/customers/customers.mock.ts +++ b/apps/edr-freight-web/portal/src/pages/customers/customers.mock.ts @@ -14,6 +14,7 @@ export interface Customer { country: string; address: string; notes: string; + documentsComplete: boolean; } const seedCustomers: Customer[] = [ @@ -30,6 +31,7 @@ const seedCustomers: Customer[] = [ country: "Ethiopia", address: "Bole Road, Sub-City 03, Building 17", notes: "Top-tier importer. Prefers weekly invoicing.", + documentsComplete: false, }, { id: 2, @@ -44,6 +46,7 @@ const seedCustomers: Customer[] = [ country: "Ethiopia", address: "Industrial Park, Zone B, Warehouse 4", notes: "Awaiting compliance documents.", + documentsComplete: false, }, { id: 3, @@ -58,6 +61,7 @@ const seedCustomers: Customer[] = [ country: "Djibouti", address: "Port Quarter, Avenue 26, Block 9", notes: "Account paused since last quarter.", + documentsComplete: true, }, ]; @@ -99,6 +103,7 @@ const generated: Customer[] = extras.map((entry, i) => { country: entry.country, address: `Block ${i + 1}, Street ${10 + i}, Quarter ${(i % 5) + 1}`, notes: `Mock customer #${id}.`, + documentsComplete: i % 3 === 0, }; }); diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts index bb5fcdcc7..baca5cd34 100644 --- a/apps/edr-freight-web/portal/src/services/api.ts +++ b/apps/edr-freight-web/portal/src/services/api.ts @@ -33,6 +33,7 @@ import type { CompanyInfoResponse, CreateCompanyPayload, } from "./companies.service"; +import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; import type { AuthUser, GenerateVerificationCodePayload, @@ -89,40 +90,6 @@ export const api = { logout: endpoint("auth", "logout", authService.logout), }, - customers: { - list: endpoint( - "customers", - "list", - customersService.list, - ), - - get: endpoint<{ id: string }, Customer>("customers", "get", ({ id }) => - customersService.getById(id), - ), - - create: endpoint( - "customers", - "create", - customersService.create, - ), - - update: endpoint<{ id: string; dto: UpdateCustomerDto }, Customer>( - "customers", - "update", - ({ id, dto }) => customersService.update(id, dto), - ), - - remove: endpoint<{ id: string }, void>("customers", "remove", ({ id }) => - customersService.remove(id), - ), - - getByUserId: endpoint<{ id: string }, Customer | null>( - "customers", - "getByUserId", - ({ id }) => customersService.getByUserId(id), - ), - }, - companies: { getInfo: endpoint( "companies", @@ -135,6 +102,18 @@ export const api = { "create", companiesService.create, ), + + getProfile: endpoint( + "companies", + "getProfile", + companiesService.getProfile, + ), + + updateProfile: endpoint( + "companies", + "updateProfile", + companiesService.updateProfile, + ), }, bookings: { diff --git a/apps/edr-freight-web/portal/src/services/companies.service.ts b/apps/edr-freight-web/portal/src/services/companies.service.ts index 4a68b5aa2..359881a9b 100644 --- a/apps/edr-freight-web/portal/src/services/companies.service.ts +++ b/apps/edr-freight-web/portal/src/services/companies.service.ts @@ -2,6 +2,7 @@ import { client } from "@/utils/api"; import { unwrap } from "@/utils/endpoint"; import { URL_CONSTANTS } from "@/constants/URLS"; import type { ApiResponse } from "@/types/apiResponse"; +import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; import { isAxiosError } from "axios"; export interface ExternalProfileResponse { @@ -80,4 +81,37 @@ export const companiesService = { ); return unwrap(response.data); }, + + getProfile: async (): Promise => { + const response = await client.get>( + URL_CONSTANTS.COMPANIES_API.PROFILE, + ); + return unwrap(response.data); + }, + + updateProfile: async (payload: UpdateProfilePayload): Promise => { + const response = await client.patch>( + URL_CONSTANTS.COMPANIES_API.PROFILE, + payload, + ); + return unwrap(response.data); + }, + + uploadDocuments: async ( + companyId: string, + files: Record, + ): Promise => { + const formData = new FormData(); + for (const [fieldName, fileOrFiles] of Object.entries(files)) { + if (!fileOrFiles) continue; + if (Array.isArray(fileOrFiles)) { + for (const f of fileOrFiles) { + formData.append(fieldName, f); + } + } else { + formData.append(fieldName, fileOrFiles); + } + } + await client.post(URL_CONSTANTS.COMPANIES_API.DOCUMENTS(companyId), formData); + }, }; diff --git a/apps/edr-freight-web/portal/src/services/customers.service.ts b/apps/edr-freight-web/portal/src/services/customers.service.ts deleted file mode 100644 index 3d809709d..000000000 --- a/apps/edr-freight-web/portal/src/services/customers.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { client } from "@/utils/api"; -import { unwrap } from "@/utils/endpoint"; -import { URL_CONSTANTS } from "@/constants/URLS"; -import type { ApiResponse } from "@/types/apiResponse"; -import type { - CreateCustomerDto, - Customer, - UpdateCustomerDto, -} from "@/types/customers"; -import { isAxiosError } from "axios"; - -const BASE = URL_CONSTANTS.CUSTOMERS_API.BASE; - -export const customersService = { - list: async (): Promise => { - const response = await client.get>(BASE); - return unwrap(response.data); - }, - - getById: async (id: string): Promise => { - const response = await client.get>( - URL_CONSTANTS.CUSTOMERS_API.BY_ID(id), - ); - return unwrap(response.data); - }, - - getByUserId: async (userId: string): Promise => { - try { - const response = await client.get>( - URL_CONSTANTS.CUSTOMERS_API.BY_USER_ID(userId), - ); - return unwrap(response.data); - } catch (e) { - if (isAxiosError(e) && e.response?.status === 404) { - return null; - } - throw e; - } - }, - - create: async (payload: CreateCustomerDto): Promise => { - const response = await client.post>(BASE, payload); - return unwrap(response.data); - }, - - update: async (id: string, payload: UpdateCustomerDto): Promise => { - const response = await client.patch>( - URL_CONSTANTS.CUSTOMERS_API.BY_ID(id), - payload, - ); - return unwrap(response.data); - }, - - remove: async (id: string): Promise => { - await client.delete(URL_CONSTANTS.CUSTOMERS_API.BY_ID(id)); - }, -}; diff --git a/apps/edr-freight-web/portal/src/types/profile.ts b/apps/edr-freight-web/portal/src/types/profile.ts new file mode 100644 index 000000000..cab34ee23 --- /dev/null +++ b/apps/edr-freight-web/portal/src/types/profile.ts @@ -0,0 +1,43 @@ +export interface ProfileResponse { + companyId: string; + companyName: string; + companyEmail: string | null; + companyPhone: string | null; + companyLocation: string; + companyAddress: string | null; + tinNumber: string; + vatNumber: string | null; + fanNumber: string | null; + contactPersonName: string | null; + contactPersonPhone: string | null; + generalManagerName: string | null; + generalManagerEmail: string | null; + generalManagerPhone: string | null; + poaName: string | null; + poaPhone: string | null; + poaEmail: string | null; + poaLocation: string | null; + poaAddress: string | null; + profileId: string; +} + +export interface UpdateProfilePayload { + companyName?: string; + companyEmail?: string; + companyPhone?: string; + companyLocation?: string; + companyAddress?: string; + tin?: string; + vatNumber?: string; + fanNumber?: string; + contactPersonName?: string; + contactPersonPhone?: string; + generalManagerName?: string; + generalManagerEmail?: string; + generalManagerPhone?: string; + poaName?: string; + poaPhone?: string; + poaEmail?: string; + poaLocation?: string; + poaAddress?: string; +}