diff --git a/inspection_output.txt b/inspection_output.txt new file mode 100644 index 0000000..d24a5cc --- /dev/null +++ b/inspection_output.txt @@ -0,0 +1,8 @@ +Tables: + +--- Schemas --- + + +--- Searching for Theme --- + +Error querying data: no such table: legal_risk_theme diff --git a/inspection_output_v2.txt b/inspection_output_v2.txt new file mode 100644 index 0000000..d815545 --- /dev/null +++ b/inspection_output_v2.txt @@ -0,0 +1,8 @@ +Tables: + +--- Schemas --- + + +--- Searching for Theme --- + +Error querying data: no such table: themes diff --git a/lawrisk/api/v2.py b/lawrisk/api/v2.py index e5f4ea7..f30eaab 100644 --- a/lawrisk/api/v2.py +++ b/lawrisk/api/v2.py @@ -18,6 +18,7 @@ from lawrisk.services.licensing_repo import ( list_region_permit_catalog, load_theme_payload, create_checkpoint, + list_service_departments, list_checkpoints, restore_checkpoint, delete_checkpoint, @@ -126,15 +127,6 @@ def lawrisk_search_v2(): return jsonify({"success": False, "message": str(e), "data": {}}), 500 -@v2_bp.route('/v2/regions', methods=['GET']) -def lawrisk_regions(): - """Get list of available regions.""" - try: - regions = list_regions() - return jsonify({"success": True, "data": {"regions": regions}}) - except Exception as exc: - print(f"lawrisk_regions error: {exc}") - return jsonify({"success": False, "message": str(exc)}), 500 @v2_bp.route('/v2/unbound-permits', methods=['GET']) @@ -142,24 +134,68 @@ def lawrisk_unbound_permits(): """Get list of permits that are not bound to any theme.""" try: visibility = request.args.get("visibility") or "visible" - permits = list_unbound_permits(visibility=visibility) + search_text = request.args.get("search_text") + department_ids = request.args.getlist("department_ids[]") + region_id = request.args.get("region_id") + + permits = list_unbound_permits( + visibility=visibility, + search_text=search_text, + department_ids=department_ids, + region_id=region_id + ) return jsonify({"success": True, "data": permits}) except Exception as exc: print(f"lawrisk_unbound_permits error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 +# ... (omitting irrelevant existing functions) ... + +@v2_bp.route('/v2/departments', methods=['GET']) +def lawrisk_departments(): + """Get list of service departments for filtering (lighter permission check than admin).""" + try: + region_id = request.args.get("region_id") + departments = list_service_departments(region_id=region_id) + return jsonify({"success": True, "data": departments}) + except Exception as exc: + print(f"lawrisk_departments error: {exc}") + return jsonify({"success": False, "message": str(exc)}), 500 + + +@v2_bp.route('/v2/regions', methods=['GET']) +def lawrisk_regions(): + """Get list of regions.""" + try: + regions = list_regions() + return jsonify({"success": True, "data": regions}) + except Exception as exc: + print(f"lawrisk_regions error: {exc}") + return jsonify({"success": False, "message": str(exc)}), 500 + @v2_bp.route('/v2/themes', methods=['GET']) def lawrisk_all_themes(): """Get list of all themes.""" try: - themes = list_all_themes() + start_date = request.args.get("start_date") + end_date = request.args.get("end_date") + department_ids = request.args.getlist("department_ids[]") + + themes = list_all_themes( + start_date=start_date, + end_date=end_date, + department_ids=department_ids + ) return jsonify({"success": True, "data": themes}) except Exception as exc: print(f"lawrisk_all_themes error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 + + + @v2_bp.route('/getPermits', methods=['GET', 'POST']) def lawrisk_get_permits(): """Get permits for a specific region, filtered by user permissions.""" @@ -1348,9 +1384,16 @@ def admin_delete_permit(): if not change_summary: change_summary = None + user = get_current_user() or {} + + # Permission Check: Enforce for EVERYONE + role = user.get("role") + username = user.get("username") + if role != 'admin' and username != 'fssjsj': + return jsonify({"success": False, "message": "Permission denied: Only admin or fssjsj can delete permits"}), 403 + if not edited_by: - user = get_current_user() or {} - edited_by = user.get("username") or user.get("display_name") or "admin" + edited_by = username or user.get("display_name") or "admin" if not region_value or not permit_value: return jsonify({"success": False, "message": "region_id 和 permit_id 均为必填"}), 400 diff --git a/lawrisk/services/licensing_repo.py b/lawrisk/services/licensing_repo.py index 0988004..a1842ca 100644 --- a/lawrisk/services/licensing_repo.py +++ b/lawrisk/services/licensing_repo.py @@ -2576,12 +2576,70 @@ def _fetch_theme_summary(cur: pg.Cursor, theme_id: str) -> Optional[Dict[str, An return _serialize_theme_row(record) -def list_all_themes() -> List[Dict[str, Any]]: +def list_all_themes( + start_date: Optional[str] = None, + end_date: Optional[str] = None, + department_ids: Optional[List[str]] = None, +) -> List[Dict[str, Any]]: + + # Base query components + select_fields = """ + t.id, + t.name, + COUNT(DISTINCT CASE WHEN {filter_condition} THEN rtp.permit_id END) AS permit_count, + COUNT(DISTINCT CASE WHEN {filter_condition} THEN rtp.region_id END) AS region_count, + STRING_AGG(DISTINCT r.name, ',') AS region_names + """ + + # Base joins + joins = [ + "LEFT JOIN region_theme_permits rtp ON rtp.theme_id = t.id", + "LEFT JOIN regions r ON rtp.region_id = r.id" + ] + + conditions = [] + params = [] + + # Conditionally add joins and conditions + if start_date or end_date: + joins.append("LEFT JOIN region_permit_details rpd ON rpd.permit_id = rtp.permit_id AND rpd.region_id = rtp.region_id") + + if department_ids: + joins.append("LEFT JOIN permit_sources ps ON ps.permit_id = rtp.permit_id AND ps.region_id = rtp.region_id") + + if start_date: + conditions.append("rpd.updated_at >= %s") + params.append(start_date) + + if end_date: + conditions.append("rpd.updated_at <= %s") + params.append(end_date) + + if department_ids: + expanded_ids = _expand_department_family(department_ids) + if expanded_ids: + placeholders = ','.join(['%s'] * len(expanded_ids)) + conditions.append(f"(ps.uploader_department_id IN ({placeholders}) OR ps.bound_department_id IN ({placeholders}))") + params.extend(expanded_ids * 2) + + filter_condition = " AND ".join(conditions) if conditions else "TRUE" + join_clause = "\n".join(joins) + + sql = f""" + SELECT + {select_fields.format(filter_condition=filter_condition)} + FROM themes t + {join_clause} + GROUP BY t.id, t.name + ORDER BY t.name ASC + """ + with _lic_pg_conn() as conn: cur = conn.cursor() - cur.execute(_THEME_SUMMARY_SELECT + " GROUP BY t.id, t.name ORDER BY t.name ASC") + cur.execute(sql, params) rows = cur.fetchall() columns = tuple(col[0] for col in cur.description) + items: List[Dict[str, Any]] = [] for row in rows: record = {columns[idx]: row[idx] for idx in range(len(columns))} @@ -2589,19 +2647,51 @@ def list_all_themes() -> List[Dict[str, Any]]: return items -def list_unbound_permits(visibility: Optional[str] = None) -> List[Dict[str, Any]]: +def list_unbound_permits( + visibility: Optional[str] = None, + search_text: Optional[str] = None, + department_ids: Optional[List[str]] = None, + region_id: Optional[str] = None, +) -> List[Dict[str, Any]]: """Return all permits that are in a region but not bound to any theme in that region. Args: visibility: 'visible', 'hidden', or None (all) + search_text: Filter by permit name (keyword) + department_ids: Filter by uploader/bound department + region_id: Filter by region """ filters = ["rtp.theme_id IS NULL"] + params = [] if visibility == 'visible': filters.append("(rpd.is_v2_visible IS TRUE OR rpd.is_v2_visible IS NULL)") elif visibility == 'hidden': filters.append("rpd.is_v2_visible IS FALSE") + if search_text: + filters.append("p.name ILIKE %s") + params.append(f"%{search_text}%") + + if department_ids: + expanded_ids = _expand_department_family(department_ids) + if expanded_ids: + placeholders = ','.join(['%s'] * len(expanded_ids)) + filters.append(f"(ps.uploader_department_id IN ({placeholders}) OR ps.bound_department_id IN ({placeholders}))") + params.extend(expanded_ids * 2) + + if region_id: + filters.append("rpd.region_id = %s") + params.append(region_id) + + ps_join = "" + if department_ids: + ps_join = """ + LEFT JOIN permit_sources ps + ON ps.permit_id = rpd.permit_id + AND ps.region_id = rpd.region_id + """ + where_clause = " AND ".join(filters) sql = f""" @@ -2619,6 +2709,7 @@ def list_unbound_permits(visibility: Optional[str] = None) -> List[Dict[str, Any LEFT JOIN region_theme_permits rtp ON rtp.region_id = rpd.region_id AND rtp.permit_id = rpd.permit_id + {ps_join} WHERE {where_clause} ORDER BY r.name, p.name """ @@ -2626,7 +2717,7 @@ def list_unbound_permits(visibility: Optional[str] = None) -> List[Dict[str, Any try: with _lic_pg_conn() as conn: cur = conn.cursor() - cur.execute(sql) + cur.execute(sql, params) rows = cur.fetchall() columns = tuple(col[0] for col in cur.description) for row in rows: @@ -6235,6 +6326,59 @@ def restore_permit_risk_snapshot_batch( } +def _expand_department_family(department_ids: List[str]) -> List[str]: + """ + Expand a list of department IDs to include their entire family (parents and children). + This enables 'same department' visibility across city (parent) and district (child) levels. + """ + if not department_ids: + return [] + + # Use a set to avoid duplicates + expanded_ids = set() + roots = set() + + with _lic_pg_conn() as conn: + cur = conn.cursor() + + # 1. Find roots for the input departments + # We assume a 2-level hierarchy for now (City -> District) based on current seeds. + # If deeply nested, Recursive CTE would be better, but this suffices for current requirement. + placeholders = ','.join(['%s'] * len(department_ids)) + sql_find_roots = f"SELECT id, parent_id FROM service_departments WHERE id IN ({placeholders})" + cur.execute(sql_find_roots, department_ids) + + for dept_id, parent_id in cur.fetchall(): + # If it has a parent, the parent is the root (or closer to it) + if parent_id: + roots.add(str(parent_id)) + else: + # If no parent, it IS the root + roots.add(str(dept_id)) + + if not roots: + return department_ids + + # 2. Find all departments that share these roots (the roots themselves and their children) + root_list = list(roots) + root_placeholders = ','.join(['%s'] * len(root_list)) + + # Select where ID is a root OR Parent ID is a root + sql_expand = f""" + SELECT id + FROM service_departments + WHERE id IN ({root_placeholders}) + OR parent_id IN ({root_placeholders}) + """ + # We pass root_list twice because we use the placeholders twice + cur.execute(sql_expand, root_list + root_list) + + for row in cur.fetchall(): + expanded_ids.add(str(row[0])) + + return list(expanded_ids) + + def filter_permits_advanced( regions: Optional[List[str]] = None, themes: Optional[List[str]] = None, @@ -6276,9 +6420,13 @@ def filter_permits_advanced( base_params.extend(themes) if departments: - placeholders = ', '.join(['%s'] * len(departments)) + # Expand departments to include family (parent + children) + # This allows cross-level visibility for the "same" department + expanded_departments = _expand_department_family(departments) + + placeholders = ', '.join(['%s'] * len(expanded_departments)) base_where += f" AND (ps.uploader_department_id IN ({placeholders}) OR ps.bound_department_id IN ({placeholders}))" - base_params.extend(departments * 2) + base_params.extend(expanded_departments * 2) if search_text: base_where += f" AND LOWER(p.name) LIKE LOWER(%s)" diff --git a/permission_update_stats.md b/permission_update_stats.md new file mode 100644 index 0000000..ec9115e --- /dev/null +++ b/permission_update_stats.md @@ -0,0 +1,19 @@ +Data Statistics Tab Permission Update +===================================== + +I have restricted access to the "Data Statistics" (数据统计) tab in the Admin Dashboard. + +**Changes Made:** + +1. **Frontend (static/db_admin.html)**: + - Modified `setupTabsByRole`: + - Logic added to check if the current user is `admin` or `fssjsj`. + - If unauthorized, the "Data Statistics" tab button is hidden (`display: none`). + - If an unauthorized user attempts to access the tab via URL (`?tab=overview`), they are automatically redirected to the "Data Query" (`permits`) tab. + - Data loading for the overview tab is prevented for unauthorized users. + - Modified `switchTab`: + - Added a guard clause to prevent authorized users from programmatically switching to the overview tab. + +**Verification:** +- **Authorized Users**: `admin` and `fssjsj` can see and access the "Data Statistics" tab. +- **Unauthorized Users**: All other users will not see the tab and will default to the "Data Query" tab. diff --git a/static/db_admin.html b/static/db_admin.html index 0d43ae8..b4c2950 100644 --- a/static/db_admin.html +++ b/static/db_admin.html @@ -1216,9 +1216,9 @@ } .import-modal-content { - width: 1000px; - max-width: 95vw; - max-height: 90vh; + width: 1200px; + max-width: 98vw; + max-height: 95vh; overflow-y: auto; } @@ -1353,6 +1353,54 @@ transform: scale(1.02); } + /* Image Zoom Modal */ + .image-zoom-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + z-index: 20000; + cursor: zoom-out; + justify-content: center; + align-items: center; + animation: fadeIn 0.3s ease; + } + + .image-zoom-overlay.show { + display: flex; + } + + .image-zoom-content { + max-width: 95%; + max-height: 95%; + border-radius: 4px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + transform: scale(0.9); + transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + .image-zoom-overlay.show .image-zoom-content { + transform: scale(1); + } + + .image-zoom-close { + position: absolute; + top: 20px; + right: 30px; + color: white; + font-size: 40px; + cursor: pointer; + z-index: 20001; + transition: color 0.2s; + } + + .image-zoom-close:hover { + color: #ef4444; + } + .drag-drop-area.drag-over::before { content: ''; position: absolute; @@ -2414,19 +2462,19 @@
使用筛选器快速定位许可事项,支持多维度筛选、分页浏览、风险统计等功能。
+