17 KiB
Admin Panel Table Sorting Feature
Overview
The admin panel now supports sorting by clicking on column headers. This feature provides server-side sorting with proper integration into the existing pagination and filtering system.
Features
✅ Sortable Columns
All table columns are now sortable:
- Antrags-ID (
pa_id) - String sorting - Projektname (
project_name) - String sorting (case-insensitive) - Typ (
variant) - String sorting - Status (
status) - String sorting - Summe (
total_amount) - Numeric sorting - Erstellt (
created_at) - Date/time sorting - Geändert (
updated_at) - Date/time sorting
🔄 Sorting Behavior
- First click: Sort ascending by selected column
- Second click: Sort descending by selected column
- Click on different column: Switch to new column with ascending sort
- Visual indicators: Active column shows arrow icon indicating sort direction
- Default sorting: By creation date, newest first (
created_at DESC)
🏗️ Technical Implementation
Backend Changes
Enhanced API endpoints:
GET /applications- Addedorder_byandorderquery parametersGET /applications/search- Addedorder_byandorderquery parameters
Supported parameters:
order_by: pa_id | project_name | variant | status | total_amount | created_at | updated_at
order: asc | desc
Database-level sorting:
- Most fields sorted directly in SQL for performance
total_amountandproject_namerequire post-processing due to JSON extraction- Proper handling of NULL values
Frontend Changes
Store Integration:
- Added
sortByandsortOrderto application state - Added
setSorting()action to update sort parameters - Automatic data refresh when sorting changes
UI Components:
- Replaced static table headers with
TableSortLabelcomponents - Visual indicators for active sort column and direction
- Click handlers for all sortable columns
API Client:
- Extended
listApplicationsAdmin()andsearchApplications()methods - Added sorting parameters to all relevant API calls
📊 Performance
- Server-side sorting: All sorting happens in the database
- Consistent pagination: Sort order maintained across pages
- Efficient queries: Direct SQL sorting for most fields
- Fallback processing: Post-query sorting only for computed fields
🔧 Usage
Administrators can now:
- Click any column header to sort by that field
- Click again to reverse sort order
- Switch between different columns while maintaining filter settings
- Navigate pages while preserving sort order
- Combine sorting with existing search and filter functionality
🎯 Benefits
- Improved UX: Intuitive sorting following Material Design patterns
- Better data discovery: Easy identification of highest/lowest values
- Scalable performance: Works efficiently with large datasets
- Consistent behavior: Sorting preserved across pagination and filtering
💻 Code Examples
Triggering sort from component:
// Sort by project name ascending
setSorting("project_name", "asc");
// Sort by amount descending
setSorting("total_amount", "desc");
API call with sorting:
await apiClient.listApplicationsAdmin({
limit: 50,
offset: 0,
order_by: "created_at",
order: "desc"
});
Bug Fixes
Fixed: "Mit Anhängen" Filter Issue
Problem: The attachment filter was returning empty results because the SQL query was looking for a pa_id field in the attachments table, which doesn't exist.
Solution:
- Fixed SQL query to properly join through the
application_attachmentsjunction table - Integrated attachment counting directly into the main query for better performance
- Added proper
has_attachmentsfiltering at the SQL level
Technical Changes:
- Modified main SQL query to include LEFT JOIN with attachment count subquery
- Updated all table references to use proper aliases (
a.for applications table) - Moved attachment filtering from post-processing loop to SQL WHERE clause
Performance Impact:
- Reduced from N+1 queries to a single efficient query with JOINs
- Eliminated post-processing filter loops for attachment checks
Future Enhancements
- Multi-column sorting support
- Remember user sort preferences
- Export sorted data functionality
- Sort indicators in column headers even when not active
Recent Improvements
1. Smart Quick Search Integration
Feature: Quick search now respects all active filters instead of ignoring them.
Before: Quick search would override all filters and only search by query text After: Quick search combines with active filters (status, variant, amount ranges, date ranges, creator, attachments)
Technical Implementation:
- Quick search now uses
searchApplicationsAdvanced()instead ofsearchApplications() - Automatically includes all active filter parameters in search request
- Maintains filter state while performing text search
2. Fuzzy Creator Filter
Feature: The "Created by" (Ersteller) filter now performs case-insensitive, fuzzy matching.
Before:
JSON_EXTRACT(..., '$.pa.applicant.contact.email') LIKE :created_by
After:
LOWER(JSON_EXTRACT(..., '$.pa.applicant.contact.email')) LIKE LOWER(:created_by)
Benefits:
- Case-insensitive matching
- Contains search (not just startsWith)
- More user-friendly search experience
3. Clear All Filters Button
Feature: Added a "Alle Filter zurücksetzen" button below the quick search.
Behavior:
- Only appears when filters are active (activeFilters, statusFilter, variantFilter, or searchText)
- Clears all filters and search text with one click
- Automatically reloads data without any filters
UX Benefits:
- Quick way to reset complex filter combinations
- Reduces friction when starting fresh searches
- Clear visual indication when filters can be cleared
Technical Details
Frontend Changes:
// Quick search now includes all active filters
const searchParams = {
q: searchText.trim(),
status: statusFilter || undefined,
variant: variantFilter || undefined,
amount_min: activeFilters.amountMin > 0 ? activeFilters.amountMin : undefined,
amount_max: activeFilters.amountMax < 300000 ? activeFilters.amountMax : undefined,
date_from: activeFilters.dateFrom?.format("YYYY-MM-DD") || undefined,
date_to: activeFilters.dateTo?.format("YYYY-MM-DD") || undefined,
created_by: activeFilters.createdBy || undefined,
has_attachments: activeFilters.hasAttachments !== null ? activeFilters.hasAttachments : undefined,
};
Backend Changes:
-- Fuzzy creator search with case-insensitive matching
LOWER(JSON_UNQUOTE(JSON_EXTRACT(a.payload_json, '$.pa.applicant.contact.email'))) LIKE LOWER(:created_by)
Implementation Date: 2024-12-19
Bug Fix Date: 2024-12-19
Improvements Date: 2024-12-19
Status: ✅ Complete and Ready for Production
Major Feature Updates (2024-12-19)
1. Status Rename: "Neu" → "Beantragt"
Change: Updated status display from "Neu" to "Beantragt" across all interfaces.
Impact:
- More intuitive status name reflecting the actual state
- Updated in all dropdowns, filters, translations, and displays
- Backend database value remains "new" for compatibility
- Only display layer changed
2. In-Review Protection System
Feature: Applications in "In Prüfung" status are now protected from modifications.
Frontend Protection:
- Edit buttons disabled for in-review applications (even for admins)
- Edit page blocks access with informative message
- Cost position forms become read-only
- Attachment uploads prevented
Backend Protection:
PUT /applications/{pa_id}- Returns 403 for non-admin usersPOST /applications/{pa_id}/attachments- Blocks attachment uploadsPOST /applications/{pa_id}/costs/{index}/offers- Blocks comparison offersPUT /applications/{pa_id}/costs/{index}/justification- Blocks justification updates
Admin Override: Admins can still modify in-review applications through status changes.
3. Bulk Actions System
Feature: Comprehensive bulk operations for efficient application management.
UI Implementation:
- Checkbox column (leftmost) for individual selection
- "Select All" checkbox in table header with indeterminate state
- Floating action bar appears when items are selected
- Smooth slide-up animation with Material Design
Available Bulk Actions:
// Status Changes
bulkSetNew() // Set to "Beantragt"
bulkSetInReview() // Set to "In Prüfung"
bulkApprove() // Set to "Genehmigt"
bulkReject() // Set to "Abgelehnt"
bulkDelete() // Permanently delete applications
Action Bar Design:
- Fixed position at bottom center
- Primary color with contrast text
- Icon-based FAB buttons with tooltips
- Selection counter display
- Clear selection option
Backend Implementation:
@app.post("/admin/applications/bulk")
def bulk_operation(request: BulkOperationRequest):
# Supports: delete, approve, reject, set_in_review, set_new
# Returns: {success: [pa_ids], failed: [{pa_id, error}]}
Safety Features:
- Confirmation dialogs for all bulk actions
- Individual error handling per application
- Success/failure reporting
- Automatic list refresh after operations
- Transaction rollback on critical failures
Performance:
- Single API call for multiple operations
- Efficient database batch processing
- Optimistic UI updates
4. Enhanced Search & Filter Integration
Smart Quick Search:
- Now respects all active filters (status, variant, date ranges, etc.)
- Uses
searchApplicationsAdvanced()with complete parameter set - Maintains filter state while performing text search
Improved Creator Filter:
- Case-insensitive matching using SQL
LOWER() - Contains search instead of starts-with
- More intuitive user experience
Filter Reset Enhancement:
- "Alle Filter zurücksetzen" button below quick search
- Only appears when filters are active
- One-click reset for all filters including search text
Technical Architecture
Frontend State Management:
// New bulk action state
const [selectedApplications, setSelectedApplications] = useState<string[]>([]);
const [showBulkActions, setShowBulkActions] = useState(false);
// Bulk operation handlers
handleSelectApplication(paId: string, checked: boolean)
handleSelectAll(checked: boolean)
handleBulkDelete/Approve/Reject/SetInReview/SetNew()
API Integration:
// Enhanced API client
bulkOperation(params: {
pa_ids: string[];
operation: "delete" | "approve" | "reject" | "set_in_review" | "set_new";
})
// Smart search with filters
searchApplicationsAdvanced({
q: searchText,
status: statusFilter,
variant: variantFilter,
amount_min: activeFilters.amountMin,
// ... all other filters
})
Database Operations:
-- Bulk status updates
UPDATE applications SET status = :new_status WHERE pa_id IN (:pa_ids)
-- Bulk deletions with cascade
DELETE FROM application_attachments WHERE application_id IN (...)
DELETE FROM comparison_offers WHERE application_id IN (...)
DELETE FROM applications WHERE pa_id IN (:pa_ids)
User Experience Improvements
Workflow Enhancement:
- Efficient Selection: Click checkboxes or use "Select All"
- Bulk Processing: Single action affects multiple applications
- Immediate Feedback: Success/error messages with counts
- Status Protection: In-review applications safely locked
- Smart Search: Filters work together intelligently
Visual Design:
- Material Design compliance
- Smooth animations and transitions
- Clear visual feedback for selection states
- Intuitive icon usage with tooltips
- Responsive floating action bar
Migration Notes
Database: No schema changes required - only display layer updates API: New endpoints added, existing ones enhanced with protection Frontend: Backward compatible, progressive enhancement approach
Latest Security Enhancement (2024-12-19)
🔒 Complete Final Status Protection
Feature: Approved and rejected applications are now completely protected from modifications.
Problem Identified: Previously, admin users could still edit applications with "approved" or "rejected" status, which could lead to data integrity issues and confusion about finalized decisions.
Solution Implemented:
Frontend Protection:
- Edit buttons disabled for approved/rejected applications (including for admins)
- Edit page blocks access with status-specific error messages:
- "Genehmigte Anträge können nicht mehr bearbeitet werden."
- "Abgelehnte Anträge können nicht mehr bearbeitet werden."
- Cost position forms become read-only for approved/rejected status
- All modification UI elements disabled
Backend Protection (All Endpoints):
# Status check applied to all modification endpoints:
if app.status in ["in-review", "approved", "rejected"] and auth["scope"] != "master":
status_messages = {
"in-review": "Cannot update application while in review",
"approved": "Cannot update approved application",
"rejected": "Cannot update rejected application"
}
raise HTTPException(status_code=403, detail=status_messages[app.status])
Protected Endpoints:
PUT /applications/{pa_id}- Main application updatesPOST /applications/{pa_id}/attachments- Attachment uploadsDELETE /applications/{pa_id}/attachments/{id}- Attachment deletionsPOST /applications/{pa_id}/costs/{index}/offers- Comparison offersDELETE /applications/{pa_id}/costs/{index}/offers/{id}- Offer deletionsPUT /applications/{pa_id}/costs/{index}/justification- Cost justificationsPUT /applications/{pa_id}/costs/{index}/offers/{id}/preferred- Preferred offer selection
Status Flow Protection:
"new" (Beantragt) → ✅ Editable by user/admin
"in-review" (In Prüfung) → ❌ Read-only for everyone
"approved" (Genehmigt) → ❌ Read-only for everyone
"rejected" (Abgelehnt) → ❌ Read-only for everyone
Admin Override: Only status changes via bulk operations or individual status updates are allowed for admins. Content modifications are blocked for everyone.
Data Integrity Benefits:
- Prevents accidental modifications to finalized applications
- Ensures audit trail integrity
- Maintains consistency between approved documents and database
- Protects against inadvertent changes to rejected applications
Filter Bug Fix (2024-12-19)
🐛 Fixed: Anhang Filter Default Value Issue
Problem: The attachment filter was showing an inconsistent state - internally set to "Nein" (false) but displaying as if "Alle" was selected.
Root Causes:
- Undefined Value Handling: When
activeFilters.hasAttachmentswasundefined, it was not properly converted tonull(which represents "Alle") - Logic Error in API Parameters: The condition
filters.hasAttachments !== null || filters.hasAttachments !== undefinedwas alwaystrue, causing incorrect API calls - Display Logic Gap: FilterPopover only checked for
=== nullbut not=== undefinedwhen determining display value
Solutions Implemented:
Frontend State Management:
// Fixed: Use nullish coalescing to ensure proper default
hasAttachments: activeFilters.hasAttachments ?? null,
// Fixed: Correct API parameter logic
has_attachments: filters.hasAttachments !== null ? filters.hasAttachments : undefined,
FilterPopover Display Logic:
// Fixed: Handle both null and undefined as "all"
value={
filters.hasAttachments === null || filters.hasAttachments === undefined
? "all"
: String(filters.hasAttachments)
}
Component Initialization:
// Fixed: Ensure consistent initialization
const [filters, setFilters] = useState<FilterState>(() => ({
...defaultFilters,
...initialFilters,
hasAttachments: initialFilters?.hasAttachments ?? null,
}));
Impact:
- Attachment filter now correctly shows "Alle" by default
- Consistent behavior between UI display and internal state
- Proper API parameter handling for attachment filtering
- No more confusion about filter state
Technical Details:
- State Consistency:
null= "Alle",true= "Mit Anhängen",false= "Ohne Anhänge" - API Integration: Only sends
has_attachmentsparameter when not null - UI Synchronization: Display correctly reflects internal state
- Default Behavior: Clean slate shows all applications regardless of attachments
Implementation Date: 2024-12-19
Bug Fix Date: 2024-12-19
Improvements Date: 2024-12-19
Major Updates Date: 2024-12-19
Security Enhancement Date: 2024-12-19
Filter Fix Date: 2024-12-19
Status: ✅ Complete and Ready for Production