stupa-pdf-api/docs/features/ADMIN_TABLE_SORTING.md

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 - 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:

// 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_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:

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 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:

// 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:

  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):

# 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:

// 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_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