Added complete dashboard backend and frontend for dynamic data#117
Added complete dashboard backend and frontend for dynamic data#117SushantBhandari04 wants to merge 9 commits intoyashvikram30:mainfrom
Conversation
|
@SushantBhandari04 is attempting to deploy a commit to the yash vikram's projects Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughAdds authenticated Cases, Documents, and Timeline APIs with MongoDB models; integrates Cloudinary for document storage; refactors Dashboard to use APIs; adds case/document/timeline UI components and forms; updates calendar UI and global styles; provides client libs, docs, dependency updates, and a pdf-parse patch. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant D as Dashboard (Client)
participant API as /api/cases
participant DB as MongoDB (Cases)
participant T as Timeline Gen
U->>D: Create Case (CaseForm submit)
D->>API: POST /api/cases {case data}
API->>DB: Insert Case (userId+caseNumber)
API->>T: generateInitialTimelineEvents(case)
T-->>API: [events]
API->>DB: Insert TimelineEvents (best-effort)
API-->>D: 201 {case}
D-->>U: Case created (refresh list)
sequenceDiagram
autonumber
actor U as User
participant D as Dashboard (Client)
participant DocAPI as /api/cases/:id/documents
participant DocIdAPI as /api/cases/:id/documents/:docId
participant DB as MongoDB (Documents)
participant C as Cloudinary
rect rgba(230,245,255,0.5)
U->>D: Open Upload Document
D->>DocAPI: POST multipart/form-data (file+meta)
DocAPI->>C: upload_stream(file) [folder per case]
C-->>DocAPI: {url, public_id}
DocAPI->>DB: Save Document metadata
DocAPI-->>D: 201 {document}
end
rect rgba(240,255,240,0.5)
U->>D: Download Document
D->>DocIdAPI: GET
DocIdAPI->>DB: Find & increment downloadCount
DocIdAPI->>C: Fetch file buffer (signed URL)
C-->>DocIdAPI: Buffer
DocIdAPI-->>D: Binary with headers
end
rect rgba(255,240,240,0.5)
U->>D: Delete Document (confirm)
D->>DocIdAPI: DELETE
DocIdAPI->>C: Destroy public_id (best-effort)
DocIdAPI->>DB: Remove document record
DocIdAPI-->>D: 200 {ok}
end
sequenceDiagram
autonumber
actor U as User
participant TL as Timeline UI
participant API as /api/cases/:id/timeline
participant DB as MongoDB (TimelineEvents)
U->>TL: View Timeline
TL->>API: GET
API->>DB: Query by caseId+userId
DB-->>API: [events]
API-->>TL: 200 [events]
TL-->>U: Render timeline
U->>TL: Add/Update Event
TL->>API: POST/PUT {event data}
API->>DB: Insert/Update
API-->>TL: 201/200 {event}
TL-->>U: Refresh list
U->>TL: Delete Event
TL->>API: DELETE /:eventId
API->>DB: Remove event
API-->>TL: 200 {ok}
TL-->>U: Remove from UI
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Poem
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 38
🧹 Nitpick comments (45)
patches/pdf-parse+1.1.1.patch (1)
11-13: Document the reason for patching pdf-parse.The patch disables debug mode by hardcoding
isDebugMode = false. While this might resolve immediate issues, patching node_modules is a maintenance burden. Consider:
- Adding a comment in the patch file explaining why this change is necessary
- Creating an issue upstream to fix the underlying problem
- Looking for alternative PDF parsing libraries that don't require patching
.env.example (1)
11-13: Consider the static analysis hint about key ordering.While not critical, maintaining alphabetical order of environment variables improves maintainability. The keys could be reordered as suggested by the linter.
# cloudinary -CLOUDINARY_CLOUD_NAME= CLOUDINARY_API_KEY= CLOUDINARY_API_SECRET= +CLOUDINARY_CLOUD_NAME=model/TimelineEvent.ts (2)
23-26: Consider using ObjectId type for references instead of String.The
caseIdanduserIdfields are defined as String type but reference other collections. Usingmongoose.Schema.Types.ObjectIdwould provide better type safety and consistency with MongoDB best practices.const TimelineEventSchema: Schema<TimelineEvent> = new mongoose.Schema({ caseId: { - type: String, + type: mongoose.Schema.Types.ObjectId, required: [true, "Case ID is required"], ref: "Case" }, // ... other fields ... userId: { - type: String, + type: mongoose.Schema.Types.ObjectId, required: [true, "User ID is required"], ref: "User" }Also update the TypeScript interface:
export interface TimelineEvent extends Document { - caseId: string; // Reference to the case + caseId: mongoose.Types.ObjectId; // Reference to the case // ... other fields ... - userId: string; // Reference to the user who owns this case + userId: mongoose.Types.ObjectId; // Reference to the user who owns this caseAlso applies to: 59-62
52-58: Validate the metadata structure to prevent data inconsistency.The metadata field accepts arbitrary values without validation. Consider adding validation for the expected number ranges and string formats.
metadata: { stage: String, status: String, - progress: Number, + progress: { + type: Number, + min: 0, + max: 100 + }, documentUrl: String, notes: String },app/api/cases/[id]/timeline/[eventId]/route.ts (1)
92-92: Add metadata validation if provided.The metadata field is accepted from the request body without validation. Consider validating its structure to prevent invalid data from being stored.
const body = await request.json(); const { title, description, eventDate, eventType, status, metadata } = body; + + // Validate metadata structure if provided + if (metadata) { + if (metadata.progress !== undefined && + (typeof metadata.progress !== 'number' || metadata.progress < 0 || metadata.progress > 100)) { + return NextResponse.json( + { error: "Progress must be a number between 0 and 100" }, + { status: 400 } + ); + } + }components/document-list.tsx (2)
61-67: Remove or conditionally include console.log statements in production.Multiple console.log statements are present for debugging. These should be removed or wrapped in a development-only condition.
- console.log("Starting download for:", fileName) + if (process.env.NODE_ENV === 'development') { + console.log("Starting download for:", fileName) + } const response = await fetch(`/api/cases/${caseId}/documents/${docId}`, { credentials: "include", }) - console.log("Download response status:", response.status) - console.log("Download response headers:", Object.fromEntries(response.headers.entries())) + if (process.env.NODE_ENV === 'development') { + console.log("Download response status:", response.status) + console.log("Download response headers:", Object.fromEntries(response.headers.entries())) + }
151-157: Extract magic number 1024 to a named constant.The formatFileSize function uses the magic number 1024. Consider extracting it to a named constant for clarity.
const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes' - const k = 1024 + const BYTES_PER_KB = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + const i = Math.floor(Math.log(bytes) / Math.log(BYTES_PER_KB)) + return parseFloat((bytes / Math.pow(BYTES_PER_KB, i)).toFixed(2)) + ' ' + sizes[i] }model/Document.ts (3)
78-81: Speed up tag-based filtering.If UI filters by tags, add a multikey index.
DocumentSchema.index({ caseId: 1, uploadDate: -1 }); DocumentSchema.index({ userId: 1, uploadDate: -1 }); DocumentSchema.index({ caseId: 1, documentType: 1 }); DocumentSchema.index({ fileName: "text", originalName: "text", description: "text" }); // Text search +DocumentSchema.index({ tags: 1 });Also applies to: 102-107
65-68: Consider relying on createdAt instead of custom uploadDate.You already have timestamps; unless you need a user-editable uploadDate, you can remove uploadDate and re-use createdAt to reduce duplication and indexing overhead.
Would you like a migration note to drop uploadDate and re-point queries to createdAt?
Also applies to: 99-100
1-1: Naming nit: avoid collision with DOM Document type.Importing Mongoose’s Document as Document can confuse TS projects with "dom" lib. Consider aliasing (e.g., MongooseDocument) or using HydratedDocument for stronger typing.
Also applies to: 3-22
app/api/cases/[id]/documents/[docId]/route.ts (2)
41-46: Count downloads only on successful fetch.Incrementing before fetch skews analytics and can double-count failures. Move the update after successful buffer retrieval.
- // Update download count and last accessed - await DocumentModel.findByIdAndUpdate(docId, { - $inc: { downloadCount: 1 }, - lastAccessed: new Date() - }); @@ - // Return file with appropriate headers + // Update download count and last accessed (success only) + await DocumentModel.findByIdAndUpdate(docId, { + $inc: { downloadCount: 1 }, + lastAccessed: new Date() + }); + + // Return file with appropriate headers return new NextResponse(fileBuffer, { headers: { - "Content-Type": document.mimeType, - "Content-Disposition": `attachment; filename="${document.originalName}"`, - "Content-Length": fileBuffer.length.toString(), - "Cache-Control": "no-cache", + "Content-Type": document.mimeType, + // sanitized filename set below + "Content-Disposition": `attachment; filename="${(document.originalName || 'download').replace(/[\r\n"]/g, '_')}"; filename*=UTF-8''${encodeURIComponent(document.originalName || 'download')}`, + "Content-Length": fileBuffer.length.toString(), + "Cache-Control": "private, no-store", }, });Also applies to: 61-69
48-60: Reduce noisy server logs in production.Gate console.log with an env flag or remove to avoid leaking filenames and sizes in logs.
Also applies to: 110-116
components/timeline-form.tsx (3)
3-3: Sync form when editingEvent changes.Default values are only read on first render. Reset when editingEvent/isOpen changes.
-import { useState } from "react" +import { useState, useEffect } from "react" @@ const form = useForm<TimelineFormValues>({ resolver: zodResolver(timelineFormSchema), defaultValues: { title: editingEvent?.title || "", description: editingEvent?.description || "", eventDate: editingEvent?.eventDate ? new Date(editingEvent.eventDate) : new Date(), eventType: editingEvent?.eventType || "custom", status: editingEvent?.status || "completed", notes: editingEvent?.metadata?.notes || "", }, }) + + useEffect(() => { + form.reset({ + title: editingEvent?.title || "", + description: editingEvent?.description || "", + eventDate: editingEvent?.eventDate ? new Date(editingEvent.eventDate) : new Date(), + eventType: editingEvent?.eventType || "custom", + status: editingEvent?.status || "completed", + notes: editingEvent?.metadata?.notes || "", + }) + }, [editingEvent, isOpen]) // eslint-disable-line react-hooks/exhaustive-depsAlso applies to: 53-64
34-40: Type editingEvent.Avoid any; use your TimelineEvent type to catch mistakes at compile time.
- editingEvent?: any | null + editingEvent?: { _id: string; title: string; description: string; eventDate: string | Date; eventType: "filing" | "hearing" | "evidence" | "document" | "status_change" | "custom"; status?: "completed" | "scheduled" | "pending"; metadata?: { notes?: string } } | nullOr import the canonical TimelineEvent type if available.
297-299: Date guard nit.Consider also preventing extremely far-future dates if that’s invalid for your domain.
components/case-form.tsx (3)
336-351: Make Select controlled (use value instead of defaultValue).Keeps RHF state in sync when editCase or form.reset changes values.
- <Select onValueChange={field.onChange} defaultValue={field.value}> + <Select onValueChange={field.onChange} value={field.value}>Also applies to: 362-375
410-410: Remove console logs in UI handlers.- console.log("Filing date selected:", date)- console.log("Next hearing date selected:", date)Also applies to: 462-462
283-291: Harden numeric parsing for number inputs.Use valueAsNumber and clamp; avoids NaN and enforces bounds in the handler itself.
- onChange={(e) => field.onChange(Number(e.target.value) || 0)} + onChange={(e) => { + const v = e.currentTarget.valueAsNumber; + field.onChange(Number.isFinite(v) ? Math.max(0, v) : 0); + }}- onChange={(e) => field.onChange(Number(e.target.value))} + onChange={(e) => { + const v = e.currentTarget.valueAsNumber; + if (Number.isFinite(v)) field.onChange(Math.min(100, Math.max(0, v))); + }}Also applies to: 304-312
app/api/cases/[id]/documents/route.ts (4)
34-39: Use lean() for read-heavy queries.Avoids hydrating Mongoose documents when you only serialize to JSON.
- const documents = await DocumentModel.find({ + const documents = await DocumentModel.find({ caseId: id, userId: session.user._id - }).sort({ uploadDate: -1 }); + }).sort({ uploadDate: -1 }).lean();
1-1: Explicitly set Node.js runtime for Buffer usage.Prevents accidental edge runtime where Buffer isn’t available.
+export const runtime = "nodejs";
146-147: Consider accepting JSON array for tags in addition to CSV.Improves client flexibility; keep existing CSV behavior as fallback.
- tags: tags ? tags.split(',').map(tag => tag.trim()) : [], + tags: (() => { + if (!tags) return []; + try { + const parsed = JSON.parse(tags); + if (Array.isArray(parsed)) return parsed.map((t) => String(t).trim()); + } catch {} + return tags.split(',').map(tag => tag.trim()); + })(),
92-108: Harden MIME validation fallback.file.type can be empty for some uploads. Fallback to extension-based check.
- if (!allowedTypes.includes(file.type)) { + const mime = file.type || ""; + const ext = (file.name.split(".").pop() || "").toLowerCase(); + const okByMime = allowedTypes.includes(mime); + const okByExt = ["pdf","doc","docx","txt","jpg","jpeg","png","gif"].includes(ext); + if (!(okByMime || okByExt)) {app/api/cases/[id]/timeline/route.ts (1)
41-45: Use lean() for GET timeline reads.Saves hydration cost when returning JSON only.
- const timelineEvents = await TimelineEventModel.find({ + const timelineEvents = await TimelineEventModel.find({ caseId: id, userId: session.user._id - }).sort({ eventDate: 1 }); // Sort from oldest to newest (ascending order) + }).sort({ eventDate: 1 }).lean(); // Sort from oldest to newest (ascending order)app/api/cases/[id]/route.ts (2)
26-33: Prefer robust ObjectId validationUse mongoose’s isValidObjectId (or Types.ObjectId.isValid) over a regex for correctness and readability.
Apply:
- // Validate ObjectId format - if (!caseId.match(/^[0-9a-fA-F]{24}$/)) { + // Validate ObjectId format + if (!Types.ObjectId.isValid(caseId)) { return NextResponse.json( { error: "Invalid case ID format" }, { status: 400 } ); }(Add
import { Types } from "mongoose"at top.)
34-37: Avoid leaking internals; ensure JSON-safe payloadConsider excluding internal fields (e.g., userId) and returning JSON-friendly objects to avoid ObjectId serialization surprises.
- const caseData = await CaseModel.findOne({ + const caseData = await CaseModel.findOne({ _id: caseId, userId: session.user._id -}).lean(); +}) + .select("-userId -__v") + .lean({ getters: true });Also applies to: 46-46
components/document-upload.tsx (4)
63-71: Enforce client-side size/type checks (UI promises 10MB max)Add basic guards to match the UI copy and reduce server load.
- const handleDrop = useCallback((e: React.DragEvent) => { + const MAX_BYTES = 10 * 1024 * 1024; // 10MB + const ALLOWED_EXTS = ["pdf","doc","docx","txt","jpg","jpeg","png","gif"]; + const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) const files = Array.from(e.dataTransfer.files) if (files.length > 0) { - setUploadedFile(files[0]) + const f = files[0] + const ext = f.name.split(".").pop()?.toLowerCase() + if (f.size > MAX_BYTES) return setError("File exceeds 10MB limit") + if (!ext || !ALLOWED_EXTS.includes(ext)) return setError("Unsupported file type") + setUploadedFile(f) } }, []) - const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { + const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { const files = e.target.files if (files && files.length > 0) { - setUploadedFile(files[0]) + const f = files[0] + const ext = f.name.split(".").pop()?.toLowerCase() + if (f.size > MAX_BYTES) return setError("File exceeds 10MB limit") + if (!ext || !ALLOWED_EXTS.includes(ext)) return setError("Unsupported file type") + setUploadedFile(f) } }Also applies to: 73-79
164-166: Prevent closing dialog mid-uploadAvoid user confusion and aborted flows by ignoring close attempts while uploading.
- <Dialog open={isOpen} onOpenChange={onClose}> + <Dialog + open={isOpen} + onOpenChange={(open) => { + if (!isUploading && !open) onClose() + }} + >
220-236: Use a ref instead of document.getElementByIdCleaner and avoids DOM coupling.
- <Input + <Input type="file" onChange={handleFileSelect} accept=".pdf,.doc,.docx,.txt,.jpg,.jpeg,.png,.gif" className="hidden" - id="file-upload" + ref={fileInputRef} /> ... - onClick={() => document.getElementById('file-upload')?.click()} + onClick={() => fileInputRef.current?.click()}Add above component:
const fileInputRef = useRef<HTMLInputElement | null>(null);And import:
import { useState, useCallback, useRef } from "react"
145-148: More resilient error parsingServer may not always return JSON. Fall back to text for clearer messages.
- if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || "Failed to upload document") - } + if (!response.ok) { + const raw = await response.text().catch(() => "") + try { + const errorData = raw ? JSON.parse(raw) : undefined + throw new Error(errorData?.error || "Failed to upload document") + } catch { + throw new Error(raw || "Failed to upload document") + } + }app/api/cases/route.ts (1)
61-67: Return lean, projected docs to reduce payloadsProject out internals and ensure JSON-safety.
- const cases = await CaseModel.find(filter) + const cases = await CaseModel.find(filter) .sort(sort) .skip(skip) .limit(limit) - .lean(); + .select("-userId -__v") + .lean({ getters: true });model/Case.ts (3)
83-86: Avoid drift between lastUpdated and updatedAt (timestamps).With timestamps: true you already get updatedAt. lastUpdated won’t auto-update on updates, risking stale data. Either drop lastUpdated or keep it in sync via hooks.
Example hook to keep it synced (add after the schema definition):
CaseSchema.pre("save", function (next) { this.lastUpdated = new Date(); next(); }); CaseSchema.pre(["updateOne","findOneAndUpdate"], function (next) { // @ts-expect-error: this is a mongoose query this.set({ lastUpdated: new Date() }); next(); });Also applies to: 102-104
1-1: Confirm userId type matches your User _id (ObjectId vs string).If User._id is ObjectId, use Schema.Types.ObjectId + Types.ObjectId here to enable populate/index efficiency; if you truly store string ids, current code is fine. Please verify.
-import mongoose, { Schema, Document } from "mongoose"; +import mongoose, { Schema, Document, Types } from "mongoose"; ... - userId: string; // Reference to the user who owns this case + userId: Types.ObjectId; // Reference to the user who owns this case ... - userId: { - type: String, + userId: { + type: Schema.Types.ObjectId, required: [true, "User ID is required"], ref: "User" }Also applies to: 20-20, 97-101
26-30: Normalize caseNumber to prevent accidental case-sensitive duplicates.If caseNumber should be unique regardless of letter case, normalize to uppercase on write. Simpler than index-level collation and keeps the existing unique index effective.
caseNumber: { type: String, required: [true, "Case number is required"], - trim: true + trim: true, + uppercase: true },Also applies to: 106-109
components/case-timeline.tsx (3)
24-41: Prevent setState after unmount/race conditions when caseId changes fast.Guard the async load to avoid updating state after unmount or id switch.
useEffect(() => { - const loadTimelineEvents = async () => { + let cancelled = false + const loadTimelineEvents = async () => { try { setLoading(true) const events = await fetchTimelineEvents(caseData._id) - setTimelineEvents(events) + if (!cancelled) setTimelineEvents(events) } catch (err) { console.error("Error loading timeline events:", err) - setError("Failed to load timeline events") + if (!cancelled) setError("Failed to load timeline events") } finally { - setLoading(false) + if (!cancelled) setLoading(false) } } if (caseData._id) { loadTimelineEvents() } - }, [caseData._id]) + return () => { cancelled = true } + }, [caseData._id])
44-61: Remove unused parameter from getEventIcon.status isn’t used. Simplify signature and call site.
-const getEventIcon = (eventType: string, status: string) => { +const getEventIcon = (eventType: string) => { ... - icon: getEventIcon(event.eventType, event.status) + icon: getEventIcon(event.eventType)Also applies to: 83-84
9-16: Sort by date and use stable keys.Ensure deterministic timeline order and React keys from backend ids (avoid index).
type TimelineEventDisplay = { + id: string date: string title: string description: string status: "completed" | "current" | "upcoming" eventType: string icon: React.ReactNode } -// Convert timeline events to display format -const displayEvents: TimelineEventDisplay[] = timelineEvents.map(event => { +// Convert + sort (oldest -> newest) for consistent display +const displayEvents: TimelineEventDisplay[] = [...timelineEvents] + .sort((a, b) => new Date(a.eventDate).getTime() - new Date(b.eventDate).getTime()) + .map(event => { const eventDate = new Date(event.eventDate) const isValidDate = !isNaN(eventDate.getTime()) ... - return { + return { + id: (event as any)._id ?? `${event.title}-${event.eventDate}`, date: isValidDate ? formatLocalDate(eventDate) : formatLocalDate(new Date()), title: event.title, description: event.description, status: event.status === "completed" ? "completed" : event.status === "scheduled" ? "upcoming" : "current", eventType: event.eventType, icon: getEventIcon(event.eventType) } }) ... - {displayEvents.map((event, index) => ( - <div key={`${event.date}-${event.title}-${index}`} className="relative flex items-start gap-4"> + {displayEvents.map((event) => ( + <div key={event.id} className="relative flex items-start gap-4">Also applies to: 64-85, 171-174
components/ui/calendar.tsx (1)
158-158: Propagate buttonVariant to day buttons for consistent theming.Currently DayButton uses hardcoded variant="ghost". Pass the chosen variant from Calendar.
- DayButton: CalendarDayButton, + DayButton: (props) => ( + <CalendarDayButton + {...props} + className={cn(buttonVariants({ variant: buttonVariant }), props.className)} + /> + ),Also applies to: 189-193
lib/cases.ts (1)
25-51: Consider adding input validation and error handling improvements.The function handles API calls well, but could benefit from a few enhancements:
- Consider validating numeric parameters (page, limit) to ensure they're positive integers
- The generic error message could be more specific for different HTTP status codes
Consider adding parameter validation:
export async function fetchCases(filters: CaseFilters = {}): Promise<CasesResponse> { const params = new URLSearchParams(); if (filters.status) params.append("status", filters.status); if (filters.stage) params.append("stage", filters.stage); if (filters.search) params.append("search", filters.search); if (filters.sortBy) params.append("sortBy", filters.sortBy); if (filters.sortOrder) params.append("sortOrder", filters.sortOrder); - if (filters.page) params.append("page", filters.page.toString()); - if (filters.limit) params.append("limit", filters.limit.toString()); + if (filters.page && filters.page > 0) params.append("page", filters.page.toString()); + if (filters.limit && filters.limit > 0) params.append("limit", filters.limit.toString());app/dashboard/page.tsx (2)
122-148: Consider optimistic UI updates for better UX.The delete operation could benefit from optimistic updates to provide immediate feedback.
const handleDeleteCase = async (caseId: string) => { + // Store the current state for rollback on error + const previousCases = cases + const previousPagination = pagination + try { setIsDeleting(true) - await deleteCase(caseId) - // Remove the case from the local state + // Optimistically update the UI setCases(prevCases => prevCases.filter(caseItem => caseItem._id !== caseId)) // If the deleted case was selected, clear the selection if (selectedCase && selectedCase._id === caseId) { setSelectedCase(null) } // Update pagination if needed setPagination(prev => ({ ...prev, totalCount: prev.totalCount - 1 })) + // Now perform the actual deletion + await deleteCase(caseId) + } catch (error) { console.error("Error deleting case:", error) + // Rollback on error + setCases(previousCases) + setPagination(previousPagination) setError("Failed to delete case. Please try again.") } finally { setIsDeleting(false) } }
561-565: Add error handling for document refresh.The document refresh after upload should handle potential errors gracefully.
onSuccess={async () => { // Refresh documents list after successful upload if (documentListRef.current) { - await documentListRef.current.refreshDocuments() + try { + await documentListRef.current.refreshDocuments() + } catch (error) { + console.error("Failed to refresh documents:", error) + setError("Document uploaded but failed to refresh list") + } } }}lib/timeline.ts (5)
3-3: Normalize API base and add a URL helper to prevent double slashes and ease encoding.This avoids
//api/...paths and centralizes path building.-const API_BASE = process.env.NEXT_PUBLIC_API_URL || ""; +const API_BASE = (process.env.NEXT_PUBLIC_API_URL ?? "").replace(/\/+$/, "");Add just below (new code):
// Joins base + path and ensures leading slash const api = (path: string) => `${API_BASE}${path.startsWith("/") ? path : `/${path}`}`;
68-89: Avoid preflighting DELETE and encode IDs.
Content-Type: application/jsonon DELETE causes a CORS preflight for no gain. Also encode path params defensively.try { - const response = await fetch(`${API_BASE}/api/cases/${caseId}/timeline/${eventId}`, { + const response = await fetch( + api(`/api/cases/${encodeURIComponent(caseId)}/timeline/${encodeURIComponent(eventId)}`), + { method: "DELETE", - headers: { - "Content-Type": "application/json", - }, credentials: "include", - }); + }); - if (!response.ok) { - throw new Error(`Failed to delete timeline event: ${response.statusText}`); - } + if (!response.ok) { + let message = `Failed to delete timeline event (${response.status})`; + try { + const err = await response.json(); + message = err?.error || err?.message || message; + } catch {} + throw new Error(message); + }
185-201: Stabilize date formatting in descriptions or omit it.
toLocaleDateString()varies by user locale/timezone; UI may show inconsistent text. Consider ISO (toISOString().slice(0,10)) or formatting in the rendering layer.- description: `Next hearing scheduled for ${nextHearingDate.toLocaleDateString()}`, + // Prefer stable formatting or move presentation to UI + description: `Next hearing scheduled for ${nextHearingDate.toISOString().slice(0, 10)}`,
205-206: Remove unused variable or add TODO to use it.
currentStageis declared but unused; likely leftover from commented scaffolding.- const currentStage = caseData.stage?.toLowerCase(); + // TODO: stage-based event expansion can use `caseData.stage` when enabled
1-1: Decouple client DTOs from Mongoose models.Importing model-layer types on the client couples you to Mongoose and
Document. Create shared API DTO types (e.g.,types/timeline.ts) withoutDocument/server fields, and use them here.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (5)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yamluploads/documents/68be8e2fcf058887f12b0c93_1757318732439.jpgis excluded by!**/*.jpguploads/documents/68bea144cf058887f12b0d8e_1757323748165.jpgis excluded by!**/*.jpguploads/documents/68bea90811987f760d2c4d70_1757325609333.jpegis excluded by!**/*.jpeg
📒 Files selected for processing (24)
.env.example(1 hunks)app/api/cases/[id]/documents/[docId]/route.ts(1 hunks)app/api/cases/[id]/documents/route.ts(1 hunks)app/api/cases/[id]/route.ts(1 hunks)app/api/cases/[id]/timeline/[eventId]/route.ts(1 hunks)app/api/cases/[id]/timeline/route.ts(1 hunks)app/api/cases/route.ts(1 hunks)app/dashboard/page.tsx(8 hunks)app/globals.css(1 hunks)components/case-form.tsx(1 hunks)components/case-timeline.tsx(3 hunks)components/document-list.tsx(1 hunks)components/document-upload.tsx(1 hunks)components/timeline-form.tsx(1 hunks)components/ui/calendar.tsx(1 hunks)docs/timeline-user-guide.md(1 hunks)lib/cases.ts(1 hunks)lib/cloudinary.ts(1 hunks)lib/timeline.ts(1 hunks)model/Case.ts(1 hunks)model/Document.ts(1 hunks)model/TimelineEvent.ts(1 hunks)package.json(3 hunks)patches/pdf-parse+1.1.1.patch(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (18)
lib/cases.ts (1)
model/Case.ts (1)
Case(3-23)
components/document-upload.tsx (2)
components/ui/dialog.tsx (5)
Dialog(112-112)DialogContent(117-117)DialogHeader(118-118)DialogTitle(120-120)props(20-29)lib/utils.ts (1)
cn(4-6)
app/api/cases/[id]/timeline/route.ts (3)
app/api/cases/[id]/route.ts (1)
GET(8-55)app/api/cases/route.ts (2)
GET(10-95)POST(98-180)app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)
app/api/cases/[id]/timeline/[eventId]/route.ts (2)
app/api/cases/[id]/route.ts (2)
DELETE(136-190)PUT(58-133)app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)
app/api/cases/[id]/documents/route.ts (2)
app/api/auth/[...nextauth]/options.ts (1)
session(91-97)lib/cloudinary.ts (2)
isCloudinaryConfigured(11-17)uploadToCloudinary(22-71)
app/api/cases/[id]/documents/[docId]/route.ts (2)
app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)lib/cloudinary.ts (3)
isCloudinaryConfigured(11-17)getCloudinaryFileBuffer(114-169)deleteFromCloudinary(74-84)
app/api/cases/[id]/route.ts (3)
app/api/cases/[id]/timeline/route.ts (1)
GET(9-54)app/api/cases/route.ts (1)
GET(10-95)app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)
app/api/cases/route.ts (2)
app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)lib/timeline.ts (1)
generateInitialTimelineEvents(133-440)
model/Document.ts (2)
model/User.ts (1)
User(4-10)types/supabase.ts (1)
Database(3-278)
components/document-list.tsx (1)
model/Document.ts (1)
CaseDocument(3-22)
model/TimelineEvent.ts (1)
model/User.ts (1)
User(4-10)
lib/timeline.ts (1)
model/TimelineEvent.ts (1)
TimelineEvent(3-20)
components/case-form.tsx (2)
model/Case.ts (1)
Case(3-23)lib/cases.ts (2)
updateCase(91-107)createCase(72-88)
components/timeline-form.tsx (2)
lib/timeline.ts (1)
createTimelineEvent(29-66)lib/utils.ts (1)
cn(4-6)
components/case-timeline.tsx (4)
model/Case.ts (1)
Case(3-23)model/TimelineEvent.ts (1)
TimelineEvent(3-20)lib/timeline.ts (1)
fetchTimelineEvents(6-26)components/timeline-form.tsx (1)
TimelineForm(42-362)
model/Case.ts (2)
model/User.ts (1)
User(4-10)types/supabase.ts (1)
Database(3-278)
components/ui/calendar.tsx (2)
components/ui/button.tsx (2)
buttonVariants(56-56)props(43-52)lib/utils.ts (1)
cn(4-6)
app/dashboard/page.tsx (3)
model/Case.ts (1)
Case(3-23)components/document-list.tsx (2)
DocumentListRef(15-17)DocumentList(19-306)lib/cases.ts (3)
CaseFilters(15-23)fetchCases(26-51)deleteCase(110-123)
🪛 dotenv-linter (3.3.0)
.env.example
[warning] 12-12: [UnorderedKey] The CLOUDINARY_API_KEY key should go before the CLOUDINARY_CLOUD_NAME key
(UnorderedKey)
[warning] 13-13: [UnorderedKey] The CLOUDINARY_API_SECRET key should go before the CLOUDINARY_CLOUD_NAME key
(UnorderedKey)
🔇 Additional comments (14)
package.json (1)
51-51: Cloudinary dependency version ^2.7.0 is the latest stable release (2.7.0) and has no direct vulnerabilities reported in Snyk. (npmjs.com, security.snyk.io)docs/timeline-user-guide.md (1)
1-146: Well-structured and comprehensive documentation!The Timeline User Guide provides excellent coverage of the feature with clear organization, visual indicators, and practical examples. The documentation effectively covers user workflows, best practices, and troubleshooting.
app/api/cases/[id]/timeline/[eventId]/route.ts (1)
9-58: LGTM! Well-structured DELETE endpoint with proper validation.The DELETE endpoint correctly validates ObjectIds, verifies case ownership, and handles errors appropriately.
app/api/cases/[id]/documents/[docId]/route.ts (1)
19-27: Optional: validate IDs early.If your models use ObjectId for _id and caseId, reject invalid ids up-front with 400. If caseId is a string in your schema, skip this.
Would you like a snippet using isValidObjectId to add early validation?
Also applies to: 99-105
components/ui/calendar.tsx (2)
128-169: Nice slot overrides and className plumbing.Clean components override (Root/Chevron/WeekNumber) and classNames mapping look solid.
9-10: Validate imports for react-day-picker v9
- DayButton and getDefaultClassNames are exported in react-day-picker v9 (daypicker.dev)
DayButtonProps=Parameters<typeof DayButton>[0]({ day: CalendarDay; modifiers: Modifiers } & ButtonHTMLAttributes<HTMLButtonElement>) (daypicker.dev, app.unpkg.com)lib/cases.ts (3)
1-23: LGTM! Clean API client interfaces.The TypeScript interfaces are well-structured with appropriate optional fields and type constraints. The pagination structure provides comprehensive navigation support.
53-123: LGTM! Consistent CRUD operations implementation.The remaining CRUD functions follow a consistent pattern with proper error handling, appropriate HTTP methods, and credential inclusion for authentication.
45-51: Clarify API error response property
The code throwsnew Error(error.error || …)but the AI summary refers toerror.field. Confirm the actual JSON shape returned by the cases API (e.g. whether errors are undererror,field, or another key) and update the code or summary for consistency.app/dashboard/page.tsx (2)
3-20: LGTM! Well-organized imports.The imports are properly structured with clear separation between external dependencies, UI components, and local modules.
538-538: Remove redundant_idchecks
selectedCase._idis guaranteed by the MongooseDocumentinterface and only accessed whenselectedCaseis non-null, so no additional validation is needed.lib/timeline.ts (3)
157-167: Good defensive validation.Early returns for missing/invalid
caseNumber/filingDatekeep the generator safe.
415-439: Solid fallback path.Gracefully attempts to return at least a “Case Filed” event; good resiliency.
20-22: Confirm shapes match backend for timeline API
The list-fetch function returnsdata.timelineEvents(backend GET returns{ timelineEvents: [...] }), and the create/update functions should usedata.timelineEvent(backend POST/PUT return{ timelineEvent: {...} }). Ensure your client calls for create/update referencetimelineEventrather thantimelineEvents.
| export async function GET( | ||
| request: NextRequest, | ||
| { params }: { params: Promise<{ id: string; docId: string }> } | ||
| ) { |
There was a problem hiding this comment.
Fix params typing (incorrect Promise type) — route handlers receive plain objects.
Typing params as Promise is wrong; remove await usage. This can mask type issues.
export async function GET(
request: NextRequest,
- { params }: { params: Promise<{ id: string; docId: string }> }
+ { params }: { params: { id: string; docId: string } }
) {
@@
- const { id, docId } = await params;
+ const { id, docId } = params;
@@
export async function DELETE(
request: NextRequest,
- { params }: { params: Promise<{ id: string; docId: string }> }
+ { params }: { params: { id: string; docId: string } }
) {
@@
- const { id, docId } = await params;
+ const { id, docId } = params;Also applies to: 20-21, 86-89, 96-98
🤖 Prompt for AI Agents
In app/api/cases/[id]/documents/[docId]/route.ts around lines 9-12 (and also at
20-21, 86-89, 96-98) the route handler signatures incorrectly type params as a
Promise<{ id: string; docId: string }> and use await on params; change the
signatures to accept plain objects (e.g. { params: { id: string; docId: string }
}) and remove any await/async usage that awaited params, updating any local uses
to access params.id and params.docId directly so the handlers use the correct
non-Promise params shape.
| // Return file with appropriate headers | ||
| return new NextResponse(fileBuffer, { | ||
| headers: { | ||
| "Content-Type": document.mimeType, | ||
| "Content-Disposition": `attachment; filename="${document.originalName}"`, | ||
| "Content-Length": fileBuffer.length.toString(), | ||
| "Cache-Control": "no-cache", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Harden Content-Disposition against header injection; add RFC 5987 filename= fallback.*
Original name can contain quotes/newlines. Sanitize and include filename* for UTF-8.
The diff above includes a safe header. If you prefer explicit vars:
- // Return file with appropriate headers
- return new NextResponse(fileBuffer, {
+ const safeName = (document.originalName || 'download').replace(/[\r\n"]/g, '_');
+ const encoded = encodeURIComponent(safeName);
+ return new NextResponse(fileBuffer, {
headers: {
- "Content-Disposition": `attachment; filename="${document.originalName}"`,
+ "Content-Disposition": `attachment; filename="${safeName}"; filename*=UTF-8''${encoded}`,
},
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Return file with appropriate headers | |
| return new NextResponse(fileBuffer, { | |
| headers: { | |
| "Content-Type": document.mimeType, | |
| "Content-Disposition": `attachment; filename="${document.originalName}"`, | |
| "Content-Length": fileBuffer.length.toString(), | |
| "Cache-Control": "no-cache", | |
| }, | |
| }); | |
| const safeName = (document.originalName || 'download').replace(/[\r\n"]/g, '_'); | |
| const encoded = encodeURIComponent(safeName); | |
| return new NextResponse(fileBuffer, { | |
| headers: { | |
| "Content-Type": document.mimeType, | |
| "Content-Disposition": `attachment; filename="${safeName}"; filename*=UTF-8''${encoded}`, | |
| "Content-Length": fileBuffer.length.toString(), | |
| "Cache-Control": "no-cache", | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In app/api/cases/[id]/documents/[docId]/route.ts around lines 61 to 69, the
Content-Disposition header currently uses document.originalName directly which
can allow header injection and break non-ASCII names; sanitize the original name
by stripping CR/LF and quotes (or replace with safe characters), derive a safe
ASCII fallback (e.g., basename with non-word chars replaced) for the filename
parameter, and add an RFC 5987 filename* parameter using UTF-8'' +
percent-encoded UTF-8 form of the original name (encodeURIComponent) so
non-ASCII names are preserved; ensure you set both Content-Disposition values
(attachment; filename="safe-fallback"; filename*=UTF-8''encodedName) and keep
other headers unchanged.
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { getServerSession } from "next-auth"; | ||
| import { authOptions } from "@/app/api/auth/[...nextauth]/options"; | ||
| import dbConnect from "@/lib/dbConnect"; | ||
| import CaseModel from "@/model/Case"; | ||
| import DocumentModel from "@/model/Document"; | ||
| import { uploadToCloudinary, isCloudinaryConfigured } from "@/lib/cloudinary"; | ||
|
|
||
| // GET /api/cases/[id]/documents - Get all documents for a case | ||
| export async function GET( | ||
| request: NextRequest, | ||
| { params }: { params: Promise<{ id: string }> } | ||
| ) { | ||
| try { | ||
| const session = await getServerSession(authOptions); | ||
| if (!session?.user?._id) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| await dbConnect(); | ||
| const { id } = await params; | ||
|
|
||
| // Verify case exists and belongs to user | ||
| const caseData = await CaseModel.findOne({ | ||
| _id: id, | ||
| userId: session.user._id | ||
| }); | ||
|
|
||
| if (!caseData) { | ||
| return NextResponse.json({ error: "Case not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| // Get documents for this case | ||
| const documents = await DocumentModel.find({ | ||
| caseId: id, | ||
| userId: session.user._id | ||
| }).sort({ uploadDate: -1 }); | ||
|
|
||
| return NextResponse.json({ documents }); | ||
| } catch (error) { | ||
| console.error("Error fetching documents:", error); | ||
| return NextResponse.json( | ||
| { error: "Failed to fetch documents" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // POST /api/cases/[id]/documents - Upload a new document | ||
| export async function POST( | ||
| request: NextRequest, | ||
| { params }: { params: Promise<{ id: string }> } | ||
| ) { | ||
| try { | ||
| const session = await getServerSession(authOptions); | ||
| if (!session?.user?._id) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| await dbConnect(); | ||
| const { id } = await params; | ||
|
|
||
| // Verify case exists and belongs to user | ||
| const caseData = await CaseModel.findOne({ | ||
| _id: id, | ||
| userId: session.user._id | ||
| }); | ||
|
|
||
| if (!caseData) { | ||
| return NextResponse.json({ error: "Case not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| const formData = await request.formData(); | ||
| const file = formData.get("file") as File; | ||
| const documentType = formData.get("documentType") as string; | ||
| const description = formData.get("description") as string; | ||
| const tags = formData.get("tags") as string; | ||
|
|
||
| if (!file) { | ||
| return NextResponse.json({ error: "No file provided" }, { status: 400 }); | ||
| } | ||
|
|
||
| // Validate file size (max 10MB) | ||
| const maxSize = 10 * 1024 * 1024; // 10MB | ||
| if (file.size > maxSize) { | ||
| return NextResponse.json( | ||
| { error: "File size exceeds 10MB limit" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Validate file type | ||
| const allowedTypes = [ | ||
| "application/pdf", | ||
| "application/msword", | ||
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document", | ||
| "text/plain", | ||
| "image/jpeg", | ||
| "image/png", | ||
| "image/gif" | ||
| ]; | ||
|
|
||
| if (!allowedTypes.includes(file.type)) { | ||
| return NextResponse.json( | ||
| { error: "File type not allowed" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Generate unique filename | ||
| const timestamp = Date.now(); | ||
| const fileExtension = file.name.split('.').pop(); | ||
| const fileName = `${id}_${timestamp}.${fileExtension}`; | ||
|
|
||
| // Check if Cloudinary is configured | ||
| if (!isCloudinaryConfigured()) { | ||
| return NextResponse.json( | ||
| { error: "Cloudinary is not configured. Please set up CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET environment variables." }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
|
|
||
| // Convert file to buffer | ||
| const bytes = await file.arrayBuffer(); | ||
| const buffer = Buffer.from(bytes); | ||
|
|
||
| // Upload to Cloudinary | ||
| const cloudinaryResult = await uploadToCloudinary( | ||
| buffer, | ||
| fileName, | ||
| `legal-ease/cases/${id}/documents` | ||
| ); | ||
|
|
||
| // Create document record | ||
| const document = new DocumentModel({ | ||
| caseId: id, | ||
| fileName: fileName, | ||
| originalName: file.name, | ||
| fileSize: file.size, | ||
| mimeType: file.type, | ||
| cloudinaryUrl: cloudinaryResult.url, | ||
| cloudinaryPublicId: cloudinaryResult.public_id, | ||
| uploadedBy: session.user._id, | ||
| documentType: documentType || "other", | ||
| description: description || "", | ||
| tags: tags ? tags.split(',').map(tag => tag.trim()) : [], | ||
| userId: session.user._id | ||
| }); | ||
|
|
||
| await document.save(); | ||
|
|
||
| return NextResponse.json({ document }, { status: 201 }); | ||
| } catch (error) { | ||
| console.error("Error uploading document:", error); | ||
| return NextResponse.json( | ||
| { error: "Failed to upload document" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Search repo for other Promise-typed params.
There may be more instances with the same typing issue.
🏁 Script executed:
#!/bin/bash
rg -nP "params\W*:\W*Promise<\s*{\s*id\s*:\s*string\s*}\s*>" -C2Length of output: 1271
Correct params typing in Next.js route handlers
Update GET/POST signatures in app/api/cases/[id]/documents/route.ts and app/api/cases/[id]/timeline/route.ts to use
{ params }: { params: { id: string } }instead of
{ params }: { params: Promise<{ id: string }> }🤖 Prompt for AI Agents
In app/api/cases/[id]/documents/route.ts lines 1-160, the GET and POST handler
signatures incorrectly type the params as a Promise; change both signatures to
use { params }: { params: { id: string } } (not a Promise), and update the
handler bodies to stop awaiting params—destructure id directly from params
(const { id } = params) and remove any await before params; keep all other logic
the same.
| export async function updateTimelineEvent( | ||
| caseId: string, | ||
| eventId: string, | ||
| eventData: { | ||
| title: string; | ||
| description: string; | ||
| eventDate: Date; | ||
| eventType: "filing" | "hearing" | "evidence" | "document" | "status_change" | "custom"; | ||
| status?: "completed" | "scheduled" | "pending"; | ||
| metadata?: { | ||
| stage?: string; | ||
| status?: string; | ||
| progress?: number; | ||
| documentUrl?: string; | ||
| notes?: string; | ||
| }; | ||
| } | ||
| ): Promise<TimelineEvent> { | ||
| try { | ||
| const response = await fetch(`${API_BASE}/api/cases/${caseId}/timeline/${eventId}`, { | ||
| method: "PUT", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| credentials: "include", | ||
| body: JSON.stringify(eventData), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Failed to update timeline event: ${response.statusText}`); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| return data.timelineEvent; | ||
| } catch (error) { | ||
| console.error("Error updating timeline event:", error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Mirror create(): serialize date in request and revive dates in response; improve error text.
try {
- const response = await fetch(`${API_BASE}/api/cases/${caseId}/timeline/${eventId}`, {
+ const response = await fetch(
+ api(`/api/cases/${encodeURIComponent(caseId)}/timeline/${encodeURIComponent(eventId)}`),
+ {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
- body: JSON.stringify(eventData),
+ body: JSON.stringify({
+ ...eventData,
+ eventDate: eventData.eventDate instanceof Date ? eventData.eventDate.toISOString() : eventData.eventDate
+ }),
});
- if (!response.ok) {
- throw new Error(`Failed to update timeline event: ${response.statusText}`);
- }
+ if (!response.ok) {
+ let message = `Failed to update timeline event (${response.status})`;
+ try {
+ const err = await response.json();
+ message = err?.error || err?.message || message;
+ } catch {}
+ throw new Error(message);
+ }
- const data = await response.json();
- return data.timelineEvent;
+ const data = await response.json();
+ return reviveTimelineEventDates(data.timelineEvent);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function updateTimelineEvent( | |
| caseId: string, | |
| eventId: string, | |
| eventData: { | |
| title: string; | |
| description: string; | |
| eventDate: Date; | |
| eventType: "filing" | "hearing" | "evidence" | "document" | "status_change" | "custom"; | |
| status?: "completed" | "scheduled" | "pending"; | |
| metadata?: { | |
| stage?: string; | |
| status?: string; | |
| progress?: number; | |
| documentUrl?: string; | |
| notes?: string; | |
| }; | |
| } | |
| ): Promise<TimelineEvent> { | |
| try { | |
| const response = await fetch(`${API_BASE}/api/cases/${caseId}/timeline/${eventId}`, { | |
| method: "PUT", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| credentials: "include", | |
| body: JSON.stringify(eventData), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Failed to update timeline event: ${response.statusText}`); | |
| } | |
| const data = await response.json(); | |
| return data.timelineEvent; | |
| } catch (error) { | |
| console.error("Error updating timeline event:", error); | |
| throw error; | |
| } | |
| } | |
| export async function updateTimelineEvent( | |
| caseId: string, | |
| eventId: string, | |
| eventData: { | |
| title: string; | |
| description: string; | |
| eventDate: Date; | |
| eventType: "filing" | "hearing" | "evidence" | "document" | "status_change" | "custom"; | |
| status?: "completed" | "scheduled" | "pending"; | |
| metadata?: { | |
| stage?: string; | |
| status?: string; | |
| progress?: number; | |
| documentUrl?: string; | |
| notes?: string; | |
| }; | |
| } | |
| ): Promise<TimelineEvent> { | |
| try { | |
| const response = await fetch( | |
| api(`/api/cases/${encodeURIComponent(caseId)}/timeline/${encodeURIComponent(eventId)}`), | |
| { | |
| method: "PUT", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| credentials: "include", | |
| body: JSON.stringify({ | |
| ...eventData, | |
| eventDate: | |
| eventData.eventDate instanceof Date | |
| ? eventData.eventDate.toISOString() | |
| : eventData.eventDate, | |
| }), | |
| } | |
| ); | |
| if (!response.ok) { | |
| let message = `Failed to update timeline event (${response.status})`; | |
| try { | |
| const err = await response.json(); | |
| message = err?.error || err?.message || message; | |
| } catch {} | |
| throw new Error(message); | |
| } | |
| const data = await response.json(); | |
| return reviveTimelineEventDates(data.timelineEvent); | |
| } catch (error) { | |
| console.error("Error updating timeline event:", error); | |
| throw error; | |
| } | |
| } |
🤖 Prompt for AI Agents
In lib/timeline.ts around lines 92 to 130, the updateTimelineEvent function
needs to serialize the eventDate before sending and revive date strings in the
returned timelineEvent, and provide more informative error messages; modify the
request body to convert eventData.eventDate to an ISO string (e.g.
eventData.eventDate.toISOString()) before JSON.stringify, and after parsing the
response JSON convert any date fields (e.g. timelineEvent.eventDate and any
nested date strings) back into JavaScript Date objects before returning; also
improve the thrown Error and console.error to include HTTP status code and
response text/body when response.ok is false to aid debugging.
| @@ -0,0 +1,112 @@ | |||
| import mongoose, { Schema, Document } from "mongoose"; | |||
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Use ObjectId refs for relational fields (populate + integrity).
Ref fields are typed as strings. Switch to Schema.Types.ObjectId to enable populate, better indexing, and consistency with Mongoose defaults.
Apply:
-import mongoose, { Schema, Document } from "mongoose";
+import mongoose, { Schema, Document, Types } from "mongoose";
export interface CaseDocument extends Document {
- caseId: string; // Reference to the case
+ caseId: Types.ObjectId; // Reference to the case
@@
- uploadedBy: string; // User ID who uploaded the file
+ uploadedBy: Types.ObjectId; // User ID who uploaded the file
@@
- userId: string; // Reference to the user who owns this case
+ userId: Types.ObjectId; // Reference to the user who owns this case
@@
caseId: {
- type: String,
+ type: Schema.Types.ObjectId,
required: [true, "Case ID is required"],
ref: "Case"
},
@@
uploadedBy: {
- type: String,
+ type: Schema.Types.ObjectId,
required: [true, "Uploader ID is required"],
ref: "User"
},
@@
userId: {
- type: String,
+ type: Schema.Types.ObjectId,
required: [true, "User ID is required"],
ref: "User"
}Note: if other models truly store string IDs, confirm before merging and we can adjust accordingly.
Also applies to: 3-22, 25-29, 60-64, 93-97
🏁 Script executed:
#!/usr/bin/env bash
set -e
# Locate model/Document.ts
FILE=$(find . -type f -path "*/model/Document.ts" | head -n1)
if [[ -z "$FILE" ]]; then
echo "model/Document.ts not found"
exit 1
fi
echo "Inspecting $FILE"
echo "--- Interface CaseDocument ---"
sed -n '1,200p' "$FILE" | sed -n '/export interface CaseDocument/,/}/p'
echo ""
echo "--- Schema field definitions for caseId, uploadedBy, userId ---"
grep -n -C2 "caseId:" "$FILE"
grep -n -C2 "uploadedBy:" "$FILE"
grep -n -C2 "userId:" "$FILE"Length of output: 2536
Use ObjectId refs for relational fields (populate + integrity).
Ref fields in model/Document.ts are currently typed as string; update them to Types.ObjectId in the interface and Schema.Types.ObjectId in the schema for proper Mongoose refs. Import Types from mongoose.
Apply:
-import mongoose, { Schema, Document } from "mongoose";
+import mongoose, { Schema, Document, Types } from "mongoose";
export interface CaseDocument extends Document {
- caseId: string; // Reference to the case
+ caseId: Types.ObjectId; // Reference to the case
…
- uploadedBy: string; // User ID who uploaded the file
+ uploadedBy: Types.ObjectId; // User ID who uploaded the file
…
- userId: string; // Reference to the user who owns this case
+ userId: Types.ObjectId; // Reference to the user who owns this case
}
const DocumentSchema: Schema<CaseDocument> = new mongoose.Schema({
caseId: {
- type: String,
+ type: Schema.Types.ObjectId,
required: [true, "Case ID is required"],
ref: "Case"
},
…
uploadedBy: {
- type: String,
+ type: Schema.Types.ObjectId,
required: [true, "Uploader ID is required"],
ref: "User"
},
…
userId: {
- type: String,
+ type: Schema.Types.ObjectId,
required: [true, "User ID is required"],
ref: "User"
}
…
});🤖 Prompt for AI Agents
In model/Document.ts around line 1, change relational fields currently typed as
string to use mongoose ObjectId: import Types from mongoose (e.g., import
mongoose, { Schema, Document, Types } from "mongoose"); update the TypeScript
interface field types to Types.ObjectId (or Types.ObjectId | string if you need
flexibility), and in the Mongoose schema change those fields to use type:
Schema.Types.ObjectId with the appropriate ref string (and any required/nullable
settings preserved). Ensure all imports are updated and any code using these
fields is adjusted to accept ObjectId where necessary.
| type: Number, | ||
| required: [true, "File size is required"], | ||
| min: 0 | ||
| }, |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Reject zero-length files.
Minimum file size should be 1 byte to avoid storing empty files.
fileSize: {
type: Number,
required: [true, "File size is required"],
- min: 0
+ min: 1
},🤖 Prompt for AI Agents
In model/Document.ts around lines 41 to 44, the schema currently allows
zero-length files by setting min: 0; change the min validator to 1 to reject
empty files (min: 1) and update the required/validation message to reflect "File
size must be at least 1 byte" or similar; ensure the schema's validation message
surfaces when size < 1 so empty files are rejected at model validation.
| cloudinaryPublicId: { | ||
| type: String, | ||
| required: [true, "Cloudinary public ID is required"], | ||
| trim: true | ||
| }, |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Guarantee uniqueness of Cloudinary assets.
Make cloudinaryPublicId unique to prevent duplicate DB rows pointing to the same asset and simplify idempotent deletes.
cloudinaryPublicId: {
type: String,
required: [true, "Cloudinary public ID is required"],
- trim: true
+ trim: true,
+ unique: true
},
@@
DocumentSchema.index({ caseId: 1, uploadDate: -1 });
DocumentSchema.index({ userId: 1, uploadDate: -1 });
DocumentSchema.index({ caseId: 1, documentType: 1 });
DocumentSchema.index({ fileName: "text", originalName: "text", description: "text" }); // Text search
+DocumentSchema.index({ cloudinaryPublicId: 1 }, { unique: true });Also applies to: 102-107
| "pdfjs-dist": "^2.16.105", | ||
| "react": "^18.3.1", | ||
| "react-day-picker": "8.10.1", | ||
| "react-day-picker": "9.9.0", |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Verify compatibility with the major version upgrade of react-day-picker.
The upgrade from react-day-picker: 8.10.1 to 9.9.0 is a major version jump that may include breaking changes.
🌐 Web query:
What are the breaking changes between react-day-picker version 8 and version 9?
💡 Result:
Here are the main breaking changes when upgrading from react-day-picker v8 → v9:
- Accessibility: updated ARIA labels (may need new translations / test selectors). [1][2]
- Styles / CSS: default CSS and class names changed (many className keys renamed or removed), so custom styles will likely break. See class-name mapping in the upgrade guide. [2]
- Components API: several built-in components renamed/removed (e.g., Caption → MonthCaption, Day now renders a DayButton, Head removed, HeadRow → Weekdays, Row → Week, IconLeft/IconRight removed in favor of Chevron). Custom components must be updated. [2]
- Events / hooks removed: useInput hook removed; some onDay* events (e.g., onDayTouchStart, onDayDoubleClick) removed; onWeekNumberClick removed (use custom components). [1][2]
- Selection control: selected prop is now controlled — you must provide onSelect to update selected state. [2]
- Typings: some TypeScript types/prop names renamed or deprecated. [1][2]
- Build / bundling: build switched to modern ESM/CommonJS outputs — may affect custom bundlers. [1]
- Misc: some props renamed (e.g., fromMonth/toMonth → startMonth/endMonth, fromYear/toYear → startMonth/endMonth as examples) and other deprecations; updated ARIA/markup may require test updates. [2]
For full details and a migration checklist, see the v9 changelog and the “Upgrading to v9” guide. [1][2]
Sources:
[1] react-day-picker CHANGELOG (v9 breaking changes).
[2] React DayPicker — Upgrading to v9 (migration guide, class-name/component renames).
Verify and migrate to react-day-picker v9
The v8→v9 upgrade introduces these breaking changes requiring updates:
- CSS class names renamed/removed – update custom styles per the migration guide.
- Component renames: Caption→MonthCaption, Head removed, HeadRow→Weekdays, Row→Week, Day now renders DayButton, IconLeft/IconRight→Chevron.
- Hooks/events removed: useInput, onDayTouchStart, onDayDoubleClick, onWeekNumberClick – replace with custom components or handlers.
- Selection is now controlled: must pass
selectedplus anonSelecthandler. - Prop renames (e.g., fromMonth/toMonth→startMonth/endMonth, fromYear/toYear→startYear/endYear).
- Updated ARIA labels – review translations and test selectors.
- TypeScript typings renamed/deprecated and bundling outputs switched to ESM/CJS – adjust imports and build config.
See the react-day-picker v9 changelog and “Upgrading to v9” migration guide for full details.
🤖 Prompt for AI Agents
package.json lines 73–73: the project upgraded react-day-picker to 9.9.0 but
hasn’t applied the v8→v9 breaking changes; update components, props, hooks, CSS
and types per the migration guide: rename/replace components
(Caption→MonthCaption, Head→remove, HeadRow→Weekdays, Row→Week, Day→DayButton,
IconLeft/IconRight→Chevron), replace removed hooks/events with custom handlers,
convert selection to controlled pattern (pass selected and onSelect), rename
month/year props (fromMonth/toMonth→startMonth/endMonth,
fromYear/toYear→startYear/endYear), update ARIA labels/translations and any test
selectors, adjust custom CSS class names to the new ones, and fix TypeScript
imports/exports or build config for ESM/CJS; run the react-day-picker v9
migration guide and update all usages and tests accordingly.
…andari04/legalease into feat/Dashboard_Backend
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (11)
app/api/cases/[id]/documents/[docId]/route.ts (1)
19-21: Remove stray await on params (handlers receive plain objects).Access params directly; awaiting a non-Promise can hide type issues.
- const { id, docId } = await params; + const { id, docId } = params;- const { id, docId } = await params; + const { id, docId } = params;Also applies to: 101-103
app/api/cases/[id]/timeline/route.ts (1)
19-21: Remove await on params; Next.js gives plain objects.Access params.id directly in both handlers.
- const { id } = await params; + const { id } = params;- const { id } = await params; + const { id } = params;Also applies to: 67-69
app/api/cases/[id]/route.ts (2)
152-152: Fix await on params in DELETE.Params aren’t Promises.
- const caseId = (await params).id; + const caseId = params.id;
116-124: Block mass-assignment; whitelist updatable fields and include userId in the update filter.Prevents clients from overwriting protected fields (e.g., userId, _id).
- // Update the case - const updatedCase = await CaseModel.findByIdAndUpdate( - caseId, - { - ...body, - lastUpdated: new Date() - }, - { new: true, runValidators: true } - ); + // Update the case (whitelist fields; keep ownership immutable) + const ALLOWED_FIELDS = [ + "caseNumber","title","court","type","stage","status","progress", + "clientName","filingDate","nextHearing","description","notes" + ] as const; + const $set = Object.fromEntries( + Object.entries(body).filter(([k]) => (ALLOWED_FIELDS as readonly string[]).includes(k)) + ); + ($set as any).lastUpdated = new Date(); + delete ($set as any)._id; + delete ($set as any).userId; + + const updatedCase = await CaseModel.findOneAndUpdate( + { _id: caseId, userId: session.user._id }, + { $set }, + { new: true, runValidators: true } + ); + + if (!updatedCase) { + return NextResponse.json({ error: "Case not found" }, { status: 404 }); + }app/api/cases/[id]/documents/route.ts (4)
20-22: Remove unnecessary await on params (not a Promise).Params are synchronous in App Router. This currently compiles but is incorrect.
- const { id } = await params; + const { id } = params;
60-62: Same issue in POST: don’t await params.- const { id } = await params; + const { id } = params;
1-8: Add mongoose import for ObjectId validation.Needed for safe ID checks below.
import { uploadToCloudinary, isCloudinaryConfigured } from "@/lib/cloudinary"; +import mongoose from "mongoose";
21-27: Validate ObjectId to avoid CastError 500s on malformed IDs.Return 400 early if the path param isn’t a valid Mongo ObjectId.
- // Verify case exists and belongs to user + // Verify case exists and belongs to user + if (!mongoose.Types.ObjectId.isValid(id)) { + return NextResponse.json( + { error: "Invalid case ID format" }, + { status: 400 } + ); + } const caseData = await CaseModel.findOne({ _id: id, userId: session.user._id });Also applies to: 63-67
app/dashboard/page.tsx (3)
34-35: Add AbortController to cancel in-flight loads on re-renders/unmount.Prevents setState on unmounted component and wasted work.
const documentListRef = useRef<DocumentListRef>(null) + const abortControllerRef = useRef<AbortController | null>(null)
44-70: Wire request cancellation into loadCases and handle AbortError.const loadCases = async (filters: CaseFilters = {}) => { if (status !== "authenticated") return - + // cancel previous request + if (abortControllerRef.current) abortControllerRef.current.abort() + const ac = new AbortController() + abortControllerRef.current = ac try { setLoading(true) setError(null) - const response = await fetchCases({ + const response = await fetchCases({ status: filterStatus === "all" ? undefined : filterStatus, search: searchQuery || undefined, sortBy: "lastUpdated", sortOrder, page: pagination.currentPage, limit: pagination.limit, ...filters - }) + }, ac.signal) setCases(response.cases) setPagination(response.pagination) } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load cases") + if ((err as any)?.name === "AbortError") return + setError(err instanceof Error ? err.message : "Failed to load cases") console.error("Error loading cases:", err) } finally { setLoading(false) } }Also add unmount cleanup:
+ useEffect(() => { + return () => abortControllerRef.current?.abort() + }, [])Support change in lib (outside this file):
// lib/cases.ts -export async function fetchCases(filters: CaseFilters = {}): Promise<CasesResponse> { +export async function fetchCases(filters: CaseFilters = {}, signal?: AbortSignal): Promise<CasesResponse> { const params = new URLSearchParams(); // ...build params... - const response = await fetch(`/api/cases?${params.toString()}`, { + const response = await fetch(`/api/cases?${params.toString()}`, { method: "GET", headers: { "Content-Type": "application/json" }, credentials: "include", + signal, });
279-280: Use a stable key derived from MongoDB _id.Avoids key churn and React re-mounts.
- key={caseItem._id || caseItem.id} + key={String(caseItem._id ?? caseItem.id ?? index)}
🧹 Nitpick comments (10)
app/api/cases/[id]/documents/[docId]/route.ts (1)
47-55: Optional: Stream the download instead of buffering to reduce memory use on large files.Consider exposing a streaming variant (ReadableStream) from Cloudinary and piping it to NextResponse for O(1) memory.
app/api/cases/[id]/timeline/route.ts (2)
40-46: Return lean() documents to cut overhead on read-only GET.Avoids creating Mongoose document instances when not needed.
- const timelineEvents = await TimelineEventModel.find({ + const timelineEvents = await TimelineEventModel.find({ caseId: id, userId: session.user._id - }).sort({ eventDate: 1 }); // Sort from oldest to newest (ascending order) + }) + .sort({ eventDate: 1 }) // Sort from oldest to newest (ascending order) + .lean();
135-136: Optional: Default status based on date (future → scheduled, past → completed).Improves UX without client logic.
- status: status || "completed", + status: status || (parsedDate > new Date() ? "scheduled" : "completed"),app/api/cases/[id]/route.ts (2)
180-186: Cascade delete Cloudinary assets for this case (best-effort) before removing docs.Avoids orphaned files for raw/image resources.
- await Promise.all([ - CaseModel.findOneAndDelete({ _id: caseId, userId: session.user._id }), - TimelineEventModel.deleteMany({ caseId, userId: session.user._id }), - DocumentModel.deleteMany({ caseId, userId: session.user._id }), - ]); + // Best-effort cloud cleanup (non-transactional) + const docs = await DocumentModel.find({ caseId, userId: session.user._id }) + .select("cloudinaryPublicId mimeType") + .lean(); + await Promise.allSettled( + docs.map(d => + deleteFromCloudinary( + d.cloudinaryPublicId, + d.mimeType?.startsWith("image/") ? "image" : "raw" + ) + ) + ); + + // DB deletes + await Promise.all([ + TimelineEventModel.deleteMany({ caseId, userId: session.user._id }), + DocumentModel.deleteMany({ caseId, userId: session.user._id }), + CaseModel.findOneAndDelete({ _id: caseId, userId: session.user._id }), + ]);Add import at top (outside selected range):
import { deleteFromCloudinary } from "@/lib/cloudinary";Note: Consider wrapping DB deletes in a MongoDB transaction for consistency; cloud cleanup should remain outside the transaction.
2-2: Nit: Align getServerSession import with other routes for consistency.Use "next-auth" everywhere.
-import { getServerSession } from "next-auth/next"; +import { getServerSession } from "next-auth";app/api/cases/[id]/documents/route.ts (1)
110-114: Harden filename generation when extension is missing.If a user uploads a file without an extension, fileName becomes “.undefined”.
- const fileExtension = file.name.split('.').pop(); - const fileName = `${id}_${timestamp}.${fileExtension}`; + let ext = file.name.includes('.') ? file.name.split('.').pop()!.toLowerCase() : ''; + if (!ext) { + const map: Record<string, string> = { + "application/pdf": "pdf", + "application/msword": "doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", + "text/plain": "txt", + "image/jpeg": "jpg", + "image/png": "png", + "image/gif": "gif", + }; + ext = map[file.type] || "bin"; + } + const fileName = `${id}_${timestamp}.${ext}`;app/dashboard/page.tsx (4)
395-396: Pass a string _id to delete handler (type-safety with ObjectId).If
_idis an ObjectId, ensure a string is passed.- onClick={() => handleDeleteCase(selectedCase._id)} + onClick={() => handleDeleteCase(String(selectedCase._id))}If the Case model lacks
_idtyping, consider addingreadonly _id: Types.ObjectIdtomodel/Case.ts, or adjustdeleteCaseto acceptTypes.ObjectId | string.
538-539: Ensure DocumentList receives a string caseId.Same reason as above.
- <DocumentList ref={documentListRef} caseId={selectedCase._id} /> + <DocumentList ref={documentListRef} caseId={String(selectedCase._id)} />
136-141: Keep pagination in sync after deletion.Manually decrementing totalCount can drift; simplest is to refresh.
- // Update pagination if needed - setPagination(prev => ({ - ...prev, - totalCount: prev.totalCount - 1 - })) + // Refresh to sync pagination (page/hasNext/hasPrev) + await loadCases()
19-21: Add explicit_idproperty to Case interface
Add_id: stringto theexport interface Case extends Documentinmodel/Case.tsto avoid downstreamString(case._id)casts.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
app/api/cases/[id]/documents/[docId]/route.ts(1 hunks)app/api/cases/[id]/documents/route.ts(1 hunks)app/api/cases/[id]/route.ts(1 hunks)app/api/cases/[id]/timeline/route.ts(1 hunks)app/dashboard/page.tsx(8 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
app/api/cases/[id]/route.ts (4)
app/api/cases/[id]/documents/route.ts (1)
GET(10-47)app/api/cases/[id]/timeline/route.ts (1)
GET(9-54)app/api/cases/route.ts (1)
GET(10-95)app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)
app/api/cases/[id]/documents/route.ts (3)
app/api/cases/[id]/documents/[docId]/route.ts (1)
GET(9-88)app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)lib/cloudinary.ts (2)
isCloudinaryConfigured(11-17)uploadToCloudinary(22-71)
app/api/cases/[id]/documents/[docId]/route.ts (3)
app/api/cases/[id]/documents/route.ts (1)
GET(10-47)app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)lib/cloudinary.ts (3)
isCloudinaryConfigured(11-17)getCloudinaryFileBuffer(114-169)deleteFromCloudinary(74-84)
app/api/cases/[id]/timeline/route.ts (5)
app/api/cases/[id]/documents/[docId]/route.ts (1)
GET(9-88)app/api/cases/[id]/documents/route.ts (2)
GET(10-47)POST(50-160)app/api/cases/[id]/route.ts (1)
GET(10-57)app/api/cases/route.ts (2)
GET(10-95)POST(98-180)app/api/auth/[...nextauth]/options.ts (2)
session(91-97)authOptions(16-112)
app/dashboard/page.tsx (6)
app/api/auth/[...nextauth]/options.ts (1)
session(91-97)model/Case.ts (1)
Case(3-23)components/document-list.tsx (2)
DocumentListRef(15-17)DocumentList(19-306)lib/cases.ts (3)
CaseFilters(15-23)fetchCases(26-51)deleteCase(110-123)components/case-form.tsx (1)
CaseForm(48-557)components/document-upload.tsx (1)
DocumentUpload(32-382)
🔇 Additional comments (3)
app/api/cases/[id]/documents/[docId]/route.ts (1)
1-75: No action needed: default Node.js runtime applies, soBufferis available. (nextjs.org)app/api/cases/[id]/timeline/route.ts (1)
108-116: Valid eventType values align with model enum; no changes needed.app/api/cases/[id]/documents/route.ts (1)
33-38:uploadDateexists and is indexed – no changes needed. TheDocumentschema defines anuploadDatefield and declares indexes on{ caseId: 1, uploadDate: -1 }and{ userId: 1, uploadDate: -1 }, so sorting byuploadDateis valid.Likely an incorrect or invalid review comment.
| const { id, docId } = await params; | ||
|
|
||
| // Find document and verify ownership | ||
| const document = await DocumentModel.findOne({ | ||
| _id: docId, | ||
| caseId: id, | ||
| userId: session.user._id | ||
| }); | ||
|
|
||
| if (!document) { | ||
| return NextResponse.json({ error: "Document not found" }, { status: 404 }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Validate ObjectId format for id and docId to avoid CastError 500s.
Return 400 early on invalid IDs.
- const { id, docId } = params;
+ const { id, docId } = params;
+ if (!/^[0-9a-fA-F]{24}$/.test(id) || !/^[0-9a-fA-F]{24}$/.test(docId)) {
+ return NextResponse.json({ error: "Invalid ID format" }, { status: 400 });
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In app/api/cases/[id]/documents/[docId]/route.ts around lines 20 to 31, the
handler directly uses id and docId in a Mongoose query which can throw a
CastError for invalid ObjectId strings; add early validation for both id and
docId using mongoose.Types.ObjectId.isValid (or equivalent) and if either is
invalid return NextResponse.json({ error: "Invalid id or docId" }, { status: 400
}) before querying the database so malformed IDs never reach Mongoose.
| // Update download count and last accessed | ||
| await DocumentModel.findByIdAndUpdate(docId, { | ||
| $inc: { downloadCount: 1 }, | ||
| lastAccessed: new Date() | ||
| }); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Count downloads only on successful fetch; harden Content-Disposition with ASCII fallback + RFC 5987.
- Move downloadCount/lastAccessed update to after successful Cloudinary fetch.
- Sanitize filename, add filename* for UTF‑8, and default Content-Type.
- // Update download count and last accessed
- await DocumentModel.findByIdAndUpdate(docId, {
- $inc: { downloadCount: 1 },
- lastAccessed: new Date()
- });- // Return file with appropriate headers
- // Properly encode filename for HTTP headers to handle Unicode characters
- const encodedFilename = encodeURIComponent(document.originalName);
- const contentDisposition = `attachment; filename*=UTF-8''${encodedFilename}`;
-
- return new NextResponse(fileBuffer, {
+ // Update metrics after successful fetch
+ await DocumentModel.findByIdAndUpdate(docId, {
+ $inc: { downloadCount: 1 },
+ lastAccessed: new Date()
+ });
+
+ // Return file with safe, standards-compliant headers
+ const safeName = (document.originalName || "download").replace(/[\r\n"]/g, "_");
+ const encoded = encodeURIComponent(safeName);
+ const contentDisposition = `attachment; filename="${safeName}"; filename*=UTF-8''${encoded}`;
+
+ return new NextResponse(fileBuffer, {
headers: {
- "Content-Type": document.mimeType,
+ "Content-Type": document.mimeType || "application/octet-stream",
"Content-Disposition": contentDisposition,
"Content-Length": fileBuffer.length.toString(),
"Cache-Control": "no-cache",
},
});Also applies to: 62-74
🤖 Prompt for AI Agents
In app/api/cases/[id]/documents/[docId]/route.ts around lines 41-46 (and
similarly 62-74), the code updates downloadCount and lastAccessed before
ensuring the Cloudinary fetch succeeded and sets Content-Disposition without a
sanitized ASCII fallback or RFC 5987 UTF-8 fallback and may not set a default
Content-Type; change the flow so the DocumentModel.findByIdAndUpdate(...) call
is executed only after a successful response/stream from Cloudinary, sanitize
the filename to remove/control unsafe characters for the ASCII filename
parameter, add a filename* parameter with RFC 5987 percent-encoded UTF-8 value,
set Content-Type to the response.contentType or a safe default like
application/octet-stream, and ensure the Content-Disposition header includes
both the sanitized filename and the filename* UTF-8 fallback.
| // Delete file from Cloudinary | ||
| try { | ||
| await deleteFromCloudinary(document.cloudinaryPublicId); | ||
| } catch (error) { | ||
| console.error("Error deleting file from Cloudinary:", error); | ||
| // Continue with database deletion even if Cloudinary deletion fails | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Ensure Cloudinary deletion works for non-images by passing resource_type.
Raw assets (e.g., PDFs, DOCX) won’t delete with default image resource_type. Detect from mimeType and pass through.
- try {
- await deleteFromCloudinary(document.cloudinaryPublicId);
+ try {
+ const resourceType = document.mimeType?.startsWith('image/') ? 'image' : 'raw';
+ await deleteFromCloudinary(document.cloudinaryPublicId, resourceType);Additionally update lib/cloudinary.ts:
-export async function deleteFromCloudinary(publicId: string): Promise<void> {
+export async function deleteFromCloudinary(publicId: string, resourceType: 'image' | 'raw' = 'image'): Promise<void> {
return new Promise((resolve, reject) => {
- cloudinary.uploader.destroy(publicId, (error, result) => {
+ cloudinary.uploader.destroy(publicId, { resource_type: resourceType }, (error, result) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Delete file from Cloudinary | |
| try { | |
| await deleteFromCloudinary(document.cloudinaryPublicId); | |
| } catch (error) { | |
| console.error("Error deleting file from Cloudinary:", error); | |
| // Continue with database deletion even if Cloudinary deletion fails | |
| } | |
| // File: app/api/cases/[id]/documents/[docId]/route.ts | |
| // Delete file from Cloudinary | |
| try { | |
| const resourceType = document.mimeType?.startsWith('image/') ? 'image' : 'raw'; | |
| await deleteFromCloudinary(document.cloudinaryPublicId, resourceType); | |
| } catch (error) { | |
| console.error("Error deleting file from Cloudinary:", error); | |
| // Continue with database deletion even if Cloudinary deletion fails | |
| } |
| // Delete file from Cloudinary | |
| try { | |
| await deleteFromCloudinary(document.cloudinaryPublicId); | |
| } catch (error) { | |
| console.error("Error deleting file from Cloudinary:", error); | |
| // Continue with database deletion even if Cloudinary deletion fails | |
| } | |
| // File: lib/cloudinary.ts | |
| export async function deleteFromCloudinary( | |
| publicId: string, | |
| resourceType: 'image' | 'raw' = 'image' | |
| ): Promise<void> { | |
| return new Promise((resolve, reject) => { | |
| cloudinary.uploader.destroy( | |
| publicId, | |
| { resource_type: resourceType }, | |
| (error, result) => { | |
| if (error) { | |
| reject(error); | |
| } else { | |
| resolve(); | |
| } | |
| } | |
| ); | |
| }); | |
| } |
🤖 Prompt for AI Agents
In app/api/cases/[id]/documents/[docId]/route.ts around lines 115 to 121, the
Cloudinary deletion call always uses the default image resource_type so
non-image raw assets (PDF, DOCX, etc.) fail to delete; change the code to derive
resource_type from document.mimeType (e.g., if mimeType starts with "image/" use
"image", otherwise use "raw") and pass that resource_type into
deleteFromCloudinary; also update lib/cloudinary.ts to accept an optional
resource_type parameter and forward it to the Cloudinary destroy/delete API so
deletions for raw files succeed.
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { getServerSession } from "next-auth/next"; | ||
| import { authOptions } from "@/app/api/auth/[...nextauth]/options"; | ||
| import dbConnect from "@/lib/dbConnect"; | ||
| import CaseModel from "@/model/Case"; | ||
| import TimelineEventModel from "@/model/TimelineEvent"; | ||
| import DocumentModel from "@/model/Document"; | ||
|
|
||
| // GET /api/cases/[id] - Fetch a specific case by ID | ||
| export async function GET( | ||
| request: NextRequest, | ||
| { params }: { params: { id: string } } | ||
| ) { | ||
| try { | ||
| const session = await getServerSession(authOptions); | ||
|
|
||
| if (!session?.user?._id) { | ||
| return NextResponse.json( | ||
| { error: "Unauthorized" }, | ||
| { status: 401 } | ||
| ); | ||
| } | ||
|
|
||
| await dbConnect(); | ||
|
|
||
| const caseId = params.id; | ||
|
|
||
| // Validate ObjectId format | ||
| if (!caseId.match(/^[0-9a-fA-F]{24}$/)) { | ||
| return NextResponse.json( | ||
| { error: "Invalid case ID format" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| const caseData = await CaseModel.findOne({ | ||
| _id: caseId, | ||
| userId: session.user._id | ||
| }).lean(); | ||
|
|
||
| if (!caseData) { | ||
| return NextResponse.json( | ||
| { error: "Case not found" }, | ||
| { status: 404 } | ||
| ); | ||
| } | ||
|
|
||
| return NextResponse.json(caseData); | ||
|
|
||
| } catch (error) { | ||
| console.error("Error fetching case:", error); | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // PUT /api/cases/[id] - Update a specific case | ||
| export async function PUT( | ||
| request: NextRequest, | ||
| { params }: { params: { id: string } } | ||
| ) { | ||
| try { | ||
| const session = await getServerSession(authOptions); | ||
|
|
||
| if (!session?.user?._id) { | ||
| return NextResponse.json( | ||
| { error: "Unauthorized" }, | ||
| { status: 401 } | ||
| ); | ||
| } | ||
|
|
||
| const caseId = params.id; | ||
| const body = await request.json(); | ||
|
|
||
| await dbConnect(); | ||
|
|
||
| // Validate ObjectId format | ||
| if (!caseId.match(/^[0-9a-fA-F]{24}$/)) { | ||
| return NextResponse.json( | ||
| { error: "Invalid case ID format" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Check if case exists and belongs to user | ||
| const existingCase = await CaseModel.findOne({ | ||
| _id: caseId, | ||
| userId: session.user._id | ||
| }); | ||
|
|
||
| if (!existingCase) { | ||
| return NextResponse.json( | ||
| { error: "Case not found" }, | ||
| { status: 404 } | ||
| ); | ||
| } | ||
|
|
||
| // If case number is being updated, check for duplicates within the same user | ||
| if (body.caseNumber && body.caseNumber !== existingCase.caseNumber) { | ||
| const duplicateCase = await CaseModel.findOne({ | ||
| caseNumber: body.caseNumber, | ||
| userId: session.user._id, | ||
| _id: { $ne: caseId } | ||
| }); | ||
|
|
||
| if (duplicateCase) { | ||
| return NextResponse.json( | ||
| { error: "Case number already exists" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // Update the case | ||
| const updatedCase = await CaseModel.findByIdAndUpdate( | ||
| caseId, | ||
| { | ||
| ...body, | ||
| lastUpdated: new Date() | ||
| }, | ||
| { new: true, runValidators: true } | ||
| ); | ||
|
|
||
| return NextResponse.json(updatedCase); | ||
|
|
||
| } catch (error) { | ||
| console.error("Error updating case:", error); | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // DELETE /api/cases/[id] - Delete a specific case | ||
| export async function DELETE( | ||
| request: NextRequest, | ||
| { params }: { params: { id: string } } | ||
| ) { | ||
| try { | ||
| const session = await getServerSession(authOptions); | ||
|
|
||
| if (!session?.user?._id) { | ||
| return NextResponse.json( | ||
| { error: "Unauthorized" }, | ||
| { status: 401 } | ||
| ); | ||
| } | ||
|
|
||
| const caseId = (await params).id; | ||
|
|
||
| await dbConnect(); | ||
|
|
||
| // Validate ObjectId format | ||
| if (!caseId.match(/^[0-9a-fA-F]{24}$/)) { | ||
| return NextResponse.json( | ||
| { error: "Invalid case ID format" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Check if case exists and belongs to user | ||
| const existingCase = await CaseModel.findOne({ | ||
| _id: caseId, | ||
| userId: session.user._id | ||
| }); | ||
|
|
||
| if (!existingCase) { | ||
| return NextResponse.json( | ||
| { error: "Case not found" }, | ||
| { status: 404 } | ||
| ); | ||
| } | ||
|
|
||
| // Delete the case | ||
| // await CaseModel.findByIdAndDelete(caseId); | ||
|
|
||
| // Delete the case and related data | ||
| await Promise.all([ | ||
| CaseModel.findOneAndDelete({ _id: caseId, userId: session.user._id }), | ||
| TimelineEventModel.deleteMany({ caseId, userId: session.user._id }), | ||
| DocumentModel.deleteMany({ caseId, userId: session.user._id }), | ||
| ]); | ||
|
|
||
| return NextResponse.json( | ||
| { message: "Case deleted successfully" }, | ||
| { status: 200 } | ||
| ); | ||
|
|
||
| } catch (error) { | ||
| console.error("Error deleting case:", error); | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Repo-wide check: remove any remaining await params patterns.
Automate detection to fix similar issues across routes.
🏁 Script executed:
#!/bin/bash
# Find 'await params' in route handlers
rg -n -C2 -S '\bawait\s+params\b' app || trueLength of output: 2155
Remove all await params usages in route handlers
- app/api/cases/[id]/timeline/route.ts (lines 20 & 68)
- app/api/cases/[id]/documents/[docId]/route.ts (lines 20 & 102)
- app/api/cases/[id]/documents/route.ts (lines 21 & 61)
- app/api/cases/[id]/route.ts (line 152)
Destructure params from the handler signature instead of awaiting it. Consider adding a lint rule or codemod to catch any remaining occurrences repo-wide.
🤖 Prompt for AI Agents
app/api/cases/[id]/route.ts lines 1-199 — The DELETE handler incorrectly awaits
params (const caseId = (await params).id); remove the await and read the id
directly from the destructured params (const caseId = params.id) — ensure the
handler signature destructures params (it already does in other handlers) and
replace any other occurrences of awaiting params in this file with direct
access; consider adding a lint rule/codemod to catch remaining usages repo-wide.
�� Complete Dashboard Backend Implementation
This PR represents a major milestone - the complete transformation of the LegalEase dashboard from a static template with mock data to a fully functional, production-ready system with comprehensive backend infrastructure.
🎯 What This PR Accomplishes
BEFORE: Dashboard was a static template with hardcoded mock data
AFTER: Fully functional dashboard with complete backend infrastructure, database integration, and real-time data management
🏗️ Backend Infrastructure (NEW)
Database Models & Schemas
Created comprehensive MongoDB schemas with proper indexing and validation:
Case Model (
model/Case.ts)TimelineEvent Model (
model/TimelineEvent.ts)Document Model (
model/Document.ts)API Endpoints (COMPLETE REST API)
Cases API (
/api/cases/)/api/cases- Fetch all cases with advanced filtering, sorting, and pagination/api/cases- Create new cases with automatic timeline generation/api/cases/[id]- Get individual case details/api/cases/[id]- Update case information/api/cases/[id]- Delete cases with proper cleanupTimeline API (
/api/cases/[id]/timeline/)/api/cases/[id]/timeline- Fetch timeline events for a case/api/cases/[id]/timeline- Create new timeline events/api/cases/[id]/timeline/[eventId]- Update timeline events/api/cases/[id]/timeline/[eventId]- Delete timeline eventsDocuments API (
/api/cases/[id]/documents/)/api/cases/[id]/documents- Fetch all documents for a case/api/cases/[id]/documents- Upload new documents with Cloudinary integration/api/cases/[id]/documents/[docId]- Get individual document details/api/cases/[id]/documents/[docId]- Delete documents�� Frontend Enhancements
Dashboard Transformation
Timeline Management System
Document Management
📊 Technical Specifications
Database Architecture
API Architecture
File Storage
Summary by CodeRabbit
New Features
UI/Style
Documentation
Chores