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]) %} +