This commit is contained in:
@@ -21,6 +21,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
|
persistDocumentFileIds,
|
||||||
companyCategories,
|
companyCategories,
|
||||||
} = useCreateCompanyPresenter({ editMode, defaultValues, id });
|
} = useCreateCompanyPresenter({ editMode, defaultValues, id });
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
categoryItems,
|
categoryItems,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
|
persistDocumentFileIds,
|
||||||
})}
|
})}
|
||||||
register={register}
|
register={register}
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface CompanyFormSectionsOptions {
|
|||||||
categoryItems: ILabelValue[];
|
categoryItems: ILabelValue[];
|
||||||
watch: UseFormWatch<ICreateCompanyCredentials>;
|
watch: UseFormWatch<ICreateCompanyCredentials>;
|
||||||
setValue: UseFormSetValue<ICreateCompanyCredentials>;
|
setValue: UseFormSetValue<ICreateCompanyCredentials>;
|
||||||
|
persistDocumentFileIds?: (ids: string[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionalNumber = (value: unknown) => {
|
const optionalNumber = (value: unknown) => {
|
||||||
@@ -240,6 +241,7 @@ export const createCompanyFormSections = ({
|
|||||||
categoryItems,
|
categoryItems,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
|
persistDocumentFileIds,
|
||||||
}: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [
|
}: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [
|
||||||
{
|
{
|
||||||
id: companyInformationSectionId,
|
id: companyInformationSectionId,
|
||||||
@@ -652,7 +654,14 @@ export const createCompanyFormSections = ({
|
|||||||
<CompanyDocuments
|
<CompanyDocuments
|
||||||
companyId={editMode ? id : undefined}
|
companyId={editMode ? id : undefined}
|
||||||
value={watch("document_file_ids") ?? []}
|
value={watch("document_file_ids") ?? []}
|
||||||
onChange={(ids) => setValue("document_file_ids", ids)}
|
onChange={(ids) =>
|
||||||
|
setValue("document_file_ids", ids, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onRemovePersist={persistDocumentFileIds}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,9 +8,15 @@ interface IProps {
|
|||||||
companyId?: string;
|
companyId?: string;
|
||||||
value: string[];
|
value: string[];
|
||||||
onChange: (ids: string[]) => void;
|
onChange: (ids: string[]) => void;
|
||||||
|
onRemovePersist?: (ids: string[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
|
const CompanyDocuments = ({
|
||||||
|
companyId,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onRemovePersist,
|
||||||
|
}: IProps) => {
|
||||||
const {
|
const {
|
||||||
isUploading,
|
isUploading,
|
||||||
isBusy,
|
isBusy,
|
||||||
@@ -20,13 +26,18 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
onSelectFiles,
|
onSelectFiles,
|
||||||
onRemove,
|
onRemove,
|
||||||
} = useCompanyDocumentsPresenter({ companyId, value, onChange });
|
} = useCompanyDocumentsPresenter({
|
||||||
|
companyId,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onRemovePersist,
|
||||||
|
});
|
||||||
|
|
||||||
const inputId = "company-documents-input";
|
const inputId = "company-documents-input";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="relative overflow-hidden rounded-2xl border border-stroke/70 bg-gradient-to-br from-white via-white to-primary/[0.04] p-4 shadow-theme-xs dark:border-dark-3 dark:from-dark-2 dark:via-dark-2 dark:to-primary/[0.12]">
|
<div className="shadow-theme-xs relative overflow-hidden rounded-2xl border border-stroke/70 bg-gradient-to-br from-white via-white to-primary/[0.04] p-4 dark:border-dark-3 dark:from-dark-2 dark:via-dark-2 dark:to-primary/[0.12]">
|
||||||
<input
|
<input
|
||||||
id={inputId}
|
id={inputId}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -45,8 +56,13 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
|
|||||||
: "cursor-pointer hover:-translate-y-0.5 hover:border-primary hover:bg-primary/[0.03] dark:hover:bg-primary/[0.08]"
|
: "cursor-pointer hover:-translate-y-0.5 hover:border-primary hover:bg-primary/[0.03] dark:hover:bg-primary/[0.08]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mb-3 rounded-2xl bg-primary/[0.12] p-3 text-primary shadow-theme-xs transition-transform duration-300 group-hover:scale-105">
|
<div className="shadow-theme-xs mb-3 rounded-2xl bg-primary/[0.12] p-3 text-primary transition-transform duration-300 group-hover:scale-105">
|
||||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
|
className="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -80,7 +96,9 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
|
|||||||
</div>
|
</div>
|
||||||
) : removingFileId ? (
|
) : removingFileId ? (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<p className="text-xs font-medium text-primary">Removing document…</p>
|
<p className="text-xs font-medium text-primary">
|
||||||
|
Removing document…
|
||||||
|
</p>
|
||||||
<div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-gray-2 dark:bg-dark-3">
|
<div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-gray-2 dark:bg-dark-3">
|
||||||
<div className="h-full w-full animate-pulse rounded-full bg-gradient-to-r from-primary to-purple-500" />
|
<div className="h-full w-full animate-pulse rounded-full bg-gradient-to-r from-primary to-purple-500" />
|
||||||
</div>
|
</div>
|
||||||
@@ -115,8 +133,12 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
|
|||||||
</p>
|
</p>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{failed.map((item, index) => (
|
{failed.map((item, index) => (
|
||||||
<li key={`${item.filename}-${index}`} className="text-sm text-error">
|
<li
|
||||||
<span className="font-medium">{item.filename}</span>: {item.error}
|
key={`${item.filename}-${index}`}
|
||||||
|
className="text-sm text-error"
|
||||||
|
>
|
||||||
|
<span className="font-medium">{item.filename}</span>:{" "}
|
||||||
|
{item.error}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useUploadCompanyDocuments } from "@/hooks/queries";
|
import { useUploadCompanyDocuments } from "@/hooks/queries";
|
||||||
import { fileService, FailedFile } from "@/lib/api";
|
import { fileService, FailedFile } from "@/lib/api";
|
||||||
import {
|
import { MAX_DOCUMENT_FILES, validateDocumentFile } from "@/utils/shared";
|
||||||
MAX_DOCUMENT_FILES,
|
|
||||||
validateDocumentFile,
|
|
||||||
} from "@/utils/shared";
|
|
||||||
import { ChangeEvent, useState } from "react";
|
import { ChangeEvent, useState } from "react";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -14,6 +11,8 @@ interface IProps {
|
|||||||
value: string[];
|
value: string[];
|
||||||
/** Persists the updated document file id list back to the form. */
|
/** Persists the updated document file id list back to the form. */
|
||||||
onChange: (ids: string[]) => void;
|
onChange: (ids: string[]) => void;
|
||||||
|
/** Persists document removal for an existing company. */
|
||||||
|
onRemovePersist?: (ids: string[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +27,7 @@ const useCompanyDocumentsPresenter = ({
|
|||||||
companyId,
|
companyId,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
onRemovePersist,
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [removingFileId, setRemovingFileId] = useState<string | null>(null);
|
const [removingFileId, setRemovingFileId] = useState<string | null>(null);
|
||||||
@@ -106,15 +106,18 @@ const useCompanyDocumentsPresenter = ({
|
|||||||
setErrorMessage(undefined);
|
setErrorMessage(undefined);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const nextValue = value.filter((id) => id !== fileId);
|
||||||
|
|
||||||
// In create mode the file is an orphan in storage (not linked to any
|
// In create mode the file is an orphan in storage (not linked to any
|
||||||
// company), so clean it up before dropping it from the form. In edit mode
|
// company), so clean it up before dropping it from the form. In edit mode
|
||||||
// the removal is persisted by the form's update (PUT replaces the full
|
// persist the replacement document list immediately.
|
||||||
// document list).
|
|
||||||
if (!companyId) {
|
if (!companyId) {
|
||||||
await fileService.deleteFile(fileId);
|
await fileService.deleteFile(fileId);
|
||||||
|
} else {
|
||||||
|
await onRemovePersist?.(nextValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(value.filter((id) => id !== fileId));
|
onChange(nextValue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("ERROR caught while deleting company document:", error);
|
console.error("ERROR caught while deleting company document:", error);
|
||||||
setErrorMessage("Failed to remove document. Please try again.");
|
setErrorMessage("Failed to remove document. Please try again.");
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany(
|
const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany(
|
||||||
id as string,
|
id as string,
|
||||||
);
|
);
|
||||||
|
const { mutateAsync: updateCompanyDocuments } = useUpdateCompany(
|
||||||
|
id as string,
|
||||||
|
{
|
||||||
|
redirectOnSuccess: false,
|
||||||
|
showSuccessToast: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const optionalNumber = (value: unknown) => {
|
const optionalNumber = (value: unknown) => {
|
||||||
if (value === "" || value === null || value === undefined) return undefined;
|
if (value === "" || value === null || value === undefined) return undefined;
|
||||||
@@ -56,27 +63,30 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
return Number(value);
|
return Number(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatCompanyData = (
|
||||||
|
data: ICreateCompanyCredentials,
|
||||||
|
documentFileIds = data.document_file_ids,
|
||||||
|
) => ({
|
||||||
|
...data,
|
||||||
|
founded_year: optionalNumber(data.founded_year),
|
||||||
|
employee_count: optionalNumber(data.employee_count),
|
||||||
|
annual_revenue: optionalNumber(data.annual_revenue),
|
||||||
|
document_file_ids: documentFileIds ?? [],
|
||||||
|
tags: {
|
||||||
|
keywords: data?.tags?.keywords?.length ? data?.tags?.keywords : [],
|
||||||
|
categories: data?.tags?.categories?.length ? data?.tags?.categories : [],
|
||||||
|
cpv_codes: data?.tags?.cpv_codes?.length ? data?.tags?.cpv_codes : [],
|
||||||
|
specializations: data?.tags?.specializations?.length
|
||||||
|
? data?.tags?.specializations
|
||||||
|
: [],
|
||||||
|
certifications: data?.tags?.certifications?.length
|
||||||
|
? data?.tags?.certifications
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<ICreateCompanyCredentials> = (data) => {
|
const onSubmit: SubmitHandler<ICreateCompanyCredentials> = (data) => {
|
||||||
const formattedData = {
|
const formattedData = formatCompanyData(data);
|
||||||
...data,
|
|
||||||
founded_year: optionalNumber(data.founded_year),
|
|
||||||
employee_count: optionalNumber(data.employee_count),
|
|
||||||
annual_revenue: optionalNumber(data.annual_revenue),
|
|
||||||
document_file_ids: data.document_file_ids ?? [],
|
|
||||||
tags: {
|
|
||||||
keywords: data?.tags?.keywords?.length ? data?.tags?.keywords : [],
|
|
||||||
categories: data?.tags?.categories?.length
|
|
||||||
? data?.tags?.categories
|
|
||||||
: [],
|
|
||||||
cpv_codes: data?.tags?.cpv_codes?.length ? data?.tags?.cpv_codes : [],
|
|
||||||
specializations: data?.tags?.specializations?.length
|
|
||||||
? data?.tags?.specializations
|
|
||||||
: [],
|
|
||||||
certifications: data?.tags?.certifications?.length
|
|
||||||
? data?.tags?.certifications
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
updateCompany({
|
updateCompany({
|
||||||
id: id as string,
|
id: id as string,
|
||||||
@@ -87,6 +97,18 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const persistDocumentFileIds = async (documentFileIds: string[]) => {
|
||||||
|
if (!editMode || !id) return;
|
||||||
|
|
||||||
|
await updateCompanyDocuments({
|
||||||
|
id,
|
||||||
|
credentials: formatCompanyData(
|
||||||
|
watch() as ICreateCompanyCredentials,
|
||||||
|
documentFileIds,
|
||||||
|
) as ICreateCompanyCredentials,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
reset();
|
reset();
|
||||||
router.back();
|
router.back();
|
||||||
@@ -104,6 +126,7 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
router,
|
router,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
|
persistDocumentFileIds,
|
||||||
companyCategories,
|
companyCategories,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,18 @@ export const useCreateCompany = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateCompany = (id: string) => {
|
interface UseUpdateCompanyOptions {
|
||||||
|
redirectOnSuccess?: boolean;
|
||||||
|
showSuccessToast?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateCompany = (
|
||||||
|
id: string,
|
||||||
|
{
|
||||||
|
redirectOnSuccess = true,
|
||||||
|
showSuccessToast = true,
|
||||||
|
}: UseUpdateCompanyOptions = {},
|
||||||
|
) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const mutationKey = useMemo(
|
const mutationKey = useMemo(
|
||||||
@@ -42,14 +53,21 @@ export const useUpdateCompany = (id: string) => {
|
|||||||
mutationKey,
|
mutationKey,
|
||||||
mutationFn: companiesService.updateCompany,
|
mutationFn: companiesService.updateCompany,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Company updated successfully");
|
if (showSuccessToast) {
|
||||||
|
toast.success("Company updated successfully");
|
||||||
|
}
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL],
|
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL],
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL, "details"],
|
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL, "details"],
|
||||||
});
|
});
|
||||||
router.push("/companies");
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL, id, "details"],
|
||||||
|
});
|
||||||
|
if (redirectOnSuccess) {
|
||||||
|
router.push("/companies");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user