feat: Enable loading document version content from attached files and update environment configuration.
This commit is contained in:
parent
320277b536
commit
79004fc75e
4
.env
4
.env
@ -1,4 +0,0 @@
|
||||
# VITE_FRONTEND_BASE_URL=http://localhost:5173
|
||||
# VITE_API_BASE_URL=http://localhost:3000/api/v1
|
||||
VITE_FRONTEND_BASE_URL=https://qasure.tech4bizsolutions.com
|
||||
VITE_API_BASE_URL=https://backendqaasure.tech4bizsolutions.com/api/v1
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,12 +6,14 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.env
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
@ -2,10 +2,10 @@ import { useEffect, useState, type ReactElement } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Layout } from "@/components/layout/Layout";
|
||||
import { FormField, FormSelect, FormTextArea, PrimaryButton, RichTextEditor } from "@/components/shared";
|
||||
import { documentService } from "@/services/document-service";
|
||||
import { documentService, type FileAttachmentItem } from "@/services/document-service";
|
||||
import type { DocumentCategory } from "@/types/document";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { ArrowLeft, FileText, Info } from "lucide-react";
|
||||
import { ArrowLeft, FileText, Info, Paperclip } from "lucide-react";
|
||||
|
||||
const CreateDocument = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
@ -22,6 +22,16 @@ const CreateDocument = (): ReactElement => {
|
||||
const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]);
|
||||
const [categories, setCategories] = useState<DocumentCategory[]>([]);
|
||||
|
||||
// File attachment fields
|
||||
const [files, setFiles] = useState<FileAttachmentItem[]>([]);
|
||||
const [selectedFileId, setSelectedFileId] = useState("");
|
||||
const [fileName, setFileName] = useState("");
|
||||
const [filePath, setFilePath] = useState("");
|
||||
const [fileSize, setFileSize] = useState<number | undefined>(undefined);
|
||||
const [mimeType, setMimeType] = useState("");
|
||||
const [fileHash, setFileHash] = useState("");
|
||||
const [isLoadingFiles, setIsLoadingFiles] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadLookups = async (): Promise<void> => {
|
||||
try {
|
||||
@ -36,8 +46,66 @@ const CreateDocument = (): ReactElement => {
|
||||
}
|
||||
};
|
||||
void loadLookups();
|
||||
void loadFiles();
|
||||
}, []);
|
||||
|
||||
const loadFiles = async (): Promise<void> => {
|
||||
setIsLoadingFiles(true);
|
||||
try {
|
||||
const res = await documentService.listFiles();
|
||||
setFiles(res.data || []);
|
||||
} catch {
|
||||
showToast.error("Failed to load files");
|
||||
} finally {
|
||||
setIsLoadingFiles(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = async (fileId: string): Promise<void> => {
|
||||
setSelectedFileId(fileId);
|
||||
if (!fileId) {
|
||||
// Clear file fields when deselected
|
||||
setFileName("");
|
||||
setFilePath("");
|
||||
setFileSize(undefined);
|
||||
setMimeType("");
|
||||
setFileHash("");
|
||||
setContentHtml("");
|
||||
setContent("");
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = files.find((f) => f.id === fileId);
|
||||
if (!selected) return;
|
||||
|
||||
// Populate file metadata fields
|
||||
setFileName(selected.original_name);
|
||||
setFilePath(selected.file_path);
|
||||
setFileSize(selected.file_size);
|
||||
setMimeType(selected.mime_type);
|
||||
setFileHash(selected.checksum);
|
||||
|
||||
// Extract actual file content from backend
|
||||
try {
|
||||
showToast.success(`Extracting content from "${selected.original_name}"...`);
|
||||
const res = await documentService.getFileContent(fileId);
|
||||
if (res.success && res.data) {
|
||||
setContentHtml(res.data.html || "");
|
||||
setContent(res.data.text || "");
|
||||
showToast.success(`Content loaded from "${selected.original_name}"`);
|
||||
} else {
|
||||
showToast.error("Failed to extract file content");
|
||||
}
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.error?.message || "Failed to extract file content";
|
||||
showToast.error(msg);
|
||||
// Fallback: set a placeholder
|
||||
const html = `<p>Document sourced from file: <strong>${selected.original_name}</strong></p>`;
|
||||
setContentHtml(html);
|
||||
setContent(`Document sourced from file: ${selected.original_name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (event: React.FormEvent): Promise<void> => {
|
||||
event.preventDefault();
|
||||
if (!title.trim() || !documentType) {
|
||||
@ -60,6 +128,11 @@ const CreateDocument = (): ReactElement => {
|
||||
.filter(Boolean),
|
||||
content: content.trim() || undefined,
|
||||
content_html: contentHtml.trim() || undefined,
|
||||
file_name: fileName || undefined,
|
||||
file_path: filePath || undefined,
|
||||
file_size: fileSize,
|
||||
mime_type: mimeType || undefined,
|
||||
file_hash: fileHash || undefined,
|
||||
});
|
||||
showToast.success("Document created successfully");
|
||||
navigate(`/tenant/documents/${response.data.id}`);
|
||||
@ -178,6 +251,39 @@ const CreateDocument = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Attachment Selection */}
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-4 md:p-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Paperclip className="w-4 h-4 text-[#112868]" />
|
||||
<h3 className="text-sm font-semibold text-[#0f1724]">Attach File (Optional)</h3>
|
||||
</div>
|
||||
<p className="text-xs text-[#6b7280] mb-3">
|
||||
Select a previously uploaded file to automatically populate content and file metadata.
|
||||
</p>
|
||||
<FormSelect
|
||||
label="Select File"
|
||||
value={selectedFileId}
|
||||
onValueChange={handleFileSelect}
|
||||
options={[
|
||||
{ value: "", label: "— None —" },
|
||||
...files.map((f) => ({
|
||||
value: f.id,
|
||||
label: `${f.original_name} (${f.file_size_formatted})`,
|
||||
})),
|
||||
]}
|
||||
placeholder={isLoadingFiles ? "Loading files..." : "Select a file to attach"}
|
||||
/>
|
||||
|
||||
{selectedFileId && (
|
||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-0 text-xs text-[#6b7280]">
|
||||
<p><span className="font-medium">File:</span> {fileName}</p>
|
||||
<p><span className="font-medium">Type:</span> {mimeType}</p>
|
||||
<p><span className="font-medium">Size:</span> {fileSize ? `${(fileSize / 1024 / 1024).toFixed(2)} MB` : "-"}</p>
|
||||
<p><span className="font-medium">Hash:</span> {fileHash ? fileHash.substring(0, 16) + "..." : "-"}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-4 md:p-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-[#0f1724]">Initial Content</h3>
|
||||
@ -231,4 +337,3 @@ const CreateDocument = (): ReactElement => {
|
||||
};
|
||||
|
||||
export default CreateDocument;
|
||||
|
||||
|
||||
@ -186,7 +186,8 @@ const TenantLogin = (): ReactElement => {
|
||||
className="text-[18px] font-bold tracking-[-0.36px]"
|
||||
style={{ color: theme?.secondary_color || '#23dce1' }}
|
||||
>
|
||||
{logoUrl ? '' : 'QAssure'}
|
||||
{/* {logoUrl ? '' : 'QAssure'} */}
|
||||
QAssure
|
||||
</div>
|
||||
<div className="text-[13px] font-medium text-white uppercase">-</div>
|
||||
<div className="text-[13px] font-medium text-white uppercase">Tenant</div>
|
||||
|
||||
@ -10,11 +10,11 @@ import {
|
||||
SecondaryButton,
|
||||
type Column,
|
||||
} from "@/components/shared";
|
||||
import { documentService } from "@/services/document-service";
|
||||
import { documentService, type FileAttachmentItem } from "@/services/document-service";
|
||||
import { workflowService } from "@/services/workflow-service";
|
||||
import type { DocumentDetail, DocumentVersion } from "@/types/document";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { ChevronDown, Plus } from "lucide-react";
|
||||
import { ChevronDown, Paperclip, Plus } from "lucide-react";
|
||||
|
||||
const formatDateTime = (value?: string | null): string => {
|
||||
if (!value) return "-";
|
||||
@ -74,6 +74,16 @@ const ViewDocument = (): ReactElement => {
|
||||
const [isMajorVersion, setIsMajorVersion] = useState(false);
|
||||
const [isVersionSaving, setIsVersionSaving] = useState(false);
|
||||
|
||||
// File attachment fields for new version
|
||||
const [versionFiles, setVersionFiles] = useState<FileAttachmentItem[]>([]);
|
||||
const [versionSelectedFileId, setVersionSelectedFileId] = useState("");
|
||||
const [versionFileName, setVersionFileName] = useState("");
|
||||
const [versionFilePath, setVersionFilePath] = useState("");
|
||||
const [versionFileSize, setVersionFileSize] = useState<number | undefined>(undefined);
|
||||
const [versionMimeType, setVersionMimeType] = useState("");
|
||||
const [versionFileHash, setVersionFileHash] = useState("");
|
||||
const [isLoadingVersionFiles, setIsLoadingVersionFiles] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
@ -98,8 +108,60 @@ const ViewDocument = (): ReactElement => {
|
||||
};
|
||||
|
||||
void loadDocument();
|
||||
void loadVersionFiles();
|
||||
}, [id]);
|
||||
|
||||
const loadVersionFiles = async (): Promise<void> => {
|
||||
setIsLoadingVersionFiles(true);
|
||||
try {
|
||||
const res = await documentService.listFiles();
|
||||
setVersionFiles(res.data || []);
|
||||
} catch {
|
||||
// silent
|
||||
} finally {
|
||||
setIsLoadingVersionFiles(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVersionFileSelect = async (fileId: string): Promise<void> => {
|
||||
setVersionSelectedFileId(fileId);
|
||||
if (!fileId) {
|
||||
setVersionFileName("");
|
||||
setVersionFilePath("");
|
||||
setVersionFileSize(undefined);
|
||||
setVersionMimeType("");
|
||||
setVersionFileHash("");
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = versionFiles.find((f) => f.id === fileId);
|
||||
if (!selected) return;
|
||||
|
||||
setVersionFileName(selected.original_name);
|
||||
setVersionFilePath(selected.file_path);
|
||||
setVersionFileSize(selected.file_size);
|
||||
setVersionMimeType(selected.mime_type);
|
||||
setVersionFileHash(selected.checksum);
|
||||
|
||||
try {
|
||||
showToast.success(`Extracting content from "${selected.original_name}"...`);
|
||||
const res = await documentService.getFileContent(fileId);
|
||||
if (res.success && res.data) {
|
||||
setNewVersionContentHtml(res.data.html || "");
|
||||
setNewVersionContent(res.data.text || "");
|
||||
showToast.success(`Content loaded from "${selected.original_name}"`);
|
||||
} else {
|
||||
showToast.error("Failed to extract file content");
|
||||
}
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.error?.message || "Failed to extract file content";
|
||||
showToast.error(msg);
|
||||
const html = `<p>Document sourced from file: <strong>${selected.original_name}</strong></p>`;
|
||||
setNewVersionContentHtml(html);
|
||||
setNewVersionContent(`Document sourced from file: ${selected.original_name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshData = async (): Promise<void> => {
|
||||
if (!id) return;
|
||||
const [documentRes, versionsRes] = await Promise.all([
|
||||
@ -224,11 +286,22 @@ const ViewDocument = (): ReactElement => {
|
||||
change_reason: newVersionChangeReason,
|
||||
change_summary: newVersionChangeSummary.trim() || undefined,
|
||||
is_major_version: isMajorVersion,
|
||||
file_name: versionFileName || undefined,
|
||||
file_path: versionFilePath || undefined,
|
||||
file_size: versionFileSize,
|
||||
mime_type: versionMimeType || undefined,
|
||||
file_hash: versionFileHash || undefined,
|
||||
});
|
||||
showToast.success("New version created successfully");
|
||||
setShowNewVersionForm(false);
|
||||
setNewVersionChangeSummary("");
|
||||
setIsMajorVersion(false);
|
||||
setVersionSelectedFileId("");
|
||||
setVersionFileName("");
|
||||
setVersionFilePath("");
|
||||
setVersionFileSize(undefined);
|
||||
setVersionMimeType("");
|
||||
setVersionFileHash("");
|
||||
await refreshData();
|
||||
} catch (err: any) {
|
||||
showToast.error(
|
||||
@ -432,6 +505,39 @@ const ViewDocument = (): ReactElement => {
|
||||
{showNewVersionForm && (
|
||||
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg p-4 bg-[#f8fafc]">
|
||||
<h4 className="text-base font-semibold text-[#0f1724] mb-3">Create New Version</h4>
|
||||
|
||||
{/* File Attachment Selection */}
|
||||
<div className="mb-4 border border-[rgba(0,0,0,0.08)] rounded-lg p-3 bg-white">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Paperclip className="w-4 h-4 text-[#112868]" />
|
||||
<span className="text-sm font-medium text-[#0f1724]">Load Content From File (Optional)</span>
|
||||
</div>
|
||||
<p className="text-xs text-[#6b7280] mb-2">
|
||||
Select a file to extract and auto-fill content below.
|
||||
</p>
|
||||
<FormSelect
|
||||
label="Select File"
|
||||
value={versionSelectedFileId}
|
||||
onValueChange={(val) => void handleVersionFileSelect(val)}
|
||||
options={[
|
||||
{ value: "", label: "— None —" },
|
||||
...versionFiles.map((f) => ({
|
||||
value: f.id,
|
||||
label: `${f.original_name} (${f.file_size_formatted})`,
|
||||
})),
|
||||
]}
|
||||
placeholder={isLoadingVersionFiles ? "Loading files..." : "Select a file to attach"}
|
||||
/>
|
||||
{versionSelectedFileId && (
|
||||
<div className="mt-2 grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-0 text-xs text-[#6b7280]">
|
||||
<p><span className="font-medium">File:</span> {versionFileName}</p>
|
||||
<p><span className="font-medium">Type:</span> {versionMimeType}</p>
|
||||
<p><span className="font-medium">Size:</span> {versionFileSize ? `${(versionFileSize / 1024 / 1024).toFixed(2)} MB` : "-"}</p>
|
||||
<p><span className="font-medium">Hash:</span> {versionFileHash ? versionFileHash.substring(0, 16) + "..." : "-"}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<RichTextEditor
|
||||
label="Document Content"
|
||||
value={newVersionContentHtml}
|
||||
|
||||
@ -29,6 +29,26 @@ export interface CreateDocumentPayload {
|
||||
custom_fields?: Record<string, unknown>;
|
||||
content?: string;
|
||||
content_html?: string;
|
||||
file_name?: string;
|
||||
file_path?: string;
|
||||
file_size?: number;
|
||||
mime_type?: string;
|
||||
file_hash?: string;
|
||||
}
|
||||
|
||||
export interface FileAttachmentItem {
|
||||
id: string;
|
||||
original_name: string;
|
||||
stored_name: string;
|
||||
file_path: string;
|
||||
mime_type: string;
|
||||
file_size: number;
|
||||
checksum: string;
|
||||
file_size_formatted: string;
|
||||
category: string;
|
||||
description: string;
|
||||
uploaded_by_email: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateCategoryPayload {
|
||||
@ -47,9 +67,24 @@ export interface CreateDocumentVersionPayload {
|
||||
change_summary?: string;
|
||||
change_reason?: string;
|
||||
is_major_version?: boolean;
|
||||
file_name?: string;
|
||||
file_path?: string;
|
||||
file_size?: number;
|
||||
mime_type?: string;
|
||||
file_hash?: string;
|
||||
}
|
||||
|
||||
export const documentService = {
|
||||
listFiles: async (): Promise<{ success: boolean; data: FileAttachmentItem[]; pagination: { total: number; limit: number; offset: number } }> => {
|
||||
const response = await apiClient.get<{ success: boolean; data: FileAttachmentItem[]; pagination: { total: number; limit: number; offset: number } }>("/files");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getFileContent: async (fileId: string): Promise<{ success: boolean; data: { html: string; text: string; original_name: string; file_path: string; file_size: number; mime_type: string; checksum: string } }> => {
|
||||
const response = await apiClient.get<{ success: boolean; data: { html: string; text: string; original_name: string; file_path: string; file_size: number; mime_type: string; checksum: string } }>(`/files/${fileId}/content`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
list: async (params: ListDocumentsParams): Promise<DocumentListResponse> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.status) queryParams.append("status", params.status);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user