feat: add visibility filter for permit management and fix V2 visibility logic
- Added 'Enabled Status' dropdown filter to db_admin.html for filtering permits by visibility (visible/hidden/all) - Updated admin_permits_advanced_filter API to accept and process visibility parameter - Modified filter_permits_advanced in licensing_repo.py to filter by is_v2_visible column - Fixed role-based access control to allow department_admin to toggle permit visibility - Improved parameter parsing in API endpoints for more robust handling
This commit is contained in:
parent
b532c46dc1
commit
c55170208b
|
|
@ -922,6 +922,38 @@ def admin_permits():
|
|||
return jsonify({"success": False, "message": str(exc)}), 500
|
||||
|
||||
|
||||
@v2_bp.route('/admin/permits/visibility', methods=['POST'])
|
||||
def admin_toggle_permit_visibility():
|
||||
"""Toggle the visibility of a permit in V2 API retrieval."""
|
||||
admin_user, error = _admin_guard(prefer_json=True, roles=("admin", "department_admin"))
|
||||
if error:
|
||||
return error
|
||||
|
||||
data = request.get_json() or {}
|
||||
region_id = data.get("region_id")
|
||||
permit_id = data.get("permit_id")
|
||||
is_visible = data.get("is_v2_visible")
|
||||
|
||||
if not region_id or not permit_id or is_visible is None:
|
||||
return jsonify({"success": False, "message": "region_id, permit_id and is_v2_visible are required"}), 400
|
||||
|
||||
try:
|
||||
from lawrisk.services.licensing_repo import update_permit_v2_visibility
|
||||
operator = (admin_user or {}).get("username") or "admin"
|
||||
success = update_permit_v2_visibility(
|
||||
region_id=region_id,
|
||||
permit_id=permit_id,
|
||||
is_visible=bool(is_visible),
|
||||
operator=operator
|
||||
)
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "Visibility updated"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Permit details not found or update failed"}), 404
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "message": str(exc)}), 500
|
||||
|
||||
|
||||
@v2_bp.route('/admin/permit-import/upload', methods=['POST'])
|
||||
def admin_permit_import_upload():
|
||||
"""Upload Excel workbook and start an import session."""
|
||||
|
|
@ -1596,70 +1628,46 @@ def admin_permits_advanced_filter():
|
|||
"""
|
||||
try:
|
||||
# Parse parameters from query string or request body
|
||||
# Parse parameters from query/body in a more unified way
|
||||
if request.method == 'GET':
|
||||
regions = request.args.getlist('regions[]') or request.args.getlist('region')
|
||||
themes = request.args.getlist('themes[]') or request.args.getlist('theme')
|
||||
departments = request.args.getlist('departments[]') or request.args.getlist('department')
|
||||
search_text = request.args.get('search_text') or request.args.get('q')
|
||||
try:
|
||||
limit = int(request.args.get('limit', '100'))
|
||||
except (TypeError, ValueError):
|
||||
limit = 100
|
||||
try:
|
||||
offset = int(request.args.get('offset', '0'))
|
||||
except (TypeError, ValueError):
|
||||
offset = 0
|
||||
visibility = request.args.get('visibility')
|
||||
limit = request.args.get('limit', '100')
|
||||
offset = request.args.get('offset', '0')
|
||||
else:
|
||||
if request.is_json:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
else:
|
||||
payload = request.form.to_dict(flat=True) if request.form else {}
|
||||
|
||||
# Handle array parameters
|
||||
payload = request.get_json(silent=True) or request.form
|
||||
regions = payload.getlist('regions[]') if hasattr(payload, 'getlist') else payload.get('regions', [])
|
||||
if isinstance(regions, str):
|
||||
regions = [regions]
|
||||
regions = regions or payload.getlist('region') if hasattr(payload, 'getlist') else payload.get('region', [])
|
||||
if isinstance(regions, str):
|
||||
regions = [regions]
|
||||
|
||||
themes = payload.getlist('themes[]') if hasattr(payload, 'getlist') else payload.get('themes', [])
|
||||
if isinstance(themes, str):
|
||||
themes = [themes]
|
||||
themes = themes or payload.getlist('theme') if hasattr(payload, 'getlist') else payload.get('theme', [])
|
||||
if isinstance(themes, str):
|
||||
themes = [themes]
|
||||
|
||||
departments = payload.getlist('departments[]') if hasattr(payload, 'getlist') else payload.get('departments', [])
|
||||
if isinstance(departments, str):
|
||||
departments = [departments]
|
||||
departments = departments or payload.getlist('department') if hasattr(payload, 'getlist') else payload.get('department', [])
|
||||
if isinstance(departments, str):
|
||||
departments = [departments]
|
||||
|
||||
search_text = payload.get('search_text') or payload.get('q')
|
||||
try:
|
||||
limit = int(payload.get('limit', '100'))
|
||||
except (TypeError, ValueError):
|
||||
limit = 100
|
||||
try:
|
||||
offset = int(payload.get('offset', '0'))
|
||||
except (TypeError, ValueError):
|
||||
offset = 0
|
||||
visibility = payload.get('visibility')
|
||||
limit = payload.get('limit', '100')
|
||||
offset = payload.get('offset', '0')
|
||||
|
||||
# Normalize parameters - convert to lists if not already
|
||||
if isinstance(regions, str):
|
||||
regions = [regions]
|
||||
if isinstance(themes, str):
|
||||
themes = [themes]
|
||||
if isinstance(departments, str):
|
||||
departments = [departments]
|
||||
|
||||
# Filter out empty values
|
||||
# Normalize parameters
|
||||
if isinstance(regions, str): regions = [regions]
|
||||
if isinstance(themes, str): themes = [themes]
|
||||
if isinstance(departments, str): departments = [departments]
|
||||
|
||||
regions = [r.strip() for r in regions if r and r.strip()] if regions else None
|
||||
themes = [t.strip() for t in themes if t and t.strip()] if themes else None
|
||||
departments = [d.strip() for d in departments if d and d.strip()] if departments else None
|
||||
search_text = search_text.strip() if search_text else None
|
||||
search_text = (search_text or "").strip() or None
|
||||
visibility = (visibility or "").strip().lower() or None
|
||||
|
||||
try:
|
||||
limit = int(limit)
|
||||
except (ValueError, TypeError):
|
||||
limit = 100
|
||||
try:
|
||||
offset = int(offset)
|
||||
except (ValueError, TypeError):
|
||||
offset = 0
|
||||
|
||||
print(f"[DEBUG] admin_permits_advanced_filter params: search={search_text}, visibility={visibility}, regions={regions}")
|
||||
|
||||
# Execute filtering
|
||||
result = filter_permits_advanced(
|
||||
|
|
@ -1667,6 +1675,7 @@ def admin_permits_advanced_filter():
|
|||
themes=themes,
|
||||
departments=departments,
|
||||
search_text=search_text,
|
||||
visibility=visibility,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from lawrisk.services.licensing_repo import (
|
|||
load_theme_payload,
|
||||
load_permits_and_risks,
|
||||
find_permit_contexts_by_name,
|
||||
_ensure_v2_visibility_column,
|
||||
)
|
||||
from lawrisk.services.lawrisk_service import ChatClient
|
||||
|
||||
|
|
@ -200,18 +201,21 @@ def _get_preset_questions_pool() -> List[str]:
|
|||
"""
|
||||
from lawrisk.services.licensing_repo import _lic_pg_conn
|
||||
|
||||
# Query themes that have at least one permit
|
||||
# Query themes that have at least one visible permit
|
||||
sql = """
|
||||
SELECT DISTINCT t.name AS theme_name
|
||||
FROM themes t
|
||||
JOIN region_theme_permits rtp ON rtp.theme_id = t.id
|
||||
JOIN region_permit_details rpd ON rpd.region_id = rtp.region_id AND rpd.permit_id = rtp.permit_id
|
||||
WHERE t.name NOT IN ('不涉及', '无', '所有主题事项')
|
||||
AND COALESCE(rpd.is_v2_visible, true) = true
|
||||
ORDER BY t.name
|
||||
"""
|
||||
|
||||
questions: List[str] = []
|
||||
try:
|
||||
with _lic_pg_conn() as conn:
|
||||
_ensure_v2_visibility_column(conn)
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql)
|
||||
for (theme_name,) in cur.fetchall():
|
||||
|
|
@ -313,6 +317,7 @@ def search_v2(
|
|||
ctx["region_id"],
|
||||
ctx["theme_id"],
|
||||
permit_id=ctx["permit_id"],
|
||||
only_visible=True,
|
||||
)
|
||||
if not permits:
|
||||
continue
|
||||
|
|
@ -368,7 +373,7 @@ def search_v2(
|
|||
if ":" not in option_id:
|
||||
continue
|
||||
region_id, theme_id = option_id.split(":", 1)
|
||||
payload = load_theme_payload(region_id, theme_id)
|
||||
payload = load_theme_payload(region_id, theme_id, only_visible=True)
|
||||
|
||||
# Sanitize permits for V2 API (V2 should only expose external contact info)
|
||||
for permit in payload.get("permits", []):
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ _PERMIT_THEME_OVERRIDE_SCHEMA_LOCK = threading.Lock()
|
|||
_PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_READY: Optional[bool] = None
|
||||
_PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_LOCK = threading.Lock()
|
||||
|
||||
_V2_VISIBILITY_SCHEMA_READY: Optional[bool] = None
|
||||
_V2_VISIBILITY_SCHEMA_LOCK = threading.Lock()
|
||||
|
||||
_IMPORT_HEADER_ALIASES: Dict[str, Set[str]] = {
|
||||
"permit_name": {
|
||||
"许可事项",
|
||||
|
|
@ -3200,6 +3203,36 @@ def _fetch_permit_all_theme_flags(
|
|||
return {str(permit_id): True for (permit_id,) in rows}
|
||||
|
||||
|
||||
def update_permit_v2_visibility(
|
||||
region_id: str, permit_id: str, is_visible: bool, operator: str = "admin"
|
||||
) -> bool:
|
||||
"""Toggle the visibility of a permit in V2 API retrieval for a specific region."""
|
||||
with _lic_pg_conn() as conn:
|
||||
_ensure_v2_visibility_column(conn)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE region_permit_details
|
||||
SET is_v2_visible = %s, updated_at = now()
|
||||
WHERE region_id = %s AND permit_id = %s
|
||||
""",
|
||||
(is_visible, region_id, permit_id),
|
||||
)
|
||||
success = cur.rowcount > 0
|
||||
if success:
|
||||
conn.commit()
|
||||
log_operation(
|
||||
operator=operator,
|
||||
operation_type="UPDATE",
|
||||
target_type="PERMIT_VISIBILITY",
|
||||
target_id=permit_id,
|
||||
target_name=f"Visibility set to {is_visible}",
|
||||
change_summary=f"Updated v2_visibility for permit {permit_id} in region {region_id} to {is_visible}",
|
||||
details={"region_id": region_id, "permit_id": permit_id, "is_v2_visible": is_visible},
|
||||
)
|
||||
return success
|
||||
|
||||
|
||||
def _permit_binds_all_themes(conn: pg.Connection, region_id: str, permit_id: str) -> bool:
|
||||
"""Check override flag for a single region-permit pair."""
|
||||
global _PERMIT_THEME_OVERRIDE_SCHEMA_READY
|
||||
|
|
@ -3351,10 +3384,17 @@ def list_region_theme_options() -> List[Dict[str, str]]:
|
|||
FROM region_themes rt
|
||||
JOIN regions r ON r.id = rt.region_id
|
||||
JOIN themes t ON t.id = rt.theme_id
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM region_theme_permits rtp
|
||||
JOIN region_permit_details rpd ON rpd.region_id = rtp.region_id AND rpd.permit_id = rtp.permit_id
|
||||
WHERE rtp.region_id = rt.region_id AND rtp.theme_id = rt.theme_id
|
||||
AND COALESCE(rpd.is_v2_visible, true) = true
|
||||
)
|
||||
ORDER BY r.name, t.name
|
||||
"""
|
||||
out: List[Dict[str, str]] = []
|
||||
with _lic_pg_conn() as conn:
|
||||
_ensure_v2_visibility_column(conn)
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql)
|
||||
for region_id, region_name, theme_id, theme_name in cur.fetchall():
|
||||
|
|
@ -3528,10 +3568,12 @@ def list_region_permit_catalog(region_id: str) -> List[Dict[str, Any]]:
|
|||
p.name AS permit_name,
|
||||
rtp.theme_id,
|
||||
COALESCE(t.name, '') AS theme_name,
|
||||
COUNT(rpr.risk_id) OVER (PARTITION BY rtp.permit_id) AS risk_total
|
||||
COUNT(rpr.risk_id) OVER (PARTITION BY rtp.permit_id) AS risk_total,
|
||||
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
|
||||
FROM region_theme_permits rtp
|
||||
JOIN permits p ON p.id = rtp.permit_id
|
||||
LEFT JOIN themes t ON t.id = rtp.theme_id
|
||||
LEFT JOIN region_permit_details rpd ON rpd.region_id = rtp.region_id AND rpd.permit_id = rtp.permit_id
|
||||
LEFT JOIN region_permit_risks rpr
|
||||
ON rpr.region_id = rtp.region_id
|
||||
AND rpr.permit_id = rtp.permit_id
|
||||
|
|
@ -3540,9 +3582,10 @@ def list_region_permit_catalog(region_id: str) -> List[Dict[str, Any]]:
|
|||
"""
|
||||
catalog_map: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
||||
with _lic_pg_conn() as conn:
|
||||
_ensure_v2_visibility_column(conn)
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql, (region_id,))
|
||||
for permit_id, permit_name, theme_id, theme_name, risk_total in cur.fetchall():
|
||||
for permit_id, permit_name, theme_id, theme_name, risk_total, v2_visible in cur.fetchall():
|
||||
pid = str(permit_id)
|
||||
entry = catalog_map.setdefault(
|
||||
pid,
|
||||
|
|
@ -3550,6 +3593,7 @@ def list_region_permit_catalog(region_id: str) -> List[Dict[str, Any]]:
|
|||
"id": pid,
|
||||
"name": str(permit_name),
|
||||
"risk_count": int(risk_total or 0),
|
||||
"is_v2_visible": bool(v2_visible),
|
||||
"theme": {"id": "", "name": ""},
|
||||
"themes": [],
|
||||
},
|
||||
|
|
@ -3713,7 +3757,10 @@ def _load_permit_sources_for_region(
|
|||
|
||||
|
||||
def load_permits_and_risks(
|
||||
region_id: str, theme_id: Optional[str] = None, permit_id: Optional[str] = None
|
||||
region_id: str,
|
||||
theme_id: Optional[str] = None,
|
||||
permit_id: Optional[str] = None,
|
||||
only_visible: bool = False
|
||||
) -> List[Dict[str, object]]:
|
||||
"""Return permits with attached risk entries for a region (optionally filtered by theme)."""
|
||||
# Ensure optional permit file tables exist before running user queries.
|
||||
|
|
@ -3747,7 +3794,8 @@ def load_permits_and_risks(
|
|||
rpd.filler_name,
|
||||
COALESCE(pad.department_name, rpd.unit_name) AS unit_name,
|
||||
rpd.source_update_date,
|
||||
rpd.contact_info
|
||||
rpd.contact_info,
|
||||
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
|
||||
FROM region_permit_details rpd
|
||||
JOIN permits p ON p.id = rpd.permit_id
|
||||
LEFT JOIN permit_approval_departments pad
|
||||
|
|
@ -3770,6 +3818,8 @@ def load_permits_and_risks(
|
|||
if permit_id is not None:
|
||||
sql += " AND rpd.permit_id = %s"
|
||||
params.append(permit_id)
|
||||
if only_visible:
|
||||
sql += " AND COALESCE(rpd.is_v2_visible, true) = true"
|
||||
|
||||
sql += """
|
||||
ORDER BY p.name, LENGTH(rpr.serial_number), rpr.serial_number, rk.risk_content
|
||||
|
|
@ -3777,6 +3827,7 @@ def load_permits_and_risks(
|
|||
permits: Dict[str, Dict[str, object]] = {}
|
||||
risk_seen_map: Dict[str, Set[str]] = {} # pid -> set of risk_ids
|
||||
with _lic_pg_conn() as conn:
|
||||
_ensure_v2_visibility_column(conn)
|
||||
_ensure_contact_info_column(conn)
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql, tuple(params))
|
||||
|
|
@ -3801,6 +3852,7 @@ def load_permits_and_risks(
|
|||
unit_name,
|
||||
source_update_date,
|
||||
contact_info,
|
||||
v2_visible,
|
||||
) = row
|
||||
pid = str(permit_id)
|
||||
theme_id_value = str(row_theme_id) if row_theme_id else ""
|
||||
|
|
@ -4252,7 +4304,8 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
|||
rtp.theme_id,
|
||||
t.name AS theme_name,
|
||||
p.id AS permit_id,
|
||||
p.name AS permit_name
|
||||
p.name AS permit_name,
|
||||
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
|
||||
FROM region_permit_details rpd
|
||||
JOIN permits p ON p.id = rpd.permit_id
|
||||
JOIN regions r ON r.id = rpd.region_id
|
||||
|
|
@ -4263,6 +4316,7 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
|||
"""
|
||||
ordered: OrderedDict[Tuple[str, str], Dict[str, str]] = OrderedDict()
|
||||
with _lic_pg_conn() as conn:
|
||||
_ensure_v2_visibility_column(conn)
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql, (permit_name,))
|
||||
rows = cur.fetchall()
|
||||
|
|
@ -4276,7 +4330,8 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
|||
rtp.theme_id,
|
||||
t.name AS theme_name,
|
||||
p.id AS permit_id,
|
||||
p.name AS permit_name
|
||||
p.name AS permit_name,
|
||||
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
|
||||
FROM region_permit_details rpd
|
||||
JOIN permits p ON p.id = rpd.permit_id
|
||||
JOIN regions r ON r.id = rpd.region_id
|
||||
|
|
@ -4289,7 +4344,7 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
|||
rows = cur.fetchall()
|
||||
|
||||
for row in rows:
|
||||
region_id, region_name, theme_id, theme_name, permit_id, canonical_name = row
|
||||
region_id, region_name, theme_id, theme_name, permit_id, canonical_name, v2_visible = row
|
||||
rid = str(region_id)
|
||||
pid = str(permit_id)
|
||||
tid = str(theme_id) if theme_id else ""
|
||||
|
|
@ -4304,11 +4359,12 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
|||
"theme_name": tname,
|
||||
"permit_id": pid,
|
||||
"permit_name": str(canonical_name),
|
||||
"is_v2_visible": bool(v2_visible),
|
||||
}
|
||||
return list(ordered.values())
|
||||
return [item for item in ordered.values() if item.get("is_v2_visible")]
|
||||
|
||||
|
||||
def load_theme_payload(region_id: str, theme_id: str) -> Dict[str, object]:
|
||||
def load_theme_payload(region_id: str, theme_id: str, only_visible: bool = False) -> Dict[str, object]:
|
||||
"""Assemble full data bundle for a region-theme selection."""
|
||||
info_sql = """
|
||||
SELECT r.id, r.name, t.id, t.name
|
||||
|
|
@ -4326,7 +4382,7 @@ def load_theme_payload(region_id: str, theme_id: str) -> Dict[str, object]:
|
|||
raise ValueError("Region/theme combination not found")
|
||||
region_uuid, region_name, theme_uuid, theme_name = row
|
||||
|
||||
permits = load_permits_and_risks(region_id, theme_id)
|
||||
permits = load_permits_and_risks(region_id, theme_id, only_visible=only_visible)
|
||||
return {
|
||||
"region": {"id": str(region_uuid), "name": str(region_name)},
|
||||
"theme": {"id": str(theme_uuid), "name": str(theme_name)},
|
||||
|
|
@ -6170,23 +6226,25 @@ def filter_permits_advanced(
|
|||
themes: Optional[List[str]] = None,
|
||||
departments: Optional[List[str]] = None,
|
||||
search_text: Optional[str] = None,
|
||||
visibility: Optional[str] = None, # 'visible', 'hidden' or None/all
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> Dict[str, Any]:
|
||||
"""Filter permits using multiple dimensions (region, theme, department, search text).
|
||||
"""Filter permits using multiple dimensions (region, theme, department, search text, visibility).
|
||||
|
||||
Args:
|
||||
regions: List of region IDs to filter by (supports multi-select)
|
||||
themes: List of theme IDs to filter by (supports multi-select)
|
||||
departments: List of department IDs to filter by (supports multi-select)
|
||||
search_text: Search in permit name
|
||||
visibility: Filter by v2 visibility ('visible', 'hidden')
|
||||
limit: Maximum number of results to return
|
||||
offset: Offset for pagination
|
||||
|
||||
Returns:
|
||||
Dictionary containing filtered permits and metadata
|
||||
"""
|
||||
print(f"[DEBUG] filter_permits_advanced called with limit={limit}, offset={offset}")
|
||||
print(f"[DEBUG] filter_permits_advanced called with limit={limit}, offset={offset}, visibility={visibility}")
|
||||
# Use subquery to avoid DISTINCT with window functions issue
|
||||
# Subquery to get unique permits matching filters with pagination
|
||||
# We use a CTE to ensure limit/offset apply to unique permits, not to rows (which can duplicate per theme)
|
||||
|
|
@ -6212,6 +6270,11 @@ def filter_permits_advanced(
|
|||
base_where += f" AND LOWER(p.name) LIKE LOWER(%s)"
|
||||
base_params.append(f"%{search_text}%")
|
||||
|
||||
if visibility == 'visible':
|
||||
base_where += " AND (rpd.is_v2_visible IS TRUE OR rpd.is_v2_visible IS NULL)"
|
||||
elif visibility == 'hidden':
|
||||
base_where += " AND rpd.is_v2_visible IS FALSE"
|
||||
|
||||
sql = f"""
|
||||
WITH filtered_p AS (
|
||||
SELECT rpd.permit_id, rpd.region_id
|
||||
|
|
@ -6236,7 +6299,8 @@ def filter_permits_advanced(
|
|||
rtp.theme_id,
|
||||
t.name AS theme_name,
|
||||
COALESCE(risk_counts.risk_count, 0) AS risk_count,
|
||||
COALESCE(theme_counts.theme_count, 0) AS theme_count
|
||||
COALESCE(theme_counts.theme_count, 0) AS theme_count,
|
||||
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
|
||||
FROM filtered_p fp
|
||||
JOIN region_permit_details rpd ON rpd.permit_id = fp.permit_id AND rpd.region_id = fp.region_id
|
||||
JOIN permits p ON p.id = rpd.permit_id
|
||||
|
|
@ -6280,6 +6344,7 @@ def filter_permits_advanced(
|
|||
theme_name,
|
||||
risk_count,
|
||||
theme_count,
|
||||
v2_visible,
|
||||
) in cur.fetchall():
|
||||
pid = str(permit_id)
|
||||
key = f"{pid}_{rid}"
|
||||
|
|
@ -6294,6 +6359,7 @@ def filter_permits_advanced(
|
|||
"themes": [],
|
||||
"risk_count": int(risk_count or 0),
|
||||
"theme_count": int(theme_count or 0),
|
||||
"is_v2_visible": bool(v2_visible),
|
||||
}
|
||||
|
||||
if tid or theme_name:
|
||||
|
|
@ -6340,8 +6406,72 @@ def filter_permits_advanced(
|
|||
}
|
||||
|
||||
|
||||
def _ensure_v2_visibility_column(conn: Optional[pg.Connection] = None) -> None:
|
||||
"""Ensure that the is_v2_visible column exists in region_permit_details."""
|
||||
global _V2_VISIBILITY_SCHEMA_READY
|
||||
if _V2_VISIBILITY_SCHEMA_READY:
|
||||
return
|
||||
|
||||
with _V2_VISIBILITY_SCHEMA_LOCK:
|
||||
if _V2_VISIBILITY_SCHEMA_READY:
|
||||
return
|
||||
|
||||
sql = "ALTER TABLE region_permit_details ADD COLUMN IF NOT EXISTS is_v2_visible BOOLEAN DEFAULT TRUE"
|
||||
|
||||
if conn is not None:
|
||||
original_autocommit = conn.autocommit
|
||||
try:
|
||||
conn.autocommit = True
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql)
|
||||
finally:
|
||||
conn.autocommit = original_autocommit
|
||||
else:
|
||||
with _lic_pg_conn(autocommit=True) as ensure_conn:
|
||||
cur = ensure_conn.cursor()
|
||||
cur.execute(sql)
|
||||
|
||||
_V2_VISIBILITY_SCHEMA_READY = True
|
||||
|
||||
|
||||
def _ensure_contact_info_column(conn: pg.Connection) -> None:
|
||||
"Ensure that the contact_info column exists in region_permit_details."
|
||||
# This check is now redundant since schema fix script was run, but kept for safety
|
||||
pass
|
||||
|
||||
|
||||
def update_permit_v2_visibility(
|
||||
region_id: str,
|
||||
permit_id: str,
|
||||
is_visible: bool,
|
||||
operator: str = "admin"
|
||||
) -> bool:
|
||||
"""Update the is_v2_visible flag for a specific region-permit pair."""
|
||||
_ensure_v2_visibility_column()
|
||||
|
||||
sql = """
|
||||
UPDATE region_permit_details
|
||||
SET is_v2_visible = %s,
|
||||
updated_at = now()
|
||||
WHERE region_id = %s AND permit_id = %s
|
||||
"""
|
||||
|
||||
with _lic_pg_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql, (is_visible, region_id, permit_id))
|
||||
count = cur.rowcount
|
||||
conn.commit()
|
||||
|
||||
if count > 0:
|
||||
log_operation(
|
||||
operator=operator,
|
||||
operation_type="UPDATE",
|
||||
target_type="PERMIT_VISIBILITY",
|
||||
target_id=f"{region_id}:{permit_id}",
|
||||
target_name=f"Permit {permit_id} in region {region_id}",
|
||||
change_summary=f"Set is_v2_visible to {is_visible}",
|
||||
details={"is_visible": is_visible, "region_id": region_id, "permit_id": permit_id}
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -2329,6 +2329,57 @@
|
|||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Toggle Switch Styles */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .3s;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked+.slider {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
input:checked+.slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
input:disabled+.slider {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -2514,6 +2565,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 启用状态筛选 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<label style="font-size: 13px; font-weight: 600; color: #555;">启用状态</label>
|
||||
<select id="filterVisibility"
|
||||
style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: white;">
|
||||
<option value="all">全部状态</option>
|
||||
<option value="visible">已启用</option>
|
||||
<option value="hidden">已隐藏</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 搜索关键词 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<label style="font-size: 13px; font-weight: 600; color: #555;">搜索关键词</label>
|
||||
|
|
@ -6424,6 +6486,8 @@
|
|||
const departmentCheckboxes = document.querySelectorAll('input[name="departmentFilter"]:checked');
|
||||
const departments = Array.from(departmentCheckboxes).map(cb => cb.value);
|
||||
|
||||
const visibility = document.getElementById('filterVisibility')?.value || 'all';
|
||||
|
||||
const searchText = document.getElementById('filterSearchText')?.value || '';
|
||||
|
||||
const filters = {
|
||||
|
|
@ -6432,7 +6496,8 @@
|
|||
departments: departments.length > 0 ? departments : null,
|
||||
search_text: searchText.trim() || null,
|
||||
limit: permitPageSize,
|
||||
offset: permitCurrentPage * permitPageSize
|
||||
offset: permitCurrentPage * permitPageSize,
|
||||
visibility: visibility !== 'all' ? visibility : null
|
||||
};
|
||||
|
||||
// 显示加载状态
|
||||
|
|
@ -6808,6 +6873,63 @@
|
|||
}
|
||||
}
|
||||
|
||||
// 切换许可事项可见性
|
||||
async function togglePermitVisibility(permitId, regionId, currentStatus, event) {
|
||||
const nextStatus = !currentStatus;
|
||||
|
||||
// 立即禁用开关防止重复点击
|
||||
if (event && event.target) {
|
||||
event.target.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permits/visibility', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permit_id: permitId,
|
||||
region_id: regionId,
|
||||
is_v2_visible: nextStatus
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || '更新失败');
|
||||
}
|
||||
|
||||
// 立即更新前端状态参数以便下次点击
|
||||
if (event && event.target) {
|
||||
event.target.setAttribute('onclick', `togglePermitVisibility('${permitId}', '${regionId}', ${nextStatus}, event)`);
|
||||
|
||||
// 可选:给个微弱的小提示
|
||||
const row = event.target.closest('tr');
|
||||
if (row) {
|
||||
const originalBg = row.style.backgroundColor;
|
||||
row.style.backgroundColor = '#f0fdf4';
|
||||
setTimeout(() => row.style.backgroundColor = originalBg, 500);
|
||||
}
|
||||
} else {
|
||||
applyPermitFilter();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新可见性失败:', error);
|
||||
showAlert('error', '更新失败:' + error.message);
|
||||
// 恢复原状
|
||||
if (event && event.target) {
|
||||
event.target.checked = currentStatus;
|
||||
}
|
||||
} finally {
|
||||
if (event && event.target) {
|
||||
event.target.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除许可事项
|
||||
async function deletePermit(permitId, regionId) {
|
||||
if (!confirm('确定要删除该许可事项吗?此操作不可恢复,并且会创建风险快照。')) {
|
||||
|
|
@ -6873,6 +6995,7 @@
|
|||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 180px;">行政区域</th>
|
||||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 120px;">主题</th>
|
||||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 100px;">风险数</th>
|
||||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 100px;">启用</th>
|
||||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 180px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -6895,6 +7018,13 @@
|
|||
<td style="padding: 16px; color: #666;">${escapeHtml(permit.region?.name || '-')}</td>
|
||||
<td style="padding: 16px; color: #666;">${permit.theme_count || 0} 个</td>
|
||||
<td style="padding: 16px; color: #666;">${permit.risk_count || 0}</td>
|
||||
<td style="padding: 16px;">
|
||||
<label class="switch">
|
||||
<input type="checkbox" ${permit.is_v2_visible ? 'checked' : ''}
|
||||
onclick="togglePermitVisibility('${permit.id}', '${regionId}', ${permit.is_v2_visible}, event)">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td style="padding: 16px;">
|
||||
<button onclick="viewPermitDetail('${permit.id}', '${regionId}')" style="padding: 6px 12px; background: #2c5282; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; margin-right: 8px;">
|
||||
查看
|
||||
|
|
@ -6968,6 +7098,9 @@
|
|||
if (departmentSelect) departmentSelect.value = '';
|
||||
if (searchInput) searchInput.value = '';
|
||||
|
||||
const filterVisibility = document.getElementById('filterVisibility');
|
||||
if (filterVisibility) filterVisibility.value = 'all';
|
||||
|
||||
permitCurrentPage = 0;
|
||||
|
||||
// 清空结果
|
||||
|
|
|
|||
Loading…
Reference in New Issue