diff --git a/app/api/v1/admin/settings.py b/app/api/v1/admin/settings.py index 0ec2ae52..d79594fc 100644 --- a/app/api/v1/admin/settings.py +++ b/app/api/v1/admin/settings.py @@ -24,6 +24,9 @@ from models.schema.admin import ( AdminSettingListResponse, AdminSettingResponse, AdminSettingUpdate, + PublicDisplaySettingsResponse, + RowsPerPageResponse, + RowsPerPageUpdateResponse, ) router = APIRouter(prefix="/settings") @@ -173,6 +176,81 @@ def upsert_setting( return result +# ============================================================================ +# CONVENIENCE ENDPOINTS FOR COMMON SETTINGS +# ============================================================================ + + +@router.get("/display/rows-per-page", response_model=RowsPerPageResponse) +def get_rows_per_page( + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_api), +) -> RowsPerPageResponse: + """Get the platform-wide rows per page setting.""" + value = admin_settings_service.get_setting_value(db, "rows_per_page", default="20") + return RowsPerPageResponse(rows_per_page=int(value)) + + +@router.put("/display/rows-per-page", response_model=RowsPerPageUpdateResponse) +def set_rows_per_page( + rows: int = Query(..., ge=10, le=100, description="Rows per page (10-100)"), + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_api), +) -> RowsPerPageUpdateResponse: + """ + Set the platform-wide rows per page setting. + + Valid values: 10, 20, 50, 100 + """ + valid_values = [10, 20, 50, 100] + if rows not in valid_values: + # Round to nearest valid value + rows = min(valid_values, key=lambda x: abs(x - rows)) + + setting_data = AdminSettingCreate( + key="rows_per_page", + value=str(rows), + value_type="integer", + category="display", + description="Default number of rows per page in admin tables", + is_public=True, + ) + + admin_settings_service.upsert_setting( + db=db, setting_data=setting_data, admin_user_id=current_admin.id + ) + + admin_audit_service.log_action( + db=db, + admin_user_id=current_admin.id, + action="update_setting", + target_type="setting", + target_id="rows_per_page", + details={"value": rows}, + ) + db.commit() + + return RowsPerPageUpdateResponse( + rows_per_page=rows, message="Rows per page setting updated" + ) + + +@router.get("/display/public", response_model=PublicDisplaySettingsResponse) +def get_public_display_settings( + db: Session = Depends(get_db), +) -> PublicDisplaySettingsResponse: + """ + Get public display settings (no auth required). + + Returns settings that can be used by frontend without admin auth. + """ + rows_per_page = admin_settings_service.get_setting_value( + db, "rows_per_page", default="20" + ) + + return PublicDisplaySettingsResponse(rows_per_page=int(rows_per_page)) + + @router.delete("/{key}") def delete_setting( key: str, diff --git a/app/templates/shared/macros/tables.html b/app/templates/shared/macros/tables.html index 0af93422..f1af74a0 100644 --- a/app/templates/shared/macros/tables.html +++ b/app/templates/shared/macros/tables.html @@ -283,3 +283,168 @@ {% endmacro %} + + +{# + Numbered Pagination + =================== + A numbered pagination component with first/prev/pages/next/last buttons. + + Parameters: + - page_var: Alpine.js variable for current page (default: 'page') + - total_var: Alpine.js variable for total items (default: 'total') + - limit_var: Alpine.js variable for items per page (default: 'limit') + - on_change: Alpine.js function to call on page change (default: 'loadItems()') + - show_rows_per_page: Whether to show rows per page selector (default: false) + - rows_options: List of rows per page options (default: [10, 20, 50, 100]) + + Required Alpine.js: + page: 1, + total: 0, + limit: 20, + get totalPages() { return Math.ceil(this.total / this.limit); }, + getPageNumbers() { + // Returns array of page numbers to display + const total = this.totalPages; + const current = this.page; + const maxVisible = 5; + + if (total <= maxVisible) { + return Array.from({length: total}, (_, i) => i + 1); + } + + const half = Math.floor(maxVisible / 2); + let start = Math.max(1, current - half); + let end = Math.min(total, start + maxVisible - 1); + + if (end - start < maxVisible - 1) { + start = Math.max(1, end - maxVisible + 1); + } + + return Array.from({length: end - start + 1}, (_, i) => start + i); + }, + goToPage(p) { + this.page = p; + this.loadItems(); + } +#} +{% macro numbered_pagination(page_var='page', total_var='total', limit_var='limit', on_change='loadItems()', show_rows_per_page=false, rows_options=[10, 20, 50, 100]) %} +
+ {# Info text #} + + Showing + - + of items + + + {# Pagination controls #} +
+ {# First page #} + + + {# Previous page #} + + + {# Page numbers #} + + + {# Next page #} + + + {# Last page #} + +
+ + {% if show_rows_per_page %} + {# Rows per page selector #} +
+ + +
+ {% endif %} +
+{% endmacro %} + + +{# + Simple Pagination + ================= + A simpler prev/next pagination without page numbers. + Use this for quick implementation or when numbered pagination isn't needed. + + Parameters: + - page_var: Alpine.js variable for current page (default: 'page') + - total_var: Alpine.js variable for total items (default: 'total') + - limit_var: Alpine.js variable for items per page (default: 'limit') + - on_change: Alpine.js function to call on page change (default: 'loadItems()') +#} +{% macro simple_pagination(page_var='page', total_var='total', limit_var='limit', on_change='loadItems()') %} +
+ + Showing + - + of + +
+ + +
+
+{% endmacro %} diff --git a/models/schema/admin.py b/models/schema/admin.py index 4f9b3586..81b58778 100644 --- a/models/schema/admin.py +++ b/models/schema/admin.py @@ -187,6 +187,30 @@ class AdminSettingListResponse(BaseModel): category: str | None = None +# ============================================================================ +# DISPLAY SETTINGS SCHEMAS +# ============================================================================ + + +class RowsPerPageResponse(BaseModel): + """Response for rows per page setting.""" + + rows_per_page: int + + +class RowsPerPageUpdateResponse(BaseModel): + """Response after updating rows per page.""" + + rows_per_page: int + message: str + + +class PublicDisplaySettingsResponse(BaseModel): + """Public display settings (no auth required).""" + + rows_per_page: int + + # ============================================================================ # PLATFORM ALERT SCHEMAS # ============================================================================ diff --git a/static/shared/js/utils.js b/static/shared/js/utils.js index 48de6b4a..7988f2a7 100644 --- a/static/shared/js/utils.js +++ b/static/shared/js/utils.js @@ -184,7 +184,86 @@ const Utils = { } }; +// ============================================================================ +// Platform Settings +// ============================================================================ + +/** + * Platform settings cache and loader + */ +const PlatformSettings = { + _settings: null, + _loading: false, + _loadPromise: null, + + /** + * Load platform display settings from API + * @returns {Promise} Platform settings + */ + async load() { + // Return cached settings if available + if (this._settings) { + return this._settings; + } + + // Return existing promise if already loading + if (this._loadPromise) { + return this._loadPromise; + } + + this._loading = true; + this._loadPromise = (async () => { + try { + const response = await fetch('/api/v1/admin/settings/display/public'); + if (response.ok) { + this._settings = await response.json(); + } else { + // Default settings + this._settings = { rows_per_page: 20 }; + } + } catch (error) { + console.warn('Failed to load platform settings, using defaults:', error); + this._settings = { rows_per_page: 20 }; + } + this._loading = false; + return this._settings; + })(); + + return this._loadPromise; + }, + + /** + * Get rows per page setting + * @returns {number} Rows per page (default: 20) + */ + getRowsPerPage() { + return this._settings?.rows_per_page || 20; + }, + + /** + * Get rows per page synchronously (returns cached or default) + * @returns {number} Rows per page + */ + get rowsPerPage() { + return this._settings?.rows_per_page || 20; + }, + + /** + * Reset cached settings (call after updating settings) + */ + reset() { + this._settings = null; + this._loadPromise = null; + } +}; + +// Load settings on page load +document.addEventListener('DOMContentLoaded', () => { + PlatformSettings.load(); +}); + // Make available globally +window.PlatformSettings = PlatformSettings; window.Utils = Utils; // Export for modules