Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Deploy: Consolidated gold tables, fixed nginx docs routing
Browse files- api/routes/bills_neon.py +59 -0
- frontend/src/pages/PolicyMap.tsx +86 -3
api/routes/bills_neon.py
CHANGED
|
@@ -94,6 +94,9 @@ async def fetch_bills_from_parquet(
|
|
| 94 |
q: Optional[str] = None,
|
| 95 |
session: Optional[str] = None,
|
| 96 |
topic: Optional[str] = None,
|
|
|
|
|
|
|
|
|
|
| 97 |
limit: int = 50,
|
| 98 |
offset: int = 0
|
| 99 |
) -> Dict[str, Any]:
|
|
@@ -143,6 +146,50 @@ async def fetch_bills_from_parquet(
|
|
| 143 |
where_clauses.append("session = ?")
|
| 144 |
params.append(session)
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
where_clause = " AND ".join(where_clauses)
|
| 147 |
|
| 148 |
# Count total
|
|
@@ -197,6 +244,9 @@ async def fetch_bills_from_parquet(
|
|
| 197 |
"state": state,
|
| 198 |
"query": q,
|
| 199 |
"topic": topic,
|
|
|
|
|
|
|
|
|
|
| 200 |
"session": session,
|
| 201 |
"bills": bills,
|
| 202 |
"total": total,
|
|
@@ -411,6 +461,9 @@ async def get_bills(
|
|
| 411 |
q: Optional[str] = Query(None, description="Search query (bill number or title)"),
|
| 412 |
session: Optional[str] = Query(None, description="Legislative session"),
|
| 413 |
topic: Optional[str] = Query(None, description="Policy topic (e.g., fluoride, dental, medicaid)"),
|
|
|
|
|
|
|
|
|
|
| 414 |
limit: int = Query(50, ge=1, le=500),
|
| 415 |
offset: int = Query(0, ge=0)
|
| 416 |
):
|
|
@@ -421,6 +474,9 @@ async def get_bills(
|
|
| 421 |
- `/api/bills?state=AL&q=dental` - Search Alabama bills for "dental"
|
| 422 |
- `/api/bills?state=AL&session=2024rs` - Get all 2024 regular session bills
|
| 423 |
- `/api/bills?state=AL&topic=fluoride` - Get fluoride-related Alabama bills
|
|
|
|
|
|
|
|
|
|
| 424 |
- `/api/bills?state=AL&limit=50` - Browse recent Alabama bills
|
| 425 |
"""
|
| 426 |
try:
|
|
@@ -429,6 +485,9 @@ async def get_bills(
|
|
| 429 |
q=q,
|
| 430 |
session=session,
|
| 431 |
topic=topic,
|
|
|
|
|
|
|
|
|
|
| 432 |
limit=limit,
|
| 433 |
offset=offset
|
| 434 |
)
|
|
|
|
| 94 |
q: Optional[str] = None,
|
| 95 |
session: Optional[str] = None,
|
| 96 |
topic: Optional[str] = None,
|
| 97 |
+
chamber: Optional[str] = None,
|
| 98 |
+
bill_type: Optional[str] = None,
|
| 99 |
+
status: Optional[str] = None,
|
| 100 |
limit: int = 50,
|
| 101 |
offset: int = 0
|
| 102 |
) -> Dict[str, Any]:
|
|
|
|
| 146 |
where_clauses.append("session = ?")
|
| 147 |
params.append(session)
|
| 148 |
|
| 149 |
+
# Chamber filter (based on bill number prefix)
|
| 150 |
+
if chamber:
|
| 151 |
+
if chamber.lower() == 'house':
|
| 152 |
+
where_clauses.append("(bill_number LIKE 'HB%' OR bill_number LIKE 'HR%' OR bill_number LIKE 'HJR%' OR bill_number LIKE 'HCR%' OR bill_number LIKE 'HJM%' OR bill_number LIKE 'H %')")
|
| 153 |
+
elif chamber.lower() == 'senate':
|
| 154 |
+
where_clauses.append("(bill_number LIKE 'SB%' OR bill_number LIKE 'SR%' OR bill_number LIKE 'SJR%' OR bill_number LIKE 'SCR%' OR bill_number LIKE 'SJM%' OR bill_number LIKE 'S %')")
|
| 155 |
+
elif chamber.lower() == 'joint':
|
| 156 |
+
where_clauses.append("(bill_number LIKE '%JR%' OR bill_number LIKE '%JM%')")
|
| 157 |
+
|
| 158 |
+
# Bill type filter (based on bill number pattern)
|
| 159 |
+
if bill_type:
|
| 160 |
+
if bill_type == 'bill':
|
| 161 |
+
where_clauses.append("(bill_number LIKE 'HB%' OR bill_number LIKE 'SB%' OR bill_number LIKE 'AB%')")
|
| 162 |
+
elif bill_type == 'resolution':
|
| 163 |
+
where_clauses.append("(bill_number LIKE 'HR%' OR bill_number LIKE 'SR%' OR bill_number LIKE 'AR%')")
|
| 164 |
+
elif bill_type == 'joint_resolution':
|
| 165 |
+
where_clauses.append("(bill_number LIKE 'HJR%' OR bill_number LIKE 'SJR%' OR bill_number LIKE 'AJR%')")
|
| 166 |
+
elif bill_type == 'concurrent_resolution':
|
| 167 |
+
where_clauses.append("(bill_number LIKE 'HCR%' OR bill_number LIKE 'SCR%')")
|
| 168 |
+
elif bill_type == 'memorial':
|
| 169 |
+
where_clauses.append("(bill_number LIKE 'HJM%' OR bill_number LIKE 'SJM%')")
|
| 170 |
+
|
| 171 |
+
# Status filter (based on latest_action_description)
|
| 172 |
+
if status:
|
| 173 |
+
status_keywords = {
|
| 174 |
+
'enacted': 'Enacted',
|
| 175 |
+
'passed': 'passed|Passed',
|
| 176 |
+
'adopted': 'Adopted|adopted',
|
| 177 |
+
'failed': 'Failed|failed',
|
| 178 |
+
'introduced': 'Introduced|introduced',
|
| 179 |
+
'referred': 'referred|Referred',
|
| 180 |
+
'reported': 'reported|Reported'
|
| 181 |
+
}
|
| 182 |
+
keyword = status_keywords.get(status.lower(), status)
|
| 183 |
+
|
| 184 |
+
if '|' in keyword:
|
| 185 |
+
keyword_parts = keyword.split('|')
|
| 186 |
+
keyword_clauses = ["LOWER(latest_action_description) LIKE LOWER(?)"] * len(keyword_parts)
|
| 187 |
+
where_clauses.append(f"({' OR '.join(keyword_clauses)})")
|
| 188 |
+
params.extend([f'%{kw}%' for kw in keyword_parts])
|
| 189 |
+
else:
|
| 190 |
+
where_clauses.append("LOWER(latest_action_description) LIKE LOWER(?)")
|
| 191 |
+
params.append(f'%{keyword}%')
|
| 192 |
+
|
| 193 |
where_clause = " AND ".join(where_clauses)
|
| 194 |
|
| 195 |
# Count total
|
|
|
|
| 244 |
"state": state,
|
| 245 |
"query": q,
|
| 246 |
"topic": topic,
|
| 247 |
+
"chamber": chamber,
|
| 248 |
+
"bill_type": bill_type,
|
| 249 |
+
"status": status,
|
| 250 |
"session": session,
|
| 251 |
"bills": bills,
|
| 252 |
"total": total,
|
|
|
|
| 461 |
q: Optional[str] = Query(None, description="Search query (bill number or title)"),
|
| 462 |
session: Optional[str] = Query(None, description="Legislative session"),
|
| 463 |
topic: Optional[str] = Query(None, description="Policy topic (e.g., fluoride, dental, medicaid)"),
|
| 464 |
+
chamber: Optional[str] = Query(None, description="Legislative chamber (house, senate, joint)"),
|
| 465 |
+
bill_type: Optional[str] = Query(None, description="Bill type (bill, resolution, joint_resolution, concurrent_resolution, memorial)"),
|
| 466 |
+
status: Optional[str] = Query(None, description="Bill status (enacted, passed, adopted, failed, introduced, referred, reported)"),
|
| 467 |
limit: int = Query(50, ge=1, le=500),
|
| 468 |
offset: int = Query(0, ge=0)
|
| 469 |
):
|
|
|
|
| 474 |
- `/api/bills?state=AL&q=dental` - Search Alabama bills for "dental"
|
| 475 |
- `/api/bills?state=AL&session=2024rs` - Get all 2024 regular session bills
|
| 476 |
- `/api/bills?state=AL&topic=fluoride` - Get fluoride-related Alabama bills
|
| 477 |
+
- `/api/bills?state=AL&chamber=house` - Get House bills only
|
| 478 |
+
- `/api/bills?state=AL&bill_type=bill` - Get only bills (not resolutions)
|
| 479 |
+
- `/api/bills?state=AL&status=enacted` - Get enacted bills only
|
| 480 |
- `/api/bills?state=AL&limit=50` - Browse recent Alabama bills
|
| 481 |
"""
|
| 482 |
try:
|
|
|
|
| 485 |
q=q,
|
| 486 |
session=session,
|
| 487 |
topic=topic,
|
| 488 |
+
chamber=chamber,
|
| 489 |
+
bill_type=bill_type,
|
| 490 |
+
status=status,
|
| 491 |
limit=limit,
|
| 492 |
offset=offset
|
| 493 |
)
|
frontend/src/pages/PolicyMap.tsx
CHANGED
|
@@ -55,6 +55,11 @@ export default function PolicyMap() {
|
|
| 55 |
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
| 56 |
const [expandedBill, setExpandedBill] = useState<string | null>(null)
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
// Read topic from URL, or default to empty (showing topic selector)
|
| 59 |
const topicFromUrl = searchParams.get('topic') || ''
|
| 60 |
const [selectedTopic, setSelectedTopic] = useState(topicFromUrl)
|
|
@@ -114,7 +119,7 @@ export default function PolicyMap() {
|
|
| 114 |
bills: Bill[]
|
| 115 |
pagination: { limit: number; offset: number; has_more: boolean }
|
| 116 |
}>({
|
| 117 |
-
queryKey: ['bills', selectedState, selectedSession, searchQuery, selectedTopic, page],
|
| 118 |
queryFn: async () => {
|
| 119 |
const params = new URLSearchParams({
|
| 120 |
state: selectedState,
|
|
@@ -124,6 +129,9 @@ export default function PolicyMap() {
|
|
| 124 |
if (selectedSession) params.append('session', selectedSession)
|
| 125 |
if (searchQuery) params.append('q', searchQuery)
|
| 126 |
if (selectedTopic) params.append('topic', selectedTopic)
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
const response = await api.get(`/bills?${params}`)
|
| 129 |
return response.data
|
|
@@ -469,6 +477,78 @@ export default function PolicyMap() {
|
|
| 469 |
</select>
|
| 470 |
</div>
|
| 471 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
|
| 473 |
{/* Search */}
|
| 474 |
<div className="flex-1 min-w-[250px]">
|
|
@@ -489,17 +569,20 @@ export default function PolicyMap() {
|
|
| 489 |
</div>
|
| 490 |
|
| 491 |
{/* Clear button */}
|
| 492 |
-
{(searchQuery || selectedSession) && (
|
| 493 |
<button
|
| 494 |
type="button"
|
| 495 |
onClick={() => {
|
| 496 |
setSearchQuery('')
|
| 497 |
setSelectedSession('')
|
|
|
|
|
|
|
|
|
|
| 498 |
setPage(1)
|
| 499 |
}}
|
| 500 |
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors text-sm font-medium"
|
| 501 |
>
|
| 502 |
-
Clear
|
| 503 |
</button>
|
| 504 |
)}
|
| 505 |
|
|
|
|
| 55 |
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
| 56 |
const [expandedBill, setExpandedBill] = useState<string | null>(null)
|
| 57 |
|
| 58 |
+
// New filter states
|
| 59 |
+
const [selectedChamber, setSelectedChamber] = useState<string>('')
|
| 60 |
+
const [selectedBillType, setSelectedBillType] = useState<string>('')
|
| 61 |
+
const [selectedStatus, setSelectedStatus] = useState<string>('')
|
| 62 |
+
|
| 63 |
// Read topic from URL, or default to empty (showing topic selector)
|
| 64 |
const topicFromUrl = searchParams.get('topic') || ''
|
| 65 |
const [selectedTopic, setSelectedTopic] = useState(topicFromUrl)
|
|
|
|
| 119 |
bills: Bill[]
|
| 120 |
pagination: { limit: number; offset: number; has_more: boolean }
|
| 121 |
}>({
|
| 122 |
+
queryKey: ['bills', selectedState, selectedSession, searchQuery, selectedTopic, selectedChamber, selectedBillType, selectedStatus, page],
|
| 123 |
queryFn: async () => {
|
| 124 |
const params = new URLSearchParams({
|
| 125 |
state: selectedState,
|
|
|
|
| 129 |
if (selectedSession) params.append('session', selectedSession)
|
| 130 |
if (searchQuery) params.append('q', searchQuery)
|
| 131 |
if (selectedTopic) params.append('topic', selectedTopic)
|
| 132 |
+
if (selectedChamber) params.append('chamber', selectedChamber)
|
| 133 |
+
if (selectedBillType) params.append('bill_type', selectedBillType)
|
| 134 |
+
if (selectedStatus) params.append('status', selectedStatus)
|
| 135 |
|
| 136 |
const response = await api.get(`/bills?${params}`)
|
| 137 |
return response.data
|
|
|
|
| 477 |
</select>
|
| 478 |
</div>
|
| 479 |
)}
|
| 480 |
+
|
| 481 |
+
{/* Chamber Filter - list view only */}
|
| 482 |
+
{viewMode === 'list' && (
|
| 483 |
+
<div className="flex-1 min-w-[150px]">
|
| 484 |
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
| 485 |
+
Chamber
|
| 486 |
+
</label>
|
| 487 |
+
<select
|
| 488 |
+
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm text-gray-900 py-2"
|
| 489 |
+
value={selectedChamber}
|
| 490 |
+
onChange={(e) => {
|
| 491 |
+
setSelectedChamber(e.target.value)
|
| 492 |
+
setPage(1)
|
| 493 |
+
}}
|
| 494 |
+
>
|
| 495 |
+
<option value="">All Chambers</option>
|
| 496 |
+
<option value="house">House</option>
|
| 497 |
+
<option value="senate">Senate</option>
|
| 498 |
+
<option value="joint">Joint</option>
|
| 499 |
+
</select>
|
| 500 |
+
</div>
|
| 501 |
+
)}
|
| 502 |
+
|
| 503 |
+
{/* Bill Type Filter - list view only */}
|
| 504 |
+
{viewMode === 'list' && (
|
| 505 |
+
<div className="flex-1 min-w-[150px]">
|
| 506 |
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
| 507 |
+
Bill Type
|
| 508 |
+
</label>
|
| 509 |
+
<select
|
| 510 |
+
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm text-gray-900 py-2"
|
| 511 |
+
value={selectedBillType}
|
| 512 |
+
onChange={(e) => {
|
| 513 |
+
setSelectedBillType(e.target.value)
|
| 514 |
+
setPage(1)
|
| 515 |
+
}}
|
| 516 |
+
>
|
| 517 |
+
<option value="">All Types</option>
|
| 518 |
+
<option value="bill">Bill (HB/SB)</option>
|
| 519 |
+
<option value="resolution">Resolution (HR/SR)</option>
|
| 520 |
+
<option value="joint_resolution">Joint Resolution (HJR/SJR)</option>
|
| 521 |
+
<option value="concurrent_resolution">Concurrent Resolution (HCR/SCR)</option>
|
| 522 |
+
<option value="memorial">Memorial (HJM/SJM)</option>
|
| 523 |
+
</select>
|
| 524 |
+
</div>
|
| 525 |
+
)}
|
| 526 |
+
|
| 527 |
+
{/* Status Filter - list view only */}
|
| 528 |
+
{viewMode === 'list' && (
|
| 529 |
+
<div className="flex-1 min-w-[150px]">
|
| 530 |
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
| 531 |
+
Status
|
| 532 |
+
</label>
|
| 533 |
+
<select
|
| 534 |
+
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm text-gray-900 py-2"
|
| 535 |
+
value={selectedStatus}
|
| 536 |
+
onChange={(e) => {
|
| 537 |
+
setSelectedStatus(e.target.value)
|
| 538 |
+
setPage(1)
|
| 539 |
+
}}
|
| 540 |
+
>
|
| 541 |
+
<option value="">All Statuses</option>
|
| 542 |
+
<option value="enacted">Enacted</option>
|
| 543 |
+
<option value="passed">Passed</option>
|
| 544 |
+
<option value="adopted">Adopted</option>
|
| 545 |
+
<option value="failed">Failed</option>
|
| 546 |
+
<option value="introduced">Introduced</option>
|
| 547 |
+
<option value="referred">Referred to Committee</option>
|
| 548 |
+
<option value="reported">Reported from Committee</option>
|
| 549 |
+
</select>
|
| 550 |
+
</div>
|
| 551 |
+
)}
|
| 552 |
|
| 553 |
{/* Search */}
|
| 554 |
<div className="flex-1 min-w-[250px]">
|
|
|
|
| 569 |
</div>
|
| 570 |
|
| 571 |
{/* Clear button */}
|
| 572 |
+
{(searchQuery || selectedSession || selectedChamber || selectedBillType || selectedStatus) && (
|
| 573 |
<button
|
| 574 |
type="button"
|
| 575 |
onClick={() => {
|
| 576 |
setSearchQuery('')
|
| 577 |
setSelectedSession('')
|
| 578 |
+
setSelectedChamber('')
|
| 579 |
+
setSelectedBillType('')
|
| 580 |
+
setSelectedStatus('')
|
| 581 |
setPage(1)
|
| 582 |
}}
|
| 583 |
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors text-sm font-medium"
|
| 584 |
>
|
| 585 |
+
Clear Filters
|
| 586 |
</button>
|
| 587 |
)}
|
| 588 |
|