From 198546ebf5197e04589620d43a2e30e78942fde5 Mon Sep 17 00:00:00 2001 From: jainiksha Date: Sun, 21 Jun 2026 12:53:59 +0530 Subject: [PATCH] feat: add advanced case search with filters and smart suggestions --- .../dashboard/AdvancedCaseSearch.jsx | 183 +++++++++++ .../src/components/dashboard/CaseCard.jsx | 302 ++++++++++-------- .../src/pages/lawyer/LawyerCasesPage.jsx | 158 ++++++++- 3 files changed, 500 insertions(+), 143 deletions(-) create mode 100644 frontend/nyaysetu-frontend/src/components/dashboard/AdvancedCaseSearch.jsx diff --git a/frontend/nyaysetu-frontend/src/components/dashboard/AdvancedCaseSearch.jsx b/frontend/nyaysetu-frontend/src/components/dashboard/AdvancedCaseSearch.jsx new file mode 100644 index 000000000..c706223c4 --- /dev/null +++ b/frontend/nyaysetu-frontend/src/components/dashboard/AdvancedCaseSearch.jsx @@ -0,0 +1,183 @@ +import { Search, Filter } from "lucide-react"; + +export default function AdvancedCaseSearch({ + searchTerm, + setSearchTerm, + filterStatus, + setFilterStatus, + caseTypeFilter, + setCaseTypeFilter, + dateFilter, + setDateFilter, + recentSearches, + showSuggestions, + setShowSuggestions, + clearFilters +}) { + return ( +
+ {/* Search Input */} +
+ + + setSearchTerm(e.target.value)} + onFocus={() => setShowSuggestions(true)} + onBlur={() => + setTimeout(() => setShowSuggestions(false), 200) + } + style={{ + width: "100%", + padding: "0.8rem 1rem 0.8rem 3rem", + background: "var(--bg-glass)", + border: "var(--border-glass)", + borderRadius: "0.75rem", + color: "var(--text-main)", + outline: "none", + }} + /> + + {/* Recent Suggestions */} + {showSuggestions && recentSearches.length > 0 && ( +
+

+ Recent Searches +

+ + {recentSearches.map((item) => ( +
{ + setSearchTerm(item); + setShowSuggestions(false); + }} + style={{ + padding: "0.7rem", + cursor: "pointer", + borderRadius: "0.5rem", + }} + > + 🔍 {item} +
+ ))} +
+ )} +
+ + {/* Status Filter */} +
+ + + +
+ + {/* Case Type Filter */} + + + {/* Date Filter */} + setDateFilter(e.target.value)} + style={{ + padding: "0.8rem", + borderRadius: "0.75rem", + background: "var(--bg-glass)", + border: "var(--border-glass)", + color: "var(--text-main)", + }} + /> + + {/* Clear Button */} + +
+ ); +} \ No newline at end of file diff --git a/frontend/nyaysetu-frontend/src/components/dashboard/CaseCard.jsx b/frontend/nyaysetu-frontend/src/components/dashboard/CaseCard.jsx index 342ad07c4..83c1b3a05 100644 --- a/frontend/nyaysetu-frontend/src/components/dashboard/CaseCard.jsx +++ b/frontend/nyaysetu-frontend/src/components/dashboard/CaseCard.jsx @@ -1,158 +1,188 @@ -/** - * CaseCard — Reusable card component for displaying a legal case summary. - * - * Props: - * id {string} Short display ID (e.g. "CS-a1b2c3") - * title {string} Case title - * status {string} Case status: "PENDING" | "OPEN" | "CLOSED" | "DRAFT_PENDING_CLIENT" | etc. - * date {string} Filed date formatted as a locale string - * onClick {function} Called when the card is clicked - * filedLabel {string} Label text for the date row (e.g. "Filed") - */ + /** + * CaseCard — Reusable card component for displaying a legal case summary. + * + * Props: + * id {string} Short display ID (e.g. "CS-a1b2c3") + * title {string} Case title + * status {string} Case status: "PENDING" | "OPEN" | "CLOSED" | "DRAFT_PENDING_CLIENT" | etc. + * date {string} Filed date formatted as a locale string + * onClick {function} Called when the card is clicked + * filedLabel {string} Label text for the date row (e.g. "Filed") + */ -import { useState } from 'react'; -import { FolderOpen } from 'lucide-react'; + import { useState } from 'react'; + import { FolderOpen } from 'lucide-react'; -// Map each status to its colour palette using CSS-variable-safe values so -// the card automatically adapts to light/dark mode via ThemeContext. -const STATUS_STYLES = { - PENDING: { - background: 'rgba(245, 158, 11, 0.1)', - color: '#f59e0b', - border: 'rgba(245, 158, 11, 0.25)', - }, - OPEN: { - background: 'rgba(59, 130, 246, 0.1)', - color: '#3b82f6', - border: 'rgba(59, 130, 246, 0.25)', - }, - CLOSED: { - background: 'rgba(16, 185, 129, 0.1)', - color: '#10b981', - border: 'rgba(16, 185, 129, 0.25)', - }, - DRAFT_PENDING_CLIENT: { - background: 'rgba(139, 92, 246, 0.1)', - color: '#8b5cf6', - border: 'rgba(139, 92, 246, 0.25)', - }, -}; + // Map each status to its colour palette using CSS-variable-safe values so + // the card automatically adapts to light/dark mode via ThemeContext. + const STATUS_STYLES = { + PENDING: { + background: 'rgba(245, 158, 11, 0.1)', + color: '#f59e0b', + border: 'rgba(245, 158, 11, 0.25)', + }, + OPEN: { + background: 'rgba(59, 130, 246, 0.1)', + color: '#3b82f6', + border: 'rgba(59, 130, 246, 0.25)', + }, + CLOSED: { + background: 'rgba(16, 185, 129, 0.1)', + color: '#10b981', + border: 'rgba(16, 185, 129, 0.25)', + }, + DRAFT_PENDING_CLIENT: { + background: 'rgba(139, 92, 246, 0.1)', + color: '#8b5cf6', + border: 'rgba(139, 92, 246, 0.25)', + }, + }; -// Fallback for any unrecognised status value -const DEFAULT_STATUS_STYLE = { - background: 'rgba(100, 116, 139, 0.1)', - color: '#64748b', - border: 'rgba(100, 116, 139, 0.25)', -}; + // Fallback for any unrecognised status value + const DEFAULT_STATUS_STYLE = { + background: 'rgba(100, 116, 139, 0.1)', + color: '#64748b', + border: 'rgba(100, 116, 139, 0.25)', + }; -export default function CaseCard({ id, title, status, date, onClick, filedLabel = 'Filed', children }) { - const [isHovered, setIsHovered] = useState(false); + export default function CaseCard({ id, title, status, date, caseType, courtName, assignedPerson, onClick, filedLabel = 'Filed', children }) { + const [isHovered, setIsHovered] = useState(false); - const statusStyle = STATUS_STYLES[status] || DEFAULT_STATUS_STYLE; + const statusStyle = STATUS_STYLES[status] || DEFAULT_STATUS_STYLE; - return ( -
e.key === 'Enter' && onClick?.()} - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={{ - padding: '1rem', - background: 'var(--bg-glass)', - borderRadius: '0.75rem', - border: isHovered - ? `1px solid var(--color-primary)` - : 'var(--border-glass)', - cursor: 'pointer', - transition: 'all 0.2s ease', - transform: isHovered ? 'translateY(-2px)' : 'translateY(0)', - boxShadow: isHovered - ? '0 6px 20px rgba(30, 42, 68, 0.1)' - : 'none', - outline: 'none', - }} - > - {/* Top row — case ID + status badge */} + return (
e.key === 'Enter' && onClick?.()} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} style={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: '0.5rem', - gap: '0.5rem', + padding: '1rem', + background: 'var(--bg-glass)', + borderRadius: '0.75rem', + border: isHovered + ? `1px solid var(--color-primary)` + : 'var(--border-glass)', + cursor: 'pointer', + transition: 'all 0.2s ease', + transform: isHovered ? 'translateY(-2px)' : 'translateY(0)', + boxShadow: isHovered + ? '0 6px 20px rgba(30, 42, 68, 0.1)' + : 'none', + outline: 'none', }} > - {/* Case ID */} - - - {id} - + {/* Case ID */} + + + {id} + + + {/* Status pill */} + + {status} + +
- {/* Status pill */} - - {status} - -
+ {title} + - {/* Case title */} -

- {title} -

+
+

+ {filedLabel}: {date} +

- {/* Filed date */} -

- {filedLabel}: {date} -

+ {caseType && ( +

+ Type: + + {' '}{caseType} + +

+ )} - {/* Extra content (e.g. CaseStepper) */} - {children && ( -
- {children} -
- )} -
- ); -} + {courtName && ( +

+ Court: + + {' '}{courtName} + +

+ )} + + {assignedPerson && ( +

+ Assigned: + + {' '}{assignedPerson} + +

+ )} + + + {/* Extra content (e.g. CaseStepper) */} + {children && ( +
+ {children} +
+ )} + + ); + } diff --git a/frontend/nyaysetu-frontend/src/pages/lawyer/LawyerCasesPage.jsx b/frontend/nyaysetu-frontend/src/pages/lawyer/LawyerCasesPage.jsx index 84fb86fcb..bdd290696 100644 --- a/frontend/nyaysetu-frontend/src/pages/lawyer/LawyerCasesPage.jsx +++ b/frontend/nyaysetu-frontend/src/pages/lawyer/LawyerCasesPage.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { caseAPI, lawyerAPI, caseAssignmentAPI } from '../../services/api'; +import AdvancedCaseSearch from "../../components/dashboard/AdvancedCaseSearch"; import { Search, Filter, @@ -29,6 +30,10 @@ export default function LawyerCasesPage() { const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [filterStatus, setFilterStatus] = useState('ALL'); + const [caseTypeFilter, setCaseTypeFilter] = useState('ALL'); + const [dateFilter, setDateFilter] = useState(''); + const [recentSearches, setRecentSearches] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); const [activeTab, setActiveTab] = useState('active'); // 'active' or 'proposals' const [showAllSearchResults, setShowAllSearchResults] = useState(false); const navigate = useNavigate(); @@ -96,19 +101,48 @@ export default function LawyerCasesPage() { const displayCases = activeTab === 'active' ? activeCases : proposals; const filteredCases = displayCases.filter(c => { - const matchesSearch = (c.title?.toLowerCase().includes(searchTerm.toLowerCase()) || - c.description?.toLowerCase().includes(searchTerm.toLowerCase())); - const matchesFilter = filterStatus === 'ALL' || c.status === filterStatus; - return matchesSearch && matchesFilter; - }); + const search = searchTerm.toLowerCase(); + + const matchesSearch = + c.title?.toLowerCase().includes(search) || + c.description?.toLowerCase().includes(search) || + c.id?.toLowerCase().includes(search) || + c.caseType?.toLowerCase().includes(search) || + c.petitioner?.toLowerCase().includes(search); + + const matchesStatus = + filterStatus === 'ALL' || c.status === filterStatus; + + const matchesType = + caseTypeFilter === 'ALL' || c.caseType === caseTypeFilter; + + const matchesDate = + !dateFilter || + new Date(c.filedDate || c.createdAt) + .toISOString() + .split('T')[0] === dateFilter; + + return ( + matchesSearch && + matchesStatus && + matchesType && + matchesDate + ); +}); const previewLimit = 4; const visibleCases = showAllSearchResults ? filteredCases : filteredCases.slice(0, previewLimit); const hiddenCases = showAllSearchResults ? [] : filteredCases.slice(previewLimit); useEffect(() => { - setShowAllSearchResults(false); - }, [searchTerm, filterStatus, activeTab]); + setShowAllSearchResults(false); +}, [ + searchTerm, + filterStatus, + caseTypeFilter, + dateFilter, + activeTab +]); const glassStyle = { background: 'var(--bg-glass-strong)', @@ -119,6 +153,18 @@ export default function LawyerCasesPage() { boxShadow: 'var(--shadow-glass-strong)' }; + useEffect(() => { + if ( + searchTerm.trim() && + !recentSearches.includes(searchTerm) + ) { + setRecentSearches(prev => [ + searchTerm, + ...prev + ].slice(0, 5)); + } +}, [searchTerm, recentSearches]); + if (loading) { return (
@@ -244,6 +290,10 @@ export default function LawyerCasesPage() { placeholder={`Search ${activeTab === 'active' ? 'active portfolio' : 'proposals'}...`} value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} + onFocus={() => setShowSuggestions(true)} + onBlur={() => + setTimeout(() => setShowSuggestions(false), 200) + } style={{ width: '100%', background: 'var(--bg-glass)', @@ -255,6 +305,54 @@ export default function LawyerCasesPage() { fontSize: '0.95rem' }} /> + { + showSuggestions && + recentSearches.length > 0 && ( +
+

+ Recent Searches +

+ + { + recentSearches.map(item => ( +
{ + setSearchTerm(item); + setShowSuggestions(false); + }} + style={{ + padding: "0.7rem", + cursor: "pointer", + borderRadius: "0.5rem" + }} + > + 🔍 {item} +
+ )) + } +
+ ) +}
{activeTab === 'active' && (
@@ -279,6 +377,52 @@ export default function LawyerCasesPage() { + + setDateFilter(e.target.value)} + style={{ + background: 'var(--bg-glass)', + border: 'var(--border-glass)', + borderRadius: '0.75rem', + padding: '0.8rem', + color: 'var(--text-main)', + outline: 'none' + }} +/>
)}