-
Notifications
You must be signed in to change notification settings - Fork 4
feat(#347): add bulk document review dialog with shared notes #665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -100,6 +100,11 @@ export function DocumentsTab({ | |||||||||||||||||||||
| const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); | ||||||||||||||||||||||
| const [isBulkUpdating, setIsBulkUpdating] = useState(false); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Bulk review dialog state | ||||||||||||||||||||||
| const [bulkReviewDialogOpen, setBulkReviewDialogOpen] = useState(false); | ||||||||||||||||||||||
| const [bulkReviewStatus, setBulkReviewStatus] = useState<string>(""); | ||||||||||||||||||||||
| const [bulkReviewNotes, setBulkReviewNotes] = useState<string>(""); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Debounce search input. Search is local to the current page, so we don't | ||||||||||||||||||||||
| // need to reset server-side pagination on every keystroke. | ||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
|
|
@@ -183,7 +188,7 @@ export function DocumentsTab({ | |||||||||||||||||||||
| setSelectedIds(next); | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleBulkStatusUpdate = async (newStatus: string) => { | ||||||||||||||||||||||
| const handleBulkStatusUpdate = async (newStatus: string, notes?: string) => { | ||||||||||||||||||||||
| if (selectedIds.size === 0) return; | ||||||||||||||||||||||
| setIsBulkUpdating(true); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -196,7 +201,10 @@ export function DocumentsTab({ | |||||||||||||||||||||
| { | ||||||||||||||||||||||
| method: "PATCH", | ||||||||||||||||||||||
| headers: { "Content-Type": "application/json" }, | ||||||||||||||||||||||
| body: JSON.stringify({ reviewStatus: newStatus }), | ||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||
| reviewStatus: newStatus, | ||||||||||||||||||||||
| reviewNotes: notes?.trim() || null, | ||||||||||||||||||||||
| }), | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| ), | ||||||||||||||||||||||
| ), | ||||||||||||||||||||||
|
|
@@ -206,14 +214,17 @@ export function DocumentsTab({ | |||||||||||||||||||||
| const failed = results.length - succeeded; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| toast({ | ||||||||||||||||||||||
| title: "Bulk Update Complete", | ||||||||||||||||||||||
| title: "Bulk Review Complete", | ||||||||||||||||||||||
| description: failed | ||||||||||||||||||||||
| ? `${succeeded} updated, ${failed} failed` | ||||||||||||||||||||||
| : `${succeeded} document${succeeded !== 1 ? "s" : ""} updated to ${getStatusLabel(newStatus)}`, | ||||||||||||||||||||||
| variant: failed ? "destructive" : "default", | ||||||||||||||||||||||
|
Comment on lines
218
to
221
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The To fix this, you should ensure that the promises passed to |
||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| setSelectedIds(new Set()); | ||||||||||||||||||||||
| setBulkReviewDialogOpen(false); | ||||||||||||||||||||||
| setBulkReviewStatus(""); | ||||||||||||||||||||||
| setBulkReviewNotes(""); | ||||||||||||||||||||||
|
Comment on lines
224
to
+227
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The selection is cleared and the dialog is closed regardless of whether some updates failed. If
Suggested change
|
||||||||||||||||||||||
| onRefresh?.(); | ||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||
| toast({ | ||||||||||||||||||||||
|
|
@@ -408,26 +419,13 @@ export function DocumentsTab({ | |||||||||||||||||||||
| <span className="text-sm font-medium text-blue-800"> | ||||||||||||||||||||||
| {selectedIds.size} selected | ||||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| <Select | ||||||||||||||||||||||
| value="" | ||||||||||||||||||||||
| onValueChange={handleBulkStatusUpdate} | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||
| onClick={() => setBulkReviewDialogOpen(true)} | ||||||||||||||||||||||
| disabled={isBulkUpdating} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| <SelectTrigger className="w-[180px] h-8"> | ||||||||||||||||||||||
| <SelectValue | ||||||||||||||||||||||
| placeholder={ | ||||||||||||||||||||||
| isBulkUpdating ? "Updating..." : "Set status..." | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| </SelectTrigger> | ||||||||||||||||||||||
| <SelectContent> | ||||||||||||||||||||||
| <SelectItem value="PENDING">Pending</SelectItem> | ||||||||||||||||||||||
| <SelectItem value="IN_REVIEW">In Review</SelectItem> | ||||||||||||||||||||||
| <SelectItem value="APPROVED">Approved</SelectItem> | ||||||||||||||||||||||
| <SelectItem value="REJECTED">Rejected</SelectItem> | ||||||||||||||||||||||
| <SelectItem value="NEEDS_REVISION">Needs Revision</SelectItem> | ||||||||||||||||||||||
| </SelectContent> | ||||||||||||||||||||||
| </Select> | ||||||||||||||||||||||
| {isBulkUpdating ? "Updating..." : "Review Selected"} | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||
|
|
@@ -712,6 +710,92 @@ export function DocumentsTab({ | |||||||||||||||||||||
| </DialogContent> | ||||||||||||||||||||||
| </Dialog> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Bulk Review Dialog */} | ||||||||||||||||||||||
| <Dialog | ||||||||||||||||||||||
| open={bulkReviewDialogOpen} | ||||||||||||||||||||||
| onOpenChange={(open) => { | ||||||||||||||||||||||
| setBulkReviewDialogOpen(open); | ||||||||||||||||||||||
| if (!open) { | ||||||||||||||||||||||
| setBulkReviewStatus(""); | ||||||||||||||||||||||
| setBulkReviewNotes(""); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| <DialogContent className="sm:max-w-[500px]"> | ||||||||||||||||||||||
| <DialogHeader> | ||||||||||||||||||||||
| <DialogTitle>Review {selectedIds.size} Documents</DialogTitle> | ||||||||||||||||||||||
| <DialogDescription> | ||||||||||||||||||||||
| Set a review status and optional notes for all selected documents. | ||||||||||||||||||||||
| </DialogDescription> | ||||||||||||||||||||||
| </DialogHeader> | ||||||||||||||||||||||
| <div className="grid gap-4 py-4"> | ||||||||||||||||||||||
| <div className="space-y-2"> | ||||||||||||||||||||||
| <label className="text-sm font-medium">Review Status</label> | ||||||||||||||||||||||
| <Select value={bulkReviewStatus} onValueChange={setBulkReviewStatus}> | ||||||||||||||||||||||
| <SelectTrigger> | ||||||||||||||||||||||
| <SelectValue placeholder="Select status" /> | ||||||||||||||||||||||
| </SelectTrigger> | ||||||||||||||||||||||
| <SelectContent> | ||||||||||||||||||||||
| <SelectItem value="PENDING">Pending</SelectItem> | ||||||||||||||||||||||
| <SelectItem value="IN_REVIEW">In Review</SelectItem> | ||||||||||||||||||||||
| <SelectItem value="APPROVED">Approved</SelectItem> | ||||||||||||||||||||||
| <SelectItem value="REJECTED">Rejected</SelectItem> | ||||||||||||||||||||||
| <SelectItem value="NEEDS_REVISION">Needs Revision</SelectItem> | ||||||||||||||||||||||
|
Comment on lines
+739
to
+743
|
||||||||||||||||||||||
| </SelectContent> | ||||||||||||||||||||||
| </Select> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| <div className="space-y-2"> | ||||||||||||||||||||||
| <label className="text-sm font-medium"> | ||||||||||||||||||||||
| Shared Notes{" "} | ||||||||||||||||||||||
| <span className="text-gray-400 font-normal">(optional)</span> | ||||||||||||||||||||||
| </label> | ||||||||||||||||||||||
| <Textarea | ||||||||||||||||||||||
| placeholder="Add notes that will apply to all selected documents..." | ||||||||||||||||||||||
| value={bulkReviewNotes} | ||||||||||||||||||||||
| onChange={(e) => setBulkReviewNotes(e.target.value)} | ||||||||||||||||||||||
| className="min-h-[100px]" | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| <div className="space-y-2"> | ||||||||||||||||||||||
| <label className="text-sm font-medium">Selected Documents</label> | ||||||||||||||||||||||
| <div className="max-h-[200px] overflow-y-auto border rounded-md p-2 space-y-2"> | ||||||||||||||||||||||
| {documents | ||||||||||||||||||||||
| .filter((d) => selectedIds.has(d.id)) | ||||||||||||||||||||||
| .map((doc) => ( | ||||||||||||||||||||||
| <div | ||||||||||||||||||||||
| key={doc.id} | ||||||||||||||||||||||
| className="flex items-center justify-between text-sm py-1" | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| <span className="truncate mr-2">{doc.originalName}</span> | ||||||||||||||||||||||
| <Badge | ||||||||||||||||||||||
| variant="secondary" | ||||||||||||||||||||||
| className={`shrink-0 ${getStatusColor(doc.reviewStatus)}`} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| {getStatusLabel(doc.reviewStatus)} | ||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ))} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| <DialogFooter> | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||
| onClick={() => setBulkReviewDialogOpen(false)} | ||||||||||||||||||||||
| disabled={isBulkUpdating} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| Cancel | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| onClick={() => handleBulkStatusUpdate(bulkReviewStatus, bulkReviewNotes)} | ||||||||||||||||||||||
| disabled={isBulkUpdating || !bulkReviewStatus} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| {isBulkUpdating ? "Updating..." : "Review All"} | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| </DialogFooter> | ||||||||||||||||||||||
| </DialogContent> | ||||||||||||||||||||||
| </Dialog> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Response Upload Dialog */} | ||||||||||||||||||||||
| {documentForResponse && ( | ||||||||||||||||||||||
| <ConsultantResponseUpload | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetch()resolves even on non-2xx HTTP responses, so thisPromise.allSettled([...fetch(...)])will reportfulfilledfor 400/401/404 responses. That makes thesucceeded/failedcounts and toast inaccurate, and can close the dialog even when the server rejected updates. Consider wrapping the request in an async helper that checksresponse.okand throws/returns a failure value so non-OK responses are counted as failures (optionally include the error body/message).