489 lines
17 KiB
Markdown
489 lines
17 KiB
Markdown
# 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` - Added `order_by` and `order` query parameters
|
|
- `GET /applications/search` - Added `order_by` and `order` query 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_amount` and `project_name` require post-processing due to JSON extraction
|
|
- Proper handling of NULL values
|
|
|
|
#### Frontend Changes
|
|
|
|
**Store Integration:**
|
|
- Added `sortBy` and `sortOrder` to application state
|
|
- Added `setSorting()` action to update sort parameters
|
|
- Automatic data refresh when sorting changes
|
|
|
|
**UI Components:**
|
|
- Replaced static table headers with `TableSortLabel` components
|
|
- Visual indicators for active sort column and direction
|
|
- Click handlers for all sortable columns
|
|
|
|
**API Client:**
|
|
- Extended `listApplicationsAdmin()` and `searchApplications()` 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:
|
|
|
|
1. Click any column header to sort by that field
|
|
2. Click again to reverse sort order
|
|
3. Switch between different columns while maintaining filter settings
|
|
4. Navigate pages while preserving sort order
|
|
5. 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:**
|
|
```typescript
|
|
// Sort by project name ascending
|
|
setSorting("project_name", "asc");
|
|
|
|
// Sort by amount descending
|
|
setSorting("total_amount", "desc");
|
|
```
|
|
|
|
**API call with sorting:**
|
|
```typescript
|
|
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_attachments` junction table
|
|
- Integrated attachment counting directly into the main query for better performance
|
|
- Added proper `has_attachments` filtering 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 of `searchApplications()`
|
|
- 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:**
|
|
```sql
|
|
JSON_EXTRACT(..., '$.pa.applicant.contact.email') LIKE :created_by
|
|
```
|
|
**After:**
|
|
```sql
|
|
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:**
|
|
```typescript
|
|
// 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:**
|
|
```sql
|
|
-- 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 users
|
|
- `POST /applications/{pa_id}/attachments` - Blocks attachment uploads
|
|
- `POST /applications/{pa_id}/costs/{index}/offers` - Blocks comparison offers
|
|
- `PUT /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:**
|
|
```typescript
|
|
// 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:**
|
|
```python
|
|
@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:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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:**
|
|
```sql
|
|
-- 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:**
|
|
1. **Efficient Selection:** Click checkboxes or use "Select All"
|
|
2. **Bulk Processing:** Single action affects multiple applications
|
|
3. **Immediate Feedback:** Success/error messages with counts
|
|
4. **Status Protection:** In-review applications safely locked
|
|
5. **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):**
|
|
```python
|
|
# 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 updates
|
|
- `POST /applications/{pa_id}/attachments` - Attachment uploads
|
|
- `DELETE /applications/{pa_id}/attachments/{id}` - Attachment deletions
|
|
- `POST /applications/{pa_id}/costs/{index}/offers` - Comparison offers
|
|
- `DELETE /applications/{pa_id}/costs/{index}/offers/{id}` - Offer deletions
|
|
- `PUT /applications/{pa_id}/costs/{index}/justification` - Cost justifications
|
|
- `PUT /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:**
|
|
1. **Undefined Value Handling:** When `activeFilters.hasAttachments` was `undefined`, it was not properly converted to `null` (which represents "Alle")
|
|
2. **Logic Error in API Parameters:** The condition `filters.hasAttachments !== null || filters.hasAttachments !== undefined` was always `true`, causing incorrect API calls
|
|
3. **Display Logic Gap:** FilterPopover only checked for `=== null` but not `=== undefined` when determining display value
|
|
|
|
**Solutions Implemented:**
|
|
|
|
**Frontend State Management:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// Fixed: Handle both null and undefined as "all"
|
|
value={
|
|
filters.hasAttachments === null || filters.hasAttachments === undefined
|
|
? "all"
|
|
: String(filters.hasAttachments)
|
|
}
|
|
```
|
|
|
|
**Component Initialization:**
|
|
```typescript
|
|
// 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_attachments` parameter 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 |