mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 07:51:02 +00:00
feat(companies): Introduce API endpoint and UI for company registration document uploads
This commit is contained in:
@@ -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';
|
||||
@@ -22,7 +24,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' })
|
||||
@@ -105,6 +110,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<Express.Multer.File>,
|
||||
) {
|
||||
return this.filesService.uploadMany(companyId, 'companies', files);
|
||||
}
|
||||
|
||||
@Post(':companyId/profiles')
|
||||
@ApiOperation({ summary: 'Add a profile (employee) to a company' })
|
||||
async createProfile(
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -46,6 +46,7 @@ const App = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, isPending, logout, customer, customerQuery } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (isPending) return;
|
||||
const isInProtectedRoutes = sidebarItems.find((item) =>
|
||||
|
||||
@@ -82,6 +82,7 @@ export const URL_CONSTANTS = {
|
||||
COMPANIES_API: {
|
||||
GET_INFO: "/api/companies/getInfo",
|
||||
CREATE: "/api/companies/create",
|
||||
DOCUMENTS: (id: string) => `/api/companies/${id}/documents`,
|
||||
},
|
||||
|
||||
BOOKINGS: {
|
||||
|
||||
@@ -116,21 +116,29 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
||||
|
||||
export default function CompanyProfileForm({
|
||||
documentSettingCode,
|
||||
documentFiles: controlledFiles,
|
||||
onDocumentFilesChange,
|
||||
user,
|
||||
onSubmit,
|
||||
isPending,
|
||||
onBack,
|
||||
}: {
|
||||
documentSettingCode: string;
|
||||
documentFiles?: Record<string, File | File[] | null>;
|
||||
onDocumentFilesChange?: (
|
||||
files: Record<string, File | File[] | null>,
|
||||
) => void;
|
||||
user: AuthUser;
|
||||
onSubmit: (data: CreateCompanyPayload) => void;
|
||||
isPending: boolean;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
const [step, setStep] = useState<CompanyStep>("company");
|
||||
const [documentFiles, setDocumentFiles] = useState<
|
||||
const [internalFiles, setInternalFiles] = useState<
|
||||
Record<string, File | File[] | null>
|
||||
>({});
|
||||
const documentFiles = controlledFiles ?? internalFiles;
|
||||
const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles;
|
||||
|
||||
const { data: uploadSetting, isLoading: loadingDocuments } = useQuery(
|
||||
api.fileUploadSettings.getByCode.queryOptions({
|
||||
|
||||
@@ -83,21 +83,29 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
||||
|
||||
export default function DjiboutiAgentForm({
|
||||
documentSettingCode,
|
||||
documentFiles: controlledFiles,
|
||||
onDocumentFilesChange,
|
||||
user,
|
||||
onSubmit,
|
||||
isPending,
|
||||
onBack,
|
||||
}: {
|
||||
documentSettingCode: string;
|
||||
documentFiles?: Record<string, File | File[] | null>;
|
||||
onDocumentFilesChange?: (
|
||||
files: Record<string, File | File[] | null>,
|
||||
) => void;
|
||||
user: AuthUser;
|
||||
onSubmit: (data: CreateCompanyPayload) => void;
|
||||
isPending: boolean;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
const [step, setStep] = useState<DjiboutiStep>("company");
|
||||
const [documentFiles, setDocumentFiles] = useState<
|
||||
const [internalFiles, setInternalFiles] = useState<
|
||||
Record<string, File | File[] | null>
|
||||
>({});
|
||||
const documentFiles = controlledFiles ?? internalFiles;
|
||||
const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles;
|
||||
|
||||
const { data: uploadSetting, isLoading: loadingDocuments } = useQuery(
|
||||
api.fileUploadSettings.getByCode.queryOptions({
|
||||
|
||||
@@ -116,21 +116,29 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
|
||||
|
||||
export default function ForwarderForm({
|
||||
documentSettingCode,
|
||||
documentFiles: controlledFiles,
|
||||
onDocumentFilesChange,
|
||||
user,
|
||||
onSubmit,
|
||||
isPending,
|
||||
onBack,
|
||||
}: {
|
||||
documentSettingCode: string;
|
||||
documentFiles?: Record<string, File | File[] | null>;
|
||||
onDocumentFilesChange?: (
|
||||
files: Record<string, File | File[] | null>,
|
||||
) => void;
|
||||
user: AuthUser;
|
||||
onSubmit: (data: CreateCompanyPayload) => void;
|
||||
isPending: boolean;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
const [step, setStep] = useState<ForwarderStep>("company");
|
||||
const [documentFiles, setDocumentFiles] = useState<
|
||||
const [internalFiles, setInternalFiles] = useState<
|
||||
Record<string, File | File[] | null>
|
||||
>({});
|
||||
const documentFiles = controlledFiles ?? internalFiles;
|
||||
const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles;
|
||||
|
||||
const { data: uploadSetting, isLoading: loadingDocuments } = useQuery(
|
||||
api.fileUploadSettings.getByCode.queryOptions({
|
||||
|
||||
@@ -10,6 +10,7 @@ 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";
|
||||
@@ -126,6 +127,9 @@ export default function OnboardingPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
const [userType, setUserType] = useState<OnboardingUserType | null>(null);
|
||||
const [documentFiles, setDocumentFiles] = useState<
|
||||
Record<string, File | File[] | null>
|
||||
>({});
|
||||
|
||||
const COMPANY_TYPE_MAP: Record<OnboardingUserType, string> = {
|
||||
importer: "customer",
|
||||
@@ -138,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(),
|
||||
});
|
||||
},
|
||||
@@ -240,6 +250,8 @@ export default function OnboardingPage() {
|
||||
{userType === "transporter" ? (
|
||||
<TransporterForm
|
||||
documentSettingCode={DOCUMENT_SETTING_CODE_MAP[userType]}
|
||||
documentFiles={documentFiles}
|
||||
onDocumentFilesChange={setDocumentFiles}
|
||||
user={user}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={createCompanyMutation.isPending}
|
||||
@@ -248,6 +260,8 @@ export default function OnboardingPage() {
|
||||
) : userType === "freight-forwarder-dj" ? (
|
||||
<DjiboutiAgentForm
|
||||
documentSettingCode={DOCUMENT_SETTING_CODE_MAP[userType]}
|
||||
documentFiles={documentFiles}
|
||||
onDocumentFilesChange={setDocumentFiles}
|
||||
user={user}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={createCompanyMutation.isPending}
|
||||
@@ -256,6 +270,8 @@ export default function OnboardingPage() {
|
||||
) : userType === "freight-forwarder-et" ? (
|
||||
<ForwarderForm
|
||||
documentSettingCode={DOCUMENT_SETTING_CODE_MAP[userType]}
|
||||
documentFiles={documentFiles}
|
||||
onDocumentFilesChange={setDocumentFiles}
|
||||
user={user}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={createCompanyMutation.isPending}
|
||||
@@ -264,6 +280,8 @@ export default function OnboardingPage() {
|
||||
) : (
|
||||
<CompanyProfileForm
|
||||
documentSettingCode={DOCUMENT_SETTING_CODE_MAP[userType]}
|
||||
documentFiles={documentFiles}
|
||||
onDocumentFilesChange={setDocumentFiles}
|
||||
user={user}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={createCompanyMutation.isPending}
|
||||
|
||||
@@ -90,21 +90,29 @@ function buildPayload(data: FormData, user: AuthUser): CreateCompanyPayload {
|
||||
|
||||
export default function TransporterForm({
|
||||
documentSettingCode,
|
||||
documentFiles: controlledFiles,
|
||||
onDocumentFilesChange,
|
||||
user,
|
||||
onSubmit,
|
||||
isPending,
|
||||
onBack,
|
||||
}: {
|
||||
documentSettingCode: string;
|
||||
documentFiles?: Record<string, File | File[] | null>;
|
||||
onDocumentFilesChange?: (
|
||||
files: Record<string, File | File[] | null>,
|
||||
) => void;
|
||||
user: AuthUser;
|
||||
onSubmit: (data: CreateCompanyPayload) => void;
|
||||
isPending: boolean;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
const [step, setStep] = useState<TransporterStep>("vehicle");
|
||||
const [documentFiles, setDocumentFiles] = useState<
|
||||
const [internalFiles, setInternalFiles] = useState<
|
||||
Record<string, File | File[] | null>
|
||||
>({});
|
||||
const documentFiles = controlledFiles ?? internalFiles;
|
||||
const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles;
|
||||
|
||||
const { data: uploadSetting, isLoading: loadingDocuments } = useQuery(
|
||||
api.fileUploadSettings.getByCode.queryOptions({
|
||||
|
||||
@@ -80,4 +80,22 @@ export const companiesService = {
|
||||
);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
uploadDocuments: async (
|
||||
companyId: string,
|
||||
files: Record<string, File | File[] | null>,
|
||||
): Promise<void> => {
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user