feat(UI Fix) - upload zone fixed
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw, FilePlus } from "lucide-react";
|
import { RefreshCw, FilePlus } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { MultipleFileUploadZone } from "../file-upload/multiple-file-upload-zone";
|
import {
|
||||||
|
MultipleFileUploadZone,
|
||||||
|
MultipleFileUploadZoneHandle,
|
||||||
|
} from "../file-upload/multiple-file-upload-zone";
|
||||||
|
|
||||||
export default function ClaimDocumentsUploadMultiple() {
|
export default function ClaimDocumentsUploadMultiple() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -22,14 +25,15 @@ export default function ClaimDocumentsUploadMultiple() {
|
|||||||
const DESCRIPTION =
|
const DESCRIPTION =
|
||||||
"You can upload up to 10 files. Allowed types: PDF, JPG, PNG, WEBP.";
|
"You can upload up to 10 files. Allowed types: PDF, JPG, PNG, WEBP.";
|
||||||
|
|
||||||
// Internal state
|
// Zone ref + minimal UI state (parent does not own files)
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
const uploadZoneRef = useRef<MultipleFileUploadZoneHandle | null>(null);
|
||||||
|
const [filesForUI, setFilesForUI] = useState<File[]>([]);
|
||||||
const [isUploading, setIsUploading] = useState(false); // forwarded to upload zone
|
const [isUploading, setIsUploading] = useState(false); // forwarded to upload zone
|
||||||
const [isExtracting, setIsExtracting] = useState(false);
|
const [isExtracting, setIsExtracting] = useState(false);
|
||||||
|
|
||||||
// Called by MultipleFileUploadZone whenever its internal list changes.
|
// Called by MultipleFileUploadZone when its internal list changes (UI-only)
|
||||||
const handleFileUpload = useCallback((files: File[]) => {
|
const handleZoneFilesChange = useCallback((files: File[]) => {
|
||||||
setUploadedFiles(files);
|
setFilesForUI(files);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Dummy save (simulate async). Replace with real API call when needed.
|
// Dummy save (simulate async). Replace with real API call when needed.
|
||||||
@@ -42,9 +46,11 @@ export default function ClaimDocumentsUploadMultiple() {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Extract handler — calls handleSave (dummy) and shows toasts.
|
// Extract handler — reads files from the zone via ref and calls handleSave
|
||||||
const handleExtract = useCallback(async () => {
|
const handleExtract = useCallback(async () => {
|
||||||
if (uploadedFiles.length === 0) {
|
const files = uploadZoneRef.current?.getFiles() ?? [];
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
title: "No files",
|
title: "No files",
|
||||||
description: "Please upload at least one file before extracting.",
|
description: "Please upload at least one file before extracting.",
|
||||||
@@ -57,17 +63,15 @@ export default function ClaimDocumentsUploadMultiple() {
|
|||||||
setIsExtracting(true);
|
setIsExtracting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleSave(uploadedFiles);
|
await handleSave(files);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Extraction started",
|
title: "Extraction started",
|
||||||
description: `Processing ${uploadedFiles.length} file(s).`,
|
description: `Processing ${files.length} file(s).`,
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
// If you want to clear the upload zone after extraction, you'll need a small
|
// we intentionally leave files intact in the zone after extraction
|
||||||
// change in MultipleFileUploadZone to accept a reset signal from parent.
|
|
||||||
// We intentionally leave files intact here.
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: "Extraction failed",
|
title: "Extraction failed",
|
||||||
@@ -80,7 +84,7 @@ export default function ClaimDocumentsUploadMultiple() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsExtracting(false);
|
setIsExtracting(false);
|
||||||
}
|
}
|
||||||
}, [uploadedFiles, handleSave, isExtracting, toast]);
|
}, [handleSave, isExtracting, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 py-8">
|
<div className="space-y-8 py-8">
|
||||||
@@ -93,20 +97,21 @@ export default function ClaimDocumentsUploadMultiple() {
|
|||||||
{/* File Upload Section */}
|
{/* File Upload Section */}
|
||||||
<div className="bg-gray-100 p-4 rounded-md space-y-4">
|
<div className="bg-gray-100 p-4 rounded-md space-y-4">
|
||||||
<MultipleFileUploadZone
|
<MultipleFileUploadZone
|
||||||
onFileUpload={handleFileUpload}
|
ref={uploadZoneRef}
|
||||||
|
onFilesChange={handleZoneFilesChange}
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
acceptedFileTypes={ACCEPTED_FILE_TYPES}
|
acceptedFileTypes={ACCEPTED_FILE_TYPES}
|
||||||
maxFiles={MAX_FILES}
|
maxFiles={MAX_FILES}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Show list of files received from the upload zone */}
|
{/* Show list of files received from the upload zone */}
|
||||||
{uploadedFiles.length > 0 && (
|
{filesForUI.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-2">
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
Uploaded ({uploadedFiles.length}/{MAX_FILES})
|
Uploaded ({filesForUI.length}/{MAX_FILES})
|
||||||
</p>
|
</p>
|
||||||
<ul className="text-sm text-gray-700 list-disc ml-6 max-h-40 overflow-auto">
|
<ul className="text-sm text-gray-700 list-disc ml-6 max-h-40 overflow-auto">
|
||||||
{uploadedFiles.map((file, index) => (
|
{filesForUI.map((file, index) => (
|
||||||
<li key={index} className="truncate" title={file.name}>
|
<li key={index} className="truncate" title={file.name}>
|
||||||
{file.name}
|
{file.name}
|
||||||
</li>
|
</li>
|
||||||
@@ -120,7 +125,7 @@ export default function ClaimDocumentsUploadMultiple() {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-12 gap-2"
|
className="w-full h-12 gap-2"
|
||||||
disabled={uploadedFiles.length === 0 || isExtracting}
|
disabled={filesForUI.length === 0 || isExtracting}
|
||||||
onClick={handleExtract}
|
onClick={handleExtract}
|
||||||
>
|
>
|
||||||
{isExtracting ? (
|
{isExtracting ? (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +20,10 @@ import {
|
|||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { MultipleFileUploadZone } from "../file-upload/multiple-file-upload-zone";
|
import {
|
||||||
|
MultipleFileUploadZone,
|
||||||
|
MultipleFileUploadZoneHandle,
|
||||||
|
} from "../file-upload/multiple-file-upload-zone";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -281,53 +284,13 @@ export function ClaimForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// FILE UPLOAD ZONE
|
// FILE UPLOAD ZONE
|
||||||
|
const uploadZoneRef = useRef<MultipleFileUploadZoneHandle | null>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
const handleFileUpload = (files: File[]) => {
|
// NO validation here — the upload zone handles validation, toasts, max files, sizes, etc.
|
||||||
setIsUploading(true);
|
const handleFilesChange = useCallback((files: File[]) => {
|
||||||
|
setForm((prev) => ({ ...prev, uploadedFiles: files }));
|
||||||
const allowedTypes = [
|
}, []);
|
||||||
"application/pdf",
|
|
||||||
"image/jpeg",
|
|
||||||
"image/jpg",
|
|
||||||
"image/png",
|
|
||||||
"image/webp",
|
|
||||||
];
|
|
||||||
|
|
||||||
const validFiles = files.filter((file) => allowedTypes.includes(file.type));
|
|
||||||
|
|
||||||
if (validFiles.length > 10) {
|
|
||||||
toast({
|
|
||||||
title: "Too Many Files",
|
|
||||||
description: "You can only upload up to 10 files (PDFs or images).",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
setIsUploading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validFiles.length === 0) {
|
|
||||||
toast({
|
|
||||||
title: "Invalid File Type",
|
|
||||||
description: "Only PDF and image files are allowed.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
setIsUploading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
uploadedFiles: validFiles,
|
|
||||||
}));
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Files Selected",
|
|
||||||
description: `${validFiles.length} file(s) ready for processing.`,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsUploading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1st Button workflow - Mass Health Button Handler
|
// 1st Button workflow - Mass Health Button Handler
|
||||||
const handleMHSubmit = async () => {
|
const handleMHSubmit = async () => {
|
||||||
@@ -865,7 +828,8 @@ export function ClaimForm({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<MultipleFileUploadZone
|
<MultipleFileUploadZone
|
||||||
onFileUpload={handleFileUpload}
|
ref={uploadZoneRef}
|
||||||
|
onFilesChange={handleFilesChange}
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
acceptedFileTypes="application/pdf,image/jpeg,image/jpg,image/png,image/webp"
|
acceptedFileTypes="application/pdf,image/jpeg,image/jpg,image/png,image/webp"
|
||||||
maxFiles={10}
|
maxFiles={10}
|
||||||
@@ -893,10 +857,7 @@ export function ClaimForm({
|
|||||||
>
|
>
|
||||||
MH
|
MH
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button className="w-32" variant="warning">
|
||||||
className="w-32"
|
|
||||||
variant="warning"
|
|
||||||
>
|
|
||||||
MH PreAuth
|
MH PreAuth
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,209 +1,352 @@
|
|||||||
import React, { useState, useRef, useCallback } from "react";
|
import React, {
|
||||||
import { Upload, File, X, FilePlus } from "lucide-react";
|
useState,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
|
import { Upload, X, FilePlus } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type MultipleFileUploadZoneHandle = {
|
||||||
|
getFiles: () => File[];
|
||||||
|
reset: () => void;
|
||||||
|
removeFile: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
interface FileUploadZoneProps {
|
interface FileUploadZoneProps {
|
||||||
onFileUpload: (files: File[]) => void;
|
onFilesChange?: (files: File[]) => void;
|
||||||
isUploading: boolean;
|
isUploading?: boolean;
|
||||||
acceptedFileTypes?: string;
|
acceptedFileTypes?: string;
|
||||||
maxFiles?: number;
|
maxFiles?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultipleFileUploadZone({
|
export const MultipleFileUploadZone = forwardRef<
|
||||||
onFileUpload,
|
MultipleFileUploadZoneHandle,
|
||||||
isUploading,
|
FileUploadZoneProps
|
||||||
acceptedFileTypes = "application/pdf,image/jpeg,image/jpg,image/png,image/webp",
|
>(
|
||||||
maxFiles = 10,
|
(
|
||||||
}: FileUploadZoneProps) {
|
{
|
||||||
const { toast } = useToast();
|
onFilesChange,
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
isUploading = false,
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
acceptedFileTypes = "application/pdf,image/jpeg,image/jpg,image/png,image/webp",
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
maxFiles = 10,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const allowedTypes = acceptedFileTypes.split(",").map((type) => type.trim());
|
const allowedTypes = acceptedFileTypes
|
||||||
|
.split(",")
|
||||||
|
.map((type) => type.trim());
|
||||||
|
|
||||||
const validateFile = (file: File) => {
|
const validateFile = (file: File) => {
|
||||||
if (!allowedTypes.includes(file.type)) {
|
if (!allowedTypes.includes(file.type)) {
|
||||||
toast({
|
toast({
|
||||||
title: "Invalid file type",
|
title: "Invalid file type",
|
||||||
description: "Only PDF and image files are allowed.",
|
description: "Only PDF and image files are allowed.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
toast({
|
|
||||||
title: "File too large",
|
|
||||||
description: "File size must be less than 5MB.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFiles = (files: FileList | null) => {
|
|
||||||
if (!files) return;
|
|
||||||
|
|
||||||
const newFiles = Array.from(files).filter(validateFile);
|
|
||||||
const totalFiles = uploadedFiles.length + newFiles.length;
|
|
||||||
|
|
||||||
if (totalFiles > maxFiles) {
|
|
||||||
toast({
|
|
||||||
title: "Too Many Files",
|
|
||||||
description: `You can only upload up to ${maxFiles} files.`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedFiles = [...uploadedFiles, ...newFiles];
|
|
||||||
setUploadedFiles(updatedFiles);
|
|
||||||
onFileUpload(updatedFiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragging(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragging(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragOver = useCallback(
|
|
||||||
(e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!isDragging) {
|
|
||||||
setIsDragging(true);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[isDragging]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
(e: React.DragEvent<HTMLDivElement>) => {
|
toast({
|
||||||
e.preventDefault();
|
title: "File too large",
|
||||||
e.stopPropagation();
|
description: "File size must be less than 5MB.",
|
||||||
setIsDragging(false);
|
variant: "destructive",
|
||||||
handleFiles(e.dataTransfer.files);
|
});
|
||||||
},
|
return false;
|
||||||
[uploadedFiles]
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const handleFileSelect = useCallback(
|
return true;
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
};
|
||||||
handleFiles(e.target.files);
|
|
||||||
},
|
|
||||||
[uploadedFiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleBrowseClick = () => {
|
// ----------------- friendly label helper -----------------
|
||||||
if (fileInputRef.current) {
|
// Convert acceptedFileTypes MIME list into human-friendly labels
|
||||||
fileInputRef.current.click();
|
const buildFriendlyTypes = (accept: string) => {
|
||||||
}
|
const types = accept
|
||||||
};
|
.split(",")
|
||||||
|
.map((s) => s.trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const handleRemoveFile = (index: number) => {
|
// track whether generic image/* is present
|
||||||
const newFiles = [...uploadedFiles];
|
const hasImageWildcard = types.includes("image/*");
|
||||||
newFiles.splice(index, 1);
|
const names = new Set<string>();
|
||||||
setUploadedFiles(newFiles);
|
|
||||||
onFileUpload(newFiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
for (const t of types) {
|
||||||
<div className="w-full">
|
if (t === "image/*") {
|
||||||
<input
|
names.add("images");
|
||||||
type="file"
|
continue;
|
||||||
ref={fileInputRef}
|
}
|
||||||
className="hidden"
|
if (t.includes("pdf")) {
|
||||||
onChange={handleFileSelect}
|
names.add("PDF");
|
||||||
accept={acceptedFileTypes}
|
continue;
|
||||||
multiple
|
}
|
||||||
/>
|
if (t.includes("jpeg") || t.includes("jpg")) {
|
||||||
|
names.add("JPG");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (t.includes("png")) {
|
||||||
|
names.add("PNG");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (t.includes("webp")) {
|
||||||
|
names.add("WEBP");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (t.includes("tiff") || t.includes("tif")) {
|
||||||
|
names.add("TIFF");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (t.includes("bmp")) {
|
||||||
|
names.add("BMP");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// fallback: attempt to extract subtype (safe)
|
||||||
|
if (t.includes("/")) {
|
||||||
|
const parts = t.split("/");
|
||||||
|
const subtype = parts[1]; // may be undefined if malformed
|
||||||
|
if (subtype) {
|
||||||
|
names.add(subtype.toUpperCase());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
names.add(t.toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
return {
|
||||||
className={cn(
|
hasImageWildcard,
|
||||||
"border-2 border-dashed rounded-lg p-8 flex flex-col items-center justify-center text-center transition-colors",
|
names: Array.from(names),
|
||||||
isDragging
|
};
|
||||||
? "border-primary bg-primary/5"
|
};
|
||||||
: "border-muted-foreground/25",
|
|
||||||
isUploading && "opacity-50 cursor-not-allowed"
|
const friendly = buildFriendlyTypes(acceptedFileTypes);
|
||||||
)}
|
|
||||||
onDragEnter={handleDragEnter}
|
// Build main title text
|
||||||
onDragLeave={handleDragLeave}
|
const uploadTitle = (() => {
|
||||||
onDragOver={handleDragOver}
|
const { hasImageWildcard, names } = friendly;
|
||||||
onDrop={handleDrop}
|
// if only "images"
|
||||||
onClick={!isUploading ? handleBrowseClick : undefined}
|
if (hasImageWildcard && names.length === 1)
|
||||||
style={{ minHeight: "200px" }}
|
return "Drag and drop image files here";
|
||||||
>
|
// if includes images plus specific others (e.g., image/* + pdf)
|
||||||
{isUploading ? (
|
if (hasImageWildcard && names.length > 1) {
|
||||||
<div className="flex flex-col items-center gap-4">
|
const others = names.filter((n) => n !== "images");
|
||||||
<div className="animate-spin">
|
return `Drag and drop image files (${others.join(", ")}) here`;
|
||||||
<Upload className="h-10 w-10 text-primary" />
|
}
|
||||||
|
// no wildcard images: list the types
|
||||||
|
if (names.length === 0) return "Drag and drop files here";
|
||||||
|
if (names.length === 1) return `Drag and drop ${names[0]} files here`;
|
||||||
|
// multiple: join
|
||||||
|
return `Drag and drop ${names.join(", ")} files here`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Build footer allowed types text (small)
|
||||||
|
const allowedHuman = (() => {
|
||||||
|
const { hasImageWildcard, names } = friendly;
|
||||||
|
if (hasImageWildcard) {
|
||||||
|
// show images + any explicit types (excluding 'images')
|
||||||
|
const extras = names.filter((n) => n !== "images");
|
||||||
|
return extras.length
|
||||||
|
? `Images (${extras.join(", ")}), ${maxFiles} max`
|
||||||
|
: `Images, ${maxFiles} max`;
|
||||||
|
}
|
||||||
|
if (names.length === 0) return `Files, Max ${maxFiles}`;
|
||||||
|
return `${names.join(", ")}, Max ${maxFiles}`;
|
||||||
|
})();
|
||||||
|
// ----------------- end helper -----------------
|
||||||
|
|
||||||
|
const notify = useCallback(
|
||||||
|
(files: File[]) => {
|
||||||
|
onFilesChange?.(files);
|
||||||
|
},
|
||||||
|
[onFilesChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFiles = (files: FileList | null) => {
|
||||||
|
if (!files) return;
|
||||||
|
|
||||||
|
const newFiles = Array.from(files).filter(validateFile);
|
||||||
|
const totalFiles = uploadedFiles.length + newFiles.length;
|
||||||
|
|
||||||
|
if (totalFiles > maxFiles) {
|
||||||
|
toast({
|
||||||
|
title: "Too Many Files",
|
||||||
|
description: `You can only upload up to ${maxFiles} files.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFiles = [...uploadedFiles, ...newFiles];
|
||||||
|
setUploadedFiles(updatedFiles);
|
||||||
|
notify(updatedFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnter = useCallback(
|
||||||
|
(e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback(
|
||||||
|
(e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback(
|
||||||
|
(e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!isDragging) {
|
||||||
|
setIsDragging(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isDragging]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDrop = useCallback(
|
||||||
|
(e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
handleFiles(e.dataTransfer.files);
|
||||||
|
},
|
||||||
|
[uploadedFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFileSelect = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
handleFiles(e.target.files);
|
||||||
|
},
|
||||||
|
[uploadedFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBrowseClick = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = (index: number) => {
|
||||||
|
const newFiles = [...uploadedFiles];
|
||||||
|
newFiles.splice(index, 1);
|
||||||
|
setUploadedFiles(newFiles);
|
||||||
|
notify(newFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
// expose imperative handle to parent
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
getFiles: () => uploadedFiles.slice(),
|
||||||
|
reset: () => {
|
||||||
|
setUploadedFiles([]);
|
||||||
|
notify([]);
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||||
|
},
|
||||||
|
removeFile: (index: number) => {
|
||||||
|
handleRemoveFile(index);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[uploadedFiles, notify]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
accept={acceptedFileTypes}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"border-2 border-dashed rounded-lg p-8 flex flex-col items-center justify-center text-center transition-colors",
|
||||||
|
isDragging
|
||||||
|
? "border-primary bg-primary/5"
|
||||||
|
: "border-muted-foreground/25",
|
||||||
|
isUploading && "opacity-50 cursor-not-allowed"
|
||||||
|
)}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={!isUploading ? handleBrowseClick : undefined}
|
||||||
|
style={{ minHeight: "200px" }}
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="animate-spin">
|
||||||
|
<Upload className="h-10 w-10 text-primary" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium">Uploading files...</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-medium">Uploading files...</p>
|
) : uploadedFiles.length > 0 ? (
|
||||||
</div>
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
) : uploadedFiles.length > 0 ? (
|
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
|
||||||
<p className="font-medium text-primary">
|
|
||||||
{uploadedFiles.length} file(s) uploaded
|
|
||||||
</p>
|
|
||||||
<ul className="w-full text-left space-y-2">
|
|
||||||
{uploadedFiles.map((file, index) => (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className="flex justify-between items-center border-b pb-1"
|
|
||||||
>
|
|
||||||
<span className="text-sm">{file.name}</span>
|
|
||||||
<button
|
|
||||||
className="ml-2 p-1 text-muted-foreground hover:text-red-500"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleRemoveFile(index);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
|
||||||
<FilePlus className="h-12 w-12 text-primary/70" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-primary">
|
<p className="font-medium text-primary">
|
||||||
Drag and drop PDF or Image files here
|
{uploadedFiles.length} file(s) uploaded
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
Or click to browse files
|
|
||||||
</p>
|
</p>
|
||||||
|
<ul className="w-full text-left space-y-2">
|
||||||
|
{uploadedFiles.map((file, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="flex justify-between items-center border-b pb-1"
|
||||||
|
>
|
||||||
|
<span className="text-sm">{file.name}</span>
|
||||||
|
<button
|
||||||
|
className="ml-2 p-1 text-muted-foreground hover:text-red-500"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemoveFile(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
) : (
|
||||||
type="button"
|
<div className="flex flex-col items-center gap-4">
|
||||||
variant="secondary"
|
<FilePlus className="h-12 w-12 text-primary/70" />
|
||||||
onClick={(e) => {
|
<div>
|
||||||
e.stopPropagation();
|
<p className="font-medium text-primary">{uploadTitle}</p>
|
||||||
handleBrowseClick();
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
}}
|
Or click to browse files
|
||||||
>
|
</p>
|
||||||
Browse files
|
</div>
|
||||||
</Button>
|
<Button
|
||||||
<p className="text-xs text-muted-foreground">
|
type="button"
|
||||||
Allowed types: PDF, JPG, PNG — Max {maxFiles} files, 5MB each
|
variant="default"
|
||||||
</p>
|
onClick={(e) => {
|
||||||
</div>
|
e.stopPropagation();
|
||||||
)}
|
handleBrowseClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Browse files
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
|
MultipleFileUploadZone.displayName = "MultipleFileUploadZone";
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
// PaymentOCRBlock.tsx
|
// PaymentOCRBlock.tsx
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Upload, Image as ImageIcon, X, Plus } from "lucide-react";
|
import { Image as ImageIcon, X, Plus } from "lucide-react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
@@ -16,16 +22,27 @@ import {
|
|||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { QK_PAYMENTS_RECENT_BASE } from "@/components/payments/payments-recent-table";
|
import { QK_PAYMENTS_RECENT_BASE } from "@/components/payments/payments-recent-table";
|
||||||
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||||
|
import {
|
||||||
|
MultipleFileUploadZone,
|
||||||
|
MultipleFileUploadZoneHandle,
|
||||||
|
} from "../file-upload/multiple-file-upload-zone";
|
||||||
|
|
||||||
// ---------------- Types ----------------
|
// ---------------- Types ----------------
|
||||||
|
|
||||||
type Row = { __id: number } & Record<string, string | number | null>;
|
type Row = { __id: number } & Record<string, string | number | null>;
|
||||||
|
|
||||||
export default function PaymentOCRBlock() {
|
export default function PaymentOCRBlock() {
|
||||||
// UI state
|
//Config
|
||||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
const MAX_FILES = 10;
|
||||||
const [uploadedImages, setUploadedImages] = React.useState<File[]>([]);
|
const ACCEPTED_FILE_TYPES = "image/jpeg,image/jpg,image/png,image/webp";
|
||||||
const [isDragging, setIsDragging] = React.useState(false);
|
const TITLE = "Payment Document OCR";
|
||||||
|
const DESCRIPTION =
|
||||||
|
"You can upload up to 10 files. Allowed types: JPG, PNG, WEBP.";
|
||||||
|
|
||||||
|
// FILE/ZONE state
|
||||||
|
const uploadZoneRef = React.useRef<MultipleFileUploadZoneHandle | null>(null);
|
||||||
|
const [filesForUI, setFilesForUI] = React.useState<File[]>([]); // reactive UI only
|
||||||
|
const [isUploading, setIsUploading] = React.useState(false); // forwarded to zone
|
||||||
const [isExtracting, setIsExtracting] = React.useState(false);
|
const [isExtracting, setIsExtracting] = React.useState(false);
|
||||||
|
|
||||||
// extracted rows shown only inside modal
|
// extracted rows shown only inside modal
|
||||||
@@ -86,52 +103,27 @@ export default function PaymentOCRBlock() {
|
|||||||
|
|
||||||
// ---- handlers (all in this file) -----------------------------------------
|
// ---- handlers (all in this file) -----------------------------------------
|
||||||
|
|
||||||
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
// Called by zone when its internal list changes (keeps parent UI reactive)
|
||||||
const list = Array.from(e.target.files || []);
|
const handleZoneFilesChange = React.useCallback((files: File[]) => {
|
||||||
if (!list.length) return;
|
setFilesForUI(files);
|
||||||
if (list.length > 10) {
|
|
||||||
setError("You can only upload up to 10 images.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setUploadedImages(list);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleImageDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
// Remove a single file by asking the zone to remove it (zone exposes removeFile)
|
||||||
e.preventDefault();
|
const removeUploadedFile = React.useCallback((index: number) => {
|
||||||
setIsDragging(false);
|
uploadZoneRef.current?.removeFile(index);
|
||||||
const list = Array.from(e.dataTransfer.files || []).filter((f) =>
|
// zone will call onFilesChange and update filesForUI automatically
|
||||||
f.type.startsWith("image/")
|
}, []);
|
||||||
);
|
|
||||||
if (!list.length) {
|
|
||||||
setError("Please drop image files (JPG/PNG/TIFF/BMP).");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (list.length > 10) {
|
|
||||||
setError("You can only upload up to 10 images.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploadedImages(list);
|
|
||||||
setError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeUploadedImage = (index: number) => {
|
|
||||||
setUploadedImages((prev) => {
|
|
||||||
const next = prev.filter((_, i) => i !== index);
|
|
||||||
if (next.length === 0) {
|
|
||||||
setRows([]);
|
|
||||||
setModalColumns([]);
|
|
||||||
setError(null);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Extract: read files from zone via ref and call mutation
|
||||||
const handleExtract = () => {
|
const handleExtract = () => {
|
||||||
if (!uploadedImages.length) return;
|
const files = uploadZoneRef.current?.getFiles() ?? [];
|
||||||
|
if (!files.length) {
|
||||||
|
setError("Please upload at least one file to extract.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsExtracting(true);
|
setIsExtracting(true);
|
||||||
extractPaymentOCR.mutate(uploadedImages);
|
extractPaymentOCR.mutate(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -197,14 +189,13 @@ export default function PaymentOCRBlock() {
|
|||||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }), // recent patients list
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }), // recent patients list
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ✅ CLEAR UI: remove files and table rows
|
// ✅ CLEAR UI: reset zone + modal + rows
|
||||||
setUploadedImages([]);
|
uploadZoneRef.current?.reset();
|
||||||
|
setFilesForUI([]);
|
||||||
setRows([]);
|
setRows([]);
|
||||||
setModalColumns([]);
|
setModalColumns([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsDragging(false);
|
setIsExtracting(false);
|
||||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
||||||
|
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast({
|
toast({
|
||||||
@@ -217,107 +208,67 @@ export default function PaymentOCRBlock() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-xl font-medium text-gray-800">
|
|
||||||
Payment Document OCR
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6 space-y-6">
|
<CardHeader>
|
||||||
|
<CardTitle>{TITLE}</CardTitle>
|
||||||
|
<CardDescription>{DESCRIPTION}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
{/* Upload block */}
|
{/* Upload block */}
|
||||||
<div
|
<div className="bg-gray-100 p-4 rounded-md space-y-4">
|
||||||
className={`flex-1 border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
<MultipleFileUploadZone
|
||||||
isDragging
|
ref={uploadZoneRef}
|
||||||
? "border-blue-400 bg-blue-50"
|
isUploading={isUploading}
|
||||||
: uploadedImages.length
|
acceptedFileTypes={ACCEPTED_FILE_TYPES}
|
||||||
? "border-green-400 bg-green-50"
|
maxFiles={MAX_FILES}
|
||||||
: "border-gray-300 bg-gray-50 hover:border-gray-400"
|
onFilesChange={handleZoneFilesChange} // reactive UI only
|
||||||
}`}
|
/>
|
||||||
onDrop={handleImageDrop}
|
|
||||||
onDragOver={(e) => {
|
{/* Show list of files received from the upload zone (UI only) */}
|
||||||
e.preventDefault();
|
{filesForUI.length > 0 && (
|
||||||
setIsDragging(true);
|
<div>
|
||||||
}}
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
onDragLeave={() => setIsDragging(false)}
|
Uploaded ({filesForUI.length}/{MAX_FILES})
|
||||||
onClick={() => {
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = ""; // ✅ reset before opening
|
|
||||||
fileInputRef.current.click();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{uploadedImages.length ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{uploadedImages.map((file, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="flex items-center justify-between space-x-4 border rounded-md p-2 bg-white"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<ImageIcon className="h-6 w-6 text-green-500" />
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="font-medium text-green-700">
|
|
||||||
{file.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{(file.size / 1024 / 1024).toFixed(2)} MB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removeUploadedImage(idx);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Upload className="h-12 w-12 text-gray-400 mx-auto" />
|
|
||||||
<div>
|
|
||||||
<p className="text-lg font-medium text-gray-700 mb-2">
|
|
||||||
Upload Payment Documents
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
|
||||||
Drag and drop up to 10 images or click to browse
|
|
||||||
</p>
|
|
||||||
<Button variant="outline" type="button">
|
|
||||||
Choose Images
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Supported formats: JPG, PNG, TIFF, BMP • Max size: 10MB each
|
|
||||||
</p>
|
</p>
|
||||||
|
<ul className="space-y-2 max-h-48 overflow-auto">
|
||||||
|
{filesForUI.map((file, idx) => (
|
||||||
|
<li
|
||||||
|
key={`${file.name}-${file.size}-${idx}`}
|
||||||
|
className="flex items-center justify-between border rounded-md p-2 bg-white"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ImageIcon className="h-6 w-6 text-green-500" />
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="font-medium text-green-700 truncate">
|
||||||
|
{file.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeUploadedFile(idx)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
id="image-upload-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={(e) => {
|
|
||||||
handleImageSelect(e);
|
|
||||||
e.currentTarget.value = "";
|
|
||||||
}}
|
|
||||||
className="hidden"
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Extract */}
|
{/* Extract */}
|
||||||
<div className="flex justify-end gap-4">
|
<div className="mt-4 flex justify-end gap-4">
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-12 gap-2"
|
className="w-full h-12 gap-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleExtract}
|
onClick={handleExtract}
|
||||||
disabled={!uploadedImages.length || isExtracting}
|
disabled={isExtracting || !filesForUI.length}
|
||||||
>
|
>
|
||||||
{extractPaymentOCR.isPending
|
{extractPaymentOCR.isPending
|
||||||
? "Extracting..."
|
? "Extracting..."
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { PatientTable, qkPatients } from "@/components/patients/patient-table";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
||||||
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
|
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, RefreshCw, File, FilePlus } from "lucide-react";
|
import { Plus, RefreshCw, FilePlus } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -168,21 +168,22 @@ export default function PatientsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File Upload Zone */}
|
{/* File Upload Zone */}
|
||||||
<div className="mb-8">
|
<div className="space-y-8 py-8">
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-xl font-medium text-gray-800">
|
|
||||||
Upload Patient Document
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6 space-y-6">
|
<CardHeader>
|
||||||
|
<CardTitle>Upload Patient Document</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
You can upload 1 file. Allowed types: PDF
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
<FileUploadZone
|
<FileUploadZone
|
||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleFileUpload}
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
acceptedFileTypes="application/pdf"
|
acceptedFileTypes="application/pdf"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<div className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-12 gap-2"
|
className="w-full h-12 gap-2"
|
||||||
disabled={!uploadedFile || isExtracting}
|
disabled={!uploadedFile || isExtracting}
|
||||||
|
|||||||
Reference in New Issue
Block a user