From 89ac256bf0d4e6c282ddb4662c05dce42bd33edc Mon Sep 17 00:00:00 2001 From: SushantBhandari Date: Mon, 8 Sep 2025 13:07:00 +0530 Subject: [PATCH 1/7] Added backend and frontend for case dashboard and case timeline --- app/api/cases/[id]/route.ts | 189 + app/api/cases/[id]/timeline/route.ts | 95 + app/api/cases/route.ts | 179 + app/dashboard/page.tsx | 291 +- app/globals.css | 33 + components/case-form.tsx | 547 ++ components/case-timeline.tsx | 328 +- components/timeline-form.tsx | 362 ++ components/ui/calendar.tsx | 229 +- docs/timeline-user-guide.md | 145 + lib/cases.ts | 123 + lib/timeline.ts | 192 + model/Case.ts | 116 + model/TimelineEvent.ts | 77 + package.json | 4 +- patches/pdf-parse+1.1.1.patch | 15 + pnpm-lock.yaml | 7489 +++++++++++++++++++++++++- 17 files changed, 10110 insertions(+), 304 deletions(-) create mode 100644 app/api/cases/[id]/route.ts create mode 100644 app/api/cases/[id]/timeline/route.ts create mode 100644 app/api/cases/route.ts create mode 100644 components/case-form.tsx create mode 100644 components/timeline-form.tsx create mode 100644 docs/timeline-user-guide.md create mode 100644 lib/cases.ts create mode 100644 lib/timeline.ts create mode 100644 model/Case.ts create mode 100644 model/TimelineEvent.ts create mode 100644 patches/pdf-parse+1.1.1.patch diff --git a/app/api/cases/[id]/route.ts b/app/api/cases/[id]/route.ts new file mode 100644 index 0000000..338afe9 --- /dev/null +++ b/app/api/cases/[id]/route.ts @@ -0,0 +1,189 @@ +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"; + +// 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 + if (body.caseNumber && body.caseNumber !== existingCase.caseNumber) { + const duplicateCase = await CaseModel.findOne({ + caseNumber: body.caseNumber, + _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 = 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); + + 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 } + ); + } +} diff --git a/app/api/cases/[id]/timeline/route.ts b/app/api/cases/[id]/timeline/route.ts new file mode 100644 index 0000000..b23fc71 --- /dev/null +++ b/app/api/cases/[id]/timeline/route.ts @@ -0,0 +1,95 @@ +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 TimelineEventModel from "@/model/TimelineEvent"; + +// GET /api/cases/[id]/timeline - Get timeline events for a case +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(); + + // Verify case exists and belongs to user + const caseData = await CaseModel.findOne({ + _id: params.id, + userId: session.user._id + }); + + if (!caseData) { + return NextResponse.json({ error: "Case not found" }, { status: 404 }); + } + + // Get timeline events for this case + const timelineEvents = await TimelineEventModel.find({ + caseId: params.id, + userId: session.user._id + }).sort({ eventDate: 1 }); // Sort from oldest to newest (ascending order) + + return NextResponse.json({ timelineEvents }); + } catch (error) { + console.error("Error fetching timeline events:", error); + return NextResponse.json( + { error: "Failed to fetch timeline events" }, + { status: 500 } + ); + } +} + +// POST /api/cases/[id]/timeline - Create a new timeline event +export async function POST( + 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(); + + // Verify case exists and belongs to user + const caseData = await CaseModel.findOne({ + _id: params.id, + userId: session.user._id + }); + + if (!caseData) { + return NextResponse.json({ error: "Case not found" }, { status: 404 }); + } + + const body = await request.json(); + const { title, description, eventDate, eventType, status, metadata } = body; + + // Create new timeline event + const timelineEvent = new TimelineEventModel({ + caseId: params.id, + title, + description, + eventDate: new Date(eventDate), + eventType, + status: status || "completed", + metadata, + userId: session.user._id + }); + + await timelineEvent.save(); + + return NextResponse.json({ timelineEvent }, { status: 201 }); + } catch (error) { + console.error("Error creating timeline event:", error); + return NextResponse.json( + { error: "Failed to create timeline event" }, + { status: 500 } + ); + } +} diff --git a/app/api/cases/route.ts b/app/api/cases/route.ts new file mode 100644 index 0000000..5b3bc6d --- /dev/null +++ b/app/api/cases/route.ts @@ -0,0 +1,179 @@ +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 { generateInitialTimelineEvents } from "@/lib/timeline"; + +// GET /api/cases - Fetch all cases for the authenticated user +export async function GET(request: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?._id) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + await dbConnect(); + + // Get query parameters for filtering and sorting + const { searchParams } = new URL(request.url); + const status = searchParams.get("status"); + const stage = searchParams.get("stage"); + const search = searchParams.get("search"); + const sortBy = searchParams.get("sortBy") || "lastUpdated"; + const sortOrder = searchParams.get("sortOrder") || "desc"; + const page = parseInt(searchParams.get("page") || "1"); + const limit = parseInt(searchParams.get("limit") || "10"); + + // Build filter object + const filter: any = { userId: session.user._id }; + + if (status && status !== "all") { + filter.status = status; + } + + if (stage && stage !== "all") { + filter.stage = stage; + } + + if (search) { + filter.$or = [ + { caseNumber: { $regex: search, $options: "i" } }, + { title: { $regex: search, $options: "i" } }, + { court: { $regex: search, $options: "i" } }, + { type: { $regex: search, $options: "i" } }, + { clientName: { $regex: search, $options: "i" } } + ]; + } + + // Build sort object + const sort: any = {}; + sort[sortBy] = sortOrder === "asc" ? 1 : -1; + + // Calculate skip for pagination + const skip = (page - 1) * limit; + + // Fetch cases with pagination + const cases = await CaseModel.find(filter) + .sort(sort) + .skip(skip) + .limit(limit) + .lean(); + + // Get total count for pagination + const totalCount = await CaseModel.countDocuments(filter); + + // Calculate pagination info + const totalPages = Math.ceil(totalCount / limit); + const hasNextPage = page < totalPages; + const hasPrevPage = page > 1; + + return NextResponse.json({ + cases, + pagination: { + currentPage: page, + totalPages, + totalCount, + hasNextPage, + hasPrevPage, + limit + } + }); + + } catch (error) { + console.error("Error fetching cases:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// POST /api/cases - Create a new case +export async function POST(request: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?._id) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + const body = await request.json(); + + // Validate required fields + const requiredFields = ["caseNumber", "title", "court", "type"]; + for (const field of requiredFields) { + if (!body[field]) { + return NextResponse.json( + { error: `${field} is required` }, + { status: 400 } + ); + } + } + + await dbConnect(); + + // Check if case number already exists + const existingCase = await CaseModel.findOne({ + caseNumber: body.caseNumber + }); + + if (existingCase) { + return NextResponse.json( + { error: "Case number already exists" }, + { status: 400 } + ); + } + + // Create new case + const newCase = new CaseModel({ + ...body, + userId: session.user._id, + lastUpdated: new Date() + }); + + const savedCase = await newCase.save(); + + // Generate initial timeline events + try { + const initialEvents = generateInitialTimelineEvents({ + _id: savedCase._id.toString(), + caseNumber: savedCase.caseNumber, + title: savedCase.title, + filingDate: savedCase.filingDate, + stage: savedCase.stage, + status: savedCase.status, + nextHearing: savedCase.nextHearing + }); + + // Create timeline events + const timelineEvents = initialEvents.map(event => ({ + ...event, + caseId: savedCase._id.toString(), + userId: session.user._id + })); + + await TimelineEventModel.insertMany(timelineEvents); + } catch (timelineError) { + console.error("Error creating initial timeline events:", timelineError); + // Don't fail the case creation if timeline events fail + } + + return NextResponse.json(savedCase, { status: 201 }); + + } catch (error) { + console.error("Error creating case:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 3fae3b8..61e6ad6 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,8 +1,9 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { motion } from "framer-motion" -import { Calendar, Filter, Search, SortAsc, SortDesc } from "lucide-react" +import { Calendar, Filter, Search, SortAsc, SortDesc, Plus, Loader2 } from "lucide-react" +import { useSession } from "next-auth/react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" @@ -10,107 +11,107 @@ import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { CaseTimeline } from "@/components/case-timeline" - -type Case = { - id: string - caseNumber: string - court: string - type: string - stage: "Filed" | "Hearing" | "Evidence" | "Arguments" | "Judgment" | "Closed" - status: "Active" | "Pending" | "Delayed" | "Completed" - progress: number - lastUpdated: string - nextHearing?: string -} +import { CaseForm } from "@/components/case-form" +import { fetchCases, CaseFilters } from "@/lib/cases" +import { Case } from "@/model/Case" export default function DashboardPage() { + const { data: session, status } = useSession() const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc") const [filterStatus, setFilterStatus] = useState("all") const [searchQuery, setSearchQuery] = useState("") const [selectedCase, setSelectedCase] = useState(null) + const [cases, setCases] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [isCaseFormOpen, setIsCaseFormOpen] = useState(false) + const [editingCase, setEditingCase] = useState(null) + const [pagination, setPagination] = useState({ + currentPage: 1, + totalPages: 1, + totalCount: 0, + hasNextPage: false, + hasPrevPage: false, + limit: 10 + }) + + // Fetch cases from API + const loadCases = async (filters: CaseFilters = {}) => { + if (status !== "authenticated") return + + try { + setLoading(true) + setError(null) + + const response = await fetchCases({ + status: filterStatus === "all" ? undefined : filterStatus, + search: searchQuery || undefined, + sortBy: "lastUpdated", + sortOrder, + page: pagination.currentPage, + limit: pagination.limit, + ...filters + }) + + setCases(response.cases) + setPagination(response.pagination) + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load cases") + console.error("Error loading cases:", err) + } finally { + setLoading(false) + } + } + + // Load cases on component mount and when filters change + useEffect(() => { + if (status === "authenticated") { + loadCases() + } + }, [status, filterStatus, searchQuery, sortOrder, pagination.currentPage]) + + // Debounced search + useEffect(() => { + const timeoutId = setTimeout(() => { + if (status === "authenticated") { + setPagination(prev => ({ ...prev, currentPage: 1 })) + loadCases() + } + }, 500) + + return () => clearTimeout(timeoutId) + }, [searchQuery]) + + // Handle filter changes + const handleFilterChange = (newFilter: string) => { + setFilterStatus(newFilter) + setPagination(prev => ({ ...prev, currentPage: 1 })) + } + + const handleSortChange = (newSort: "asc" | "desc") => { + setSortOrder(newSort) + setPagination(prev => ({ ...prev, currentPage: 1 })) + } + + // Handle pagination + const handlePageChange = (newPage: number) => { + setPagination(prev => ({ ...prev, currentPage: newPage })) + } + + // Handle case form + const handleOpenCaseForm = (caseToEdit?: Case) => { + setEditingCase(caseToEdit || null) + setIsCaseFormOpen(true) + } + + const handleCloseCaseForm = () => { + setIsCaseFormOpen(false) + setEditingCase(null) + } - // Mock data for cases - const cases: Case[] = [ - { - id: "1", - caseNumber: "CWP-1234/2023", - court: "Delhi High Court", - type: "Civil Writ Petition", - stage: "Hearing", - status: "Active", - progress: 40, - lastUpdated: "2023-11-15", - nextHearing: "2023-12-10", - }, - { - id: "2", - caseNumber: "CRL-5678/2023", - court: "District Court, Mumbai", - type: "Criminal Appeal", - stage: "Evidence", - status: "Delayed", - progress: 30, - lastUpdated: "2023-10-20", - nextHearing: "2023-12-15", - }, - { - id: "3", - caseNumber: "CS-9101/2022", - court: "Civil Court, Bangalore", - type: "Civil Suit", - stage: "Arguments", - status: "Active", - progress: 70, - lastUpdated: "2023-11-05", - nextHearing: "2023-11-25", - }, - { - id: "4", - caseNumber: "ARB-1122/2023", - court: "Arbitration Tribunal", - type: "Arbitration", - stage: "Filed", - status: "Pending", - progress: 10, - lastUpdated: "2023-11-10", - }, - { - id: "5", - caseNumber: "FAM-3344/2022", - court: "Family Court, Chennai", - type: "Divorce Petition", - stage: "Judgment", - status: "Active", - progress: 90, - lastUpdated: "2023-11-18", - }, - { - id: "6", - caseNumber: "TAX-5566/2021", - court: "Income Tax Appellate Tribunal", - type: "Tax Appeal", - stage: "Closed", - status: "Completed", - progress: 100, - lastUpdated: "2023-09-30", - }, - ] - - // Filter and sort cases - const filteredCases = cases - .filter( - (c) => - (filterStatus === "all" || c.status === filterStatus) && - (searchQuery === "" || - c.caseNumber.toLowerCase().includes(searchQuery.toLowerCase()) || - c.court.toLowerCase().includes(searchQuery.toLowerCase()) || - c.type.toLowerCase().includes(searchQuery.toLowerCase())), - ) - .sort((a, b) => { - const dateA = new Date(a.lastUpdated).getTime() - const dateB = new Date(b.lastUpdated).getTime() - return sortOrder === "asc" ? dateA - dateB : dateB - dateA - }) + const handleCaseFormSuccess = () => { + loadCases() // Refresh the cases list + } // Get status color const getStatusColor = (status: string) => { @@ -157,7 +158,13 @@ export default function DashboardPage() {

Track and manage your legal cases

- + {/* Filters and Search */} @@ -174,7 +181,7 @@ export default function DashboardPage() {
- @@ -194,7 +201,7 @@ export default function DashboardPage() { ) : ( )} - handleSortChange(value as "asc" | "desc")}> @@ -206,12 +213,36 @@ export default function DashboardPage() {
+ {/* Loading State */} + {loading && ( +
+ + Loading cases... +
+ )} + + {/* Error State */} + {error && ( +
+

Error: {error}

+ +
+ )} + {/* Case Cards */} -
- {filteredCases.length > 0 ? ( - filteredCases.map((caseItem, index) => ( + {!loading && !error && ( +
+ {cases.length > 0 ? ( + cases.map((caseItem, index) => ( - Next hearing: {caseItem.nextHearing} + Next hearing: {new Date(caseItem.nextHearing).toLocaleDateString()}
)} -
Last updated: {caseItem.lastUpdated}
+
+ Last updated: {new Date(caseItem.lastUpdated).toLocaleDateString()} +
)) - ) : ( -
-

No cases found matching your filters.

-
- )} - + ) : ( +
+

No cases found. Create your first case to get started!

+
+ )} + + )} + + {/* Pagination */} + {!loading && !error && pagination.totalPages > 1 && ( +
+ + + Page {pagination.currentPage} of {pagination.totalPages} + + +
+ )} {/* Case Timeline View */} {selectedCase && ( @@ -318,7 +377,7 @@ export default function DashboardPage() {

Last Updated

-

{selectedCase.lastUpdated}

+

{new Date(selectedCase.lastUpdated).toLocaleDateString()}

@@ -334,6 +393,14 @@ export default function DashboardPage() { )} + + {/* Case Form Modal */} + ) diff --git a/app/globals.css b/app/globals.css index f2ff4e0..b2e35c5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -87,3 +87,36 @@ body { @apply bg-background text-foreground; } } + +@layer components { + /* Enhanced shadcn date picker styling */ + .rdp { + --rdp-cell-size: 36px; + --rdp-accent-color: #0d9488; + --rdp-background-color: #f8fafc; + --rdp-accent-color-dark: #0f766e; + --rdp-background-color-dark: #1e293b; + --rdp-outline: 2px solid var(--rdp-accent-color); + --rdp-outline-selected: 2px solid var(--rdp-accent-color); + margin: 0; + } + + .rdp-day_selected { + background-color: var(--rdp-accent-color) !important; + color: white !important; + } + + .rdp-day_selected:hover { + background-color: var(--rdp-accent-color-dark) !important; + } + + .rdp-day_today { + background-color: #f1f5f9 !important; + color: #0f172a !important; + font-weight: 600; + } + + .rdp-day:hover:not(.rdp-day_selected):not(.rdp-day_disabled) { + background-color: #f1f5f9 !important; + } +} \ No newline at end of file diff --git a/components/case-form.tsx b/components/case-form.tsx new file mode 100644 index 0000000..eda033b --- /dev/null +++ b/components/case-form.tsx @@ -0,0 +1,547 @@ +"use client" + +import { useState } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Calendar, X, Loader2, Plus } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Calendar as CalendarComponent } from "@/components/ui/calendar" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { cn } from "@/lib/utils" +import { format } from "date-fns" +import { createCase } from "@/lib/cases" +import { Case } from "@/model/Case" + +const caseFormSchema = z.object({ + caseNumber: z.string().min(1, "Case number is required"), + title: z.string().min(1, "Case title is required"), + court: z.string().min(1, "Court name is required"), + type: z.string().min(1, "Case type is required"), + stage: z.enum(["Filed", "Hearing", "Evidence", "Arguments", "Judgment", "Closed"]), + status: z.enum(["Active", "Pending", "Delayed", "Completed"]), + progress: z.number().min(0).max(100), + description: z.string().optional(), + clientName: z.string().optional(), + opposingParty: z.string().optional(), + caseValue: z.number().min(0).default(0), + filingDate: z.date(), + nextHearing: z.date().optional(), + notes: z.string().optional(), +}) + +type CaseFormValues = z.infer + +interface CaseFormProps { + isOpen: boolean + onClose: () => void + onSuccess: () => void + editCase?: Case | null +} + +export function CaseForm({ isOpen, onClose, onSuccess, editCase }: CaseFormProps) { + const [isSubmitting, setIsSubmitting] = useState(false) + const [error, setError] = useState(null) + const [filingDateOpen, setFilingDateOpen] = useState(false) + const [nextHearingOpen, setNextHearingOpen] = useState(false) + + const form = useForm({ + resolver: zodResolver(caseFormSchema), + defaultValues: { + caseNumber: editCase?.caseNumber || "", + title: editCase?.title || "", + court: editCase?.court || "", + type: editCase?.type || "", + stage: editCase?.stage || "Filed", + status: editCase?.status || "Active", + progress: editCase?.progress || 0, + description: editCase?.description || "", + clientName: editCase?.clientName || "", + opposingParty: editCase?.opposingParty || "", + caseValue: editCase?.caseValue || 0, + filingDate: editCase?.filingDate ? new Date(editCase.filingDate) : new Date(), + nextHearing: editCase?.nextHearing ? new Date(editCase.nextHearing) : undefined, + notes: editCase?.notes || "", + }, + }) + + const onSubmit = async (values: CaseFormValues) => { + try { + setIsSubmitting(true) + setError(null) + + // Prepare data for API - convert 0 caseValue to undefined + const apiData = { + ...values, + caseValue: values.caseValue === 0 ? undefined : values.caseValue + } + + if (editCase) { + // Update existing case + const { updateCase } = await import("@/lib/cases") + await updateCase(editCase._id, apiData) + } else { + // Create new case + await createCase(apiData) + } + + onSuccess() + onClose() + form.reset() + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to save case") + } finally { + setIsSubmitting(false) + } + } + + const handleClose = () => { + form.reset() + setError(null) + onClose() + } + + return ( + + + +
+
+ +
+
+ + {editCase ? "Edit Case" : "Add New Case"} + +

+ {editCase ? "Update case information and details" : "Create a new legal case entry"} +

+
+
+
+ +
+ + {/* Basic Information */} +
+
+
+

Basic Information

+
+ +
+ ( + + Case Number * + + + + + + )} + /> + + ( + + Case Title * + + + + + + )} + /> + + ( + + Court * + + + + + + )} + /> + + ( + + Case Type * + + + + + + )} + /> +
+ + ( + + Description + +