feat(documents): implement document upload functionality for companies

- Added DocumentItem component to display uploaded documents in CompanyDetailsPage.
- Introduced useUploadCompanyDocuments hook for handling document uploads.
- Updated CreateCompany form to include document upload capabilities and validation for file types and sizes.
- Enhanced API services to support batch document uploads and retrieval of document information.
- Improved response handling in the file service for better error management and user feedback.
This commit is contained in:
AmirReza Jamali
2026-06-06 17:18:02 +03:30
parent 88f223bf08
commit da1e2f89e7
19 changed files with 1038 additions and 429 deletions
@@ -0,0 +1,114 @@
"use client";
import { DOCUMENT_FILE_ACCEPT, MAX_DOCUMENT_FILES } from "@/utils/shared";
import DocumentItem from "./DocumentItem";
import useCompanyDocumentsPresenter from "./useCompanyDocumentsPresenter";
interface IProps {
/** Present in edit mode; switches uploads to the company-documents endpoint. */
companyId?: string;
value: string[];
onChange: (ids: string[]) => void;
}
const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
const {
isUploading,
uploadProgress,
failed,
errorMessage,
onSelectFiles,
onRemove,
} = useCompanyDocumentsPresenter({ companyId, value, onChange });
const inputId = "company-documents-input";
return (
<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]">
<input
id={inputId}
type="file"
multiple
accept={DOCUMENT_FILE_ACCEPT}
className="sr-only"
onChange={onSelectFiles}
disabled={isUploading}
/>
<label
htmlFor={inputId}
className="group flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-stroke/80 bg-white/70 px-4 py-9 text-center transition-all duration-300 hover:-translate-y-0.5 hover:border-primary hover:bg-primary/[0.03] dark:border-dark-3 dark:bg-dark/20 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">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
</div>
<p className="text-sm font-semibold text-dark dark:text-white">
Click to upload documents
</p>
<p className="mt-1 text-xs text-dark-5">
PDF, Word, Excel, images, archives up to 100MB each
</p>
<div className="mt-4 inline-flex items-center rounded-full border border-primary/20 bg-primary/[0.08] px-3 py-1 text-[11px] font-medium text-primary dark:border-primary/40 dark:bg-primary/[0.18]">
Max {MAX_DOCUMENT_FILES} files per upload
</div>
</label>
{isUploading ? (
<div className="mt-3">
<p className="text-xs font-medium text-primary">
Uploading documents {companyId ? "" : `${uploadProgress}%`}
</p>
<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 rounded-full bg-gradient-to-r from-primary to-purple-500 transition-all duration-300"
style={{ width: companyId ? "100%" : `${uploadProgress}%` }}
/>
</div>
</div>
) : null}
</div>
{errorMessage ? (
<p className="text-sm font-medium text-red-500">{errorMessage}</p>
) : null}
{value.length > 0 ? (
<div className="flex flex-col gap-2">
{value.map((fileId) => (
<DocumentItem
key={fileId}
fileId={fileId}
onRemove={onRemove}
disabled={isUploading}
/>
))}
</div>
) : (
<p className="text-sm text-dark-5">No documents attached yet.</p>
)}
{failed.length > 0 ? (
<div className="rounded-xl border border-error/35 bg-error/5 p-3">
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-error">
Failed uploads
</p>
<ul className="space-y-1">
{failed.map((item, index) => (
<li key={`${item.filename}-${index}`} className="text-sm text-error">
<span className="font-medium">{item.filename}</span>: {item.error}
</li>
))}
</ul>
</div>
) : null}
</div>
);
};
export default CompanyDocuments;