feat(ui,api): resolve department filter errors and enhance import wizard
This commit is contained in:
parent
fbc696b61c
commit
e7da819fea
|
|
@ -0,0 +1,8 @@
|
|||
Tables:
|
||||
|
||||
--- Schemas ---
|
||||
|
||||
|
||||
--- Searching for Theme ---
|
||||
|
||||
Error querying data: no such table: legal_risk_theme
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
Tables:
|
||||
|
||||
--- Schemas ---
|
||||
|
||||
|
||||
--- Searching for Theme ---
|
||||
|
||||
Error querying data: no such table: themes
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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 @@
|
|||
<div class="tabs-container">
|
||||
<ul class="tabs-nav" id="tabsNav">
|
||||
<li><button class="tab-button active" data-tab="overview-tab" onclick="switchTab('overview-tab')">
|
||||
<span>📊</span> 数据概览
|
||||
<span>📊</span> 数据统计
|
||||
</button></li>
|
||||
<li><button class="tab-button" data-tab="permits-tab" onclick="switchTab('permits-tab')">
|
||||
<span>📋</span> 许可事项管理
|
||||
<span>📋</span> 数据查询
|
||||
</button></li>
|
||||
<li><button class="tab-button" data-tab="checkpoints-tab" onclick="switchTab('checkpoints-tab')">
|
||||
<span>🔒</span> 检查点管理
|
||||
</button></li>
|
||||
<li><button class="tab-button" data-tab="files-tab" onclick="switchTab('files-tab')">
|
||||
<span>📁</span> 文件管理
|
||||
<span>📁</span> 历史文件
|
||||
</button></li>
|
||||
<li><button class="tab-button" data-tab="import-tab" onclick="switchTab('import-tab')">
|
||||
<span>📥</span> 许可导入
|
||||
<span>📥</span> 事项更新
|
||||
</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -2441,10 +2489,7 @@
|
|||
<div class="panel" style="margin-top: 0;">
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<!-- <h3
|
||||
style="color: #333; margin: 0; font-size: 18px; display: flex; align-items: center; gap: 8px;">
|
||||
<span>📂</span> 行业主题统计
|
||||
</h3>-->
|
||||
<span style="font-weight: 600; color: #333;">行业主题统计</span>
|
||||
<button onclick="loadOverviewThemes()"
|
||||
style="padding: 4px 8px; font-size: 12px; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 4px; cursor: pointer;">刷新</button>
|
||||
</div>
|
||||
|
|
@ -2454,16 +2499,53 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待办事项/异常面板 -->
|
||||
<!-- 待办事项/异常面板 (Right Panel) -->
|
||||
<div class="panel" style="margin-top: 0;">
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<!-- <h3
|
||||
style="color: #333; margin: 0; font-size: 18px; display: flex; align-items: center; gap: 8px;">
|
||||
<span>⚠️</span> 待分类事项 (未绑定主题)
|
||||
</h3>-->
|
||||
<button onclick="loadOverviewUnbound()"
|
||||
style="padding: 4px 8px; font-size: 12px; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 4px; cursor: pointer;">刷新</button>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-weight: 600; color: #333;">待分类事项 (未绑定主题)</span>
|
||||
</div>
|
||||
|
||||
<!-- Local Filters -->
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
|
||||
<input type="text" id="overviewUnboundKeyword" placeholder="输入许可名称关键词..."
|
||||
style="padding: 6px; border: 1px solid #d1d5db; border-radius: 4px; color: #374151; font-size: 13px; flex: 1;">
|
||||
|
||||
<!-- Region Filter -->
|
||||
<select id="overviewUnboundRegion" onchange="onOverviewRegionChange()"
|
||||
style="padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 4px; background: white; cursor: pointer; color: #374151; font-size: 13px; margin-right: 4px; outline: none; min-width: 100px;">
|
||||
<option value="">全部区域</option>
|
||||
</select>
|
||||
|
||||
<div class="dropdown" style="position: relative;">
|
||||
<button id="overviewUnboundFilterDepartment"
|
||||
onclick="toggleMultiSelect('overviewUnboundDepartmentOptions')"
|
||||
style="padding: 6px 12px; border: 1px solid #d1d5db; border-radius: 4px; background: white; cursor: pointer; min-width: 120px; text-align: left; display: flex; justify-content: space-between; align-items: center; color: #374151; font-size: 13px;">
|
||||
<span id="overviewUnboundDepartmentSelectedText"
|
||||
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 90px;">全部部门</span>
|
||||
<span style="font-size: 10px; color: #666;">▼</span>
|
||||
</button>
|
||||
<div id="overviewUnboundDepartmentOptions" class="dropdown-content"
|
||||
style="display: none; position: absolute; top: 100%; right: 0; background: white; border: 1px solid #d1d5db; border-radius: 4px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); z-index: 50; width: 250px; max-height: 300px; overflow-y: auto; margin-top: 4px;">
|
||||
<div style="padding: 8px; border-bottom: 1px solid #f3f4f6;">
|
||||
<label
|
||||
style="display: flex; align-items: center; font-weight: 600; color: #4b5563; cursor: pointer; font-size: 13px;">
|
||||
<input type="checkbox" id="overviewUnboundDepartmentSelectAll"
|
||||
onchange="toggleSelectAll('overviewUnboundDepartment')"
|
||||
style="margin-right: 8px;">
|
||||
全选
|
||||
</label>
|
||||
</div>
|
||||
<div id="overviewUnboundDepartmentOptionsList">
|
||||
<!-- Populated by JS -->
|
||||
<div style="padding: 10px; text-align: center; color: #999;">正在加载部门...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick="loadOverviewUnbound()"
|
||||
style="padding: 6px 12px; background: #2c5282; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="overviewUnboundContainer" style="min-height: 200px;">
|
||||
<div class="loading" style="padding: 40px; justify-content: center;"><span
|
||||
|
|
@ -2476,13 +2558,14 @@
|
|||
<!-- 许可事项管理标签页 -->
|
||||
<div id="permits-tab" class="tab-content">
|
||||
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||||
<span>📋</span> 许可事项管理
|
||||
<span>📋</span> 数据查询
|
||||
</h2>
|
||||
<p style="color: #666; margin-bottom: 20px;">使用筛选器快速定位许可事项,支持多维度筛选、分页浏览、风险统计等功能。</p>
|
||||
<!-- <p style="color: #666; margin-bottom: 20px;">使用筛选器快速定位许可事项,支持多维度筛选、分页浏览、风险统计等功能。</p> -->
|
||||
|
||||
<!-- 筛选器区域 -->
|
||||
<div
|
||||
style="background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin-bottom: 20px; position: relative;">
|
||||
<h5 style="margin-top: 0; margin-bottom: 16px; font-weight: 600; color: #333;">主题查询</h5>
|
||||
<!-- 筛选器加载状态 -->
|
||||
<div id="filterOptionsLoading"
|
||||
style="display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.9); border-radius: 8px; z-index: 100; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
|
|
@ -2578,7 +2661,7 @@
|
|||
|
||||
<!-- 搜索关键词 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<label style="font-size: 13px; font-weight: 600; color: #555;">搜索关键词</label>
|
||||
<label style="font-size: 13px; font-weight: 600; color: #555;">单事项查询</label>
|
||||
<input type="text" id="filterSearchText" placeholder="输入许可名称关键词..."
|
||||
style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
|
||||
</div>
|
||||
|
|
@ -2647,7 +2730,7 @@
|
|||
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||||
<span>🔒</span> 数据库检查点管理
|
||||
</h2>
|
||||
<p style="color: #666; margin-bottom: 20px;">管理系统数据库备份点,创建、恢复、删除检查点。支持许可风险快照查看与管理。</p>
|
||||
<!-- <p style="color: #666; margin-bottom: 20px;">管理系统数据库备份点,创建、恢复、删除检查点。支持许可风险快照查看与管理。</p> -->
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0;">
|
||||
<div id="checkpointTabContainer">
|
||||
|
|
@ -2667,7 +2750,7 @@
|
|||
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||||
<span>📁</span> 文件管理
|
||||
</h2>
|
||||
<p style="color: #666; margin-bottom: 20px;">管理许可导入相关文件,查看文件关联、重新导入、删除文件等操作。</p>
|
||||
<!-- <p style="color: #666; margin-bottom: 20px;">管理许可导入相关文件,查看文件关联、重新导入、删除文件等操作。</p> -->
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0;">
|
||||
<div id="fileManagerContainer">
|
||||
<!-- 文件管理内容将动态加载到这里 -->
|
||||
|
|
@ -2686,7 +2769,7 @@
|
|||
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||||
<span>📥</span> 许可导入
|
||||
</h2>
|
||||
<p style="color: #666; margin-bottom: 20px;">通过Excel文件批量导入许可数据,支持多区划批量处理、主题绑定、预览确认等功能。</p>
|
||||
<!-- <p style="color: #666; margin-bottom: 20px;">通过Excel文件批量导入许可数据,支持多区划批量处理、主题绑定、预览确认等功能。</p> -->
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0;">
|
||||
<div class="drag-drop-area" onclick="triggerFileUpload()" id="dragDropArea">
|
||||
<div class="drag-drop-icon">📄</div>
|
||||
|
|
@ -2696,7 +2779,7 @@
|
|||
<button class="import-template-button" onclick="event.stopPropagation(); openImportModal()">
|
||||
<span class="import-template-icon">⚙️</span>
|
||||
<span class="import-template-text">
|
||||
<span class="import-template-title">使用导入向导</span>
|
||||
<span class="import-template-title">上传</span>
|
||||
<span class="import-template-subtitle">打开完整导入流程</span>
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -2768,6 +2851,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片放大预览 -->
|
||||
<div id="imageZoomModal" class="image-zoom-overlay" onclick="closeImageZoom()">
|
||||
<span class="image-zoom-close">×</span>
|
||||
<img class="image-zoom-content" id="zoomedImage">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const LOGIN_PATH = '/fs-ai-asistant/api/workflow/lawrisk/login';
|
||||
let currentUserProfile = null;
|
||||
|
|
@ -3351,7 +3440,8 @@
|
|||
</div>
|
||||
<div class="detail-actions">
|
||||
<button class="btn" id="downloadPermitFileBtn" ${downloadDisabledAttr} onclick="downloadPermitFile()" title="${hasPermitFile ? '下载原文件' : '暂无原始文件'}">下载原文件</button>
|
||||
<button class="btn btn-danger" id="deletePermitBtn" ${deleteButtonDisabled} onclick="confirmDeleteCurrentPermit()">${deleteButtonLabel}</button>
|
||||
${(currentUserProfile && (currentUserProfile.role === 'admin' || currentUserProfile.username === 'fssjsj')) ? `
|
||||
<button class="btn btn-danger" id="deletePermitBtn" ${deleteButtonDisabled} onclick="confirmDeleteCurrentPermit()">${deleteButtonLabel}</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -3442,6 +3532,12 @@
|
|||
}
|
||||
|
||||
function confirmDeleteCurrentPermit() {
|
||||
// 权限检查
|
||||
if (!currentUserProfile || (currentUserProfile.role !== 'admin' && currentUserProfile.username !== 'fssjsj')) {
|
||||
alert('您没有权限删除许可事项');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDeletingPermit) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -4484,16 +4580,31 @@
|
|||
|
||||
// 添加样表示例
|
||||
html += '<div class="sample-excel-preview-section">';
|
||||
html += '<div class="sample-excel-preview-title"><span>💡</span> 样表示例 (仅销售预包装食品备案)</div>';
|
||||
html += '<div class="sample-excel-preview-title"><span>💡</span> 导入模板与样表示例</div>';
|
||||
html += getSampleExcelHtml();
|
||||
html += '</div>';
|
||||
|
||||
|
||||
|
||||
function getSampleExcelHtml() {
|
||||
return `
|
||||
<div class="sample-excel-preview-container" style="text-align: center;">
|
||||
<img src="/static/images/sample_table.png" alt="样表示例" style="max-width: 100%; height: auto; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.08);">
|
||||
<div class="sample-excel-preview-container" style="display: flex; gap: 24px; padding: 20px; background: #f8fafc;">
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; background: white; padding: 8px; border-radius: 6px; border: 1px solid #e2e8f0;">
|
||||
<span style="background: #3b82f6; color: white; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px;">1</span>
|
||||
空表 (空模板)
|
||||
</div>
|
||||
<div style="flex: 1; background: white; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); cursor: zoom-in;" onclick="openImageZoom('/static/images/empty_table.png')">
|
||||
<img src="/static/images/empty_table.png" alt="空表" style="width: 100%; height: auto; display: block; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.05));">
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; background: white; padding: 8px; border-radius: 6px; border: 1px solid #e2e8f0;">
|
||||
<span style="background: #3b82f6; color: white; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px;">2</span>
|
||||
样表示例 (含填报说明)
|
||||
</div>
|
||||
<div style="flex: 1; background: white; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); cursor: zoom-in;" onclick="openImageZoom('/static/images/sample_table.png')">
|
||||
<img src="/static/images/sample_table.png" alt="样表示例" style="width: 100%; height: auto; display: block; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.05));">
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -5965,6 +6076,27 @@
|
|||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 图片放大预览
|
||||
function openImageZoom(imgSrc) {
|
||||
const modal = document.getElementById('imageZoomModal');
|
||||
const zoomedImg = document.getElementById('zoomedImage');
|
||||
if (modal && zoomedImg) {
|
||||
zoomedImg.src = imgSrc;
|
||||
modal.classList.add('show');
|
||||
// 禁用背景滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function closeImageZoom() {
|
||||
const modal = document.getElementById('imageZoomModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('show');
|
||||
// 恢复背景滚动
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
|
|
@ -6191,6 +6323,13 @@
|
|||
|
||||
// 标签页切换功能
|
||||
function switchTab(tabId) {
|
||||
// 权限检查
|
||||
if (tabId === 'overview-tab') {
|
||||
if (!currentUserProfile || (currentUserProfile.role !== 'admin' && currentUserProfile.username !== 'fssjsj')) {
|
||||
console.warn('Unauthorized access to overview tab');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 隐藏所有标签页内容
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
tabContents.forEach(content => {
|
||||
|
|
@ -6932,6 +7071,12 @@
|
|||
|
||||
// 删除许可事项
|
||||
async function deletePermit(permitId, regionId) {
|
||||
// 权限检查:只有 superadmin (role='admin') 和 市级管理员 (username='fssjsj') 可以删除
|
||||
if (!currentUserProfile || (currentUserProfile.role !== 'admin' && currentUserProfile.username !== 'fssjsj')) {
|
||||
showAlert('error', '您没有权限删除许可事项');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('确定要删除该许可事项吗?此操作不可恢复,并且会创建风险快照。')) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -6994,7 +7139,7 @@
|
|||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px;">许可事项</th>
|
||||
<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: 100px;">启用</th>
|
||||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 180px;">操作</th>
|
||||
</tr>
|
||||
|
|
@ -7029,9 +7174,10 @@
|
|||
<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;">
|
||||
查看
|
||||
</button>
|
||||
${(currentUserProfile && (currentUserProfile.role === 'admin' || currentUserProfile.username === 'fssjsj')) ? `
|
||||
<button onclick="deletePermit('${permit.id}', '${regionId}')" style="padding: 6px 12px; background: #dc2626; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">
|
||||
删除
|
||||
</button>
|
||||
</button>` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
|
@ -7313,6 +7459,114 @@
|
|||
// 初始化标签页
|
||||
async function loadOverviewTab() {
|
||||
loadOverviewThemes();
|
||||
await loadOverviewRegionsForUnbound();
|
||||
loadOverviewUnboundDepartmentOptions();
|
||||
loadOverviewUnbound();
|
||||
}
|
||||
|
||||
async function loadOverviewRegionsForUnbound() {
|
||||
const select = document.getElementById('overviewUnboundRegion');
|
||||
if (!select) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/v2/regions');
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
let html = '<option value="">全部区域</option>';
|
||||
(data.data || []).forEach(r => {
|
||||
html += `<option value="${r.id}">${escapeHtml(r.name)}</option>`;
|
||||
});
|
||||
select.innerHTML = html;
|
||||
}
|
||||
} catch (e) { console.error('Error loading regions:', e); }
|
||||
}
|
||||
|
||||
async function onOverviewRegionChange() {
|
||||
loadOverviewUnboundDepartmentOptions();
|
||||
loadOverviewUnbound();
|
||||
}
|
||||
|
||||
async function loadOverviewUnboundDepartmentOptions() {
|
||||
const container = document.getElementById('overviewUnboundDepartmentOptionsList');
|
||||
if (!container) return;
|
||||
|
||||
// Always reload when region changes, so ignore dataset.loaded check or reset it?
|
||||
// Existing logic checked dataset.loaded. We should just overwrite it.
|
||||
|
||||
const regionId = document.getElementById('overviewUnboundRegion') ? document.getElementById('overviewUnboundRegion').value : '';
|
||||
if (regionId) {
|
||||
// If a region is selected, we MUST reload to filter departments
|
||||
container.innerHTML = '<div style="padding: 10px; text-align: center; color: #999;">正在加载部门...</div>';
|
||||
} else if (container.dataset.loaded === 'true') {
|
||||
// If no region selected (All) and already loaded, maybe skip?
|
||||
// But wait, if we switch from Specific Region -> All, we need to reload All.
|
||||
// So simplistic caching is problematic unless we track WHAT was loaded.
|
||||
// For simplicity, let's always reload or track the current filter state.
|
||||
// Let's just always reload for now to be safe and responsive.
|
||||
}
|
||||
|
||||
try {
|
||||
let url = '/fs-ai-asistant/api/workflow/lawrisk/v2/departments';
|
||||
if (regionId) url += `?region_id=${regionId}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
container.innerHTML = '';
|
||||
let departments = data.data || [];
|
||||
|
||||
if (departments.length === 0) {
|
||||
container.innerHTML = '<div style="padding: 10px; text-align: center; color: #999;">该区域暂无部门</div>';
|
||||
} else {
|
||||
departments.forEach(dept => {
|
||||
const div = document.createElement('div');
|
||||
div.style.padding = '6px 12px';
|
||||
div.innerHTML = `
|
||||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input type="checkbox" name="overviewUnboundDepartmentFilter" value="${dept.id}" onchange="updateSelectedText('overviewUnboundDepartment')" style="margin-right: 8px;">
|
||||
<span>${escapeHtml(dept.name)}</span>
|
||||
</label>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
container.dataset.loaded = 'true';
|
||||
|
||||
// Reset selection text
|
||||
updateSelectedText('overviewUnboundDepartment');
|
||||
} else {
|
||||
container.innerHTML = '<div style="padding: 10px; color: red;">加载失败</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
container.innerHTML = '<div style="padding: 10px; color: red;">网络错误</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function applyOverviewFilter() {
|
||||
// This function is now specific to unbound permits
|
||||
loadOverviewUnbound();
|
||||
}
|
||||
|
||||
function resetOverviewFilter() {
|
||||
document.getElementById('overviewUnboundKeyword').value = '';
|
||||
|
||||
const regionSelect = document.getElementById('overviewUnboundRegion');
|
||||
if (regionSelect) regionSelect.value = "";
|
||||
|
||||
const checkboxes = document.querySelectorAll('input[name="overviewUnboundDepartmentFilter"]');
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
updateSelectedText('overviewUnboundDepartment');
|
||||
|
||||
const selectAll = document.getElementById('overviewUnboundDepartmentSelectAll');
|
||||
if (selectAll) {
|
||||
selectAll.checked = false;
|
||||
selectAll.indeterminate = false;
|
||||
}
|
||||
|
||||
// Reload all departments (since region reset to All)
|
||||
loadOverviewUnboundDepartmentOptions();
|
||||
loadOverviewUnbound();
|
||||
}
|
||||
|
||||
|
|
@ -7322,6 +7576,7 @@
|
|||
container.innerHTML = '<div class="loading" style="padding: 40px; justify-content: center;"><span class="loading-icon"></span>加载中...</div>';
|
||||
|
||||
try {
|
||||
// No filters for themes overview
|
||||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/v2/themes');
|
||||
const data = await response.json();
|
||||
|
||||
|
|
@ -7370,7 +7625,21 @@
|
|||
container.innerHTML = '<div class="loading" style="padding: 40px; justify-content: center;"><span class="loading-icon"></span>加载中...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/v2/unbound-permits');
|
||||
const keywordInput = document.getElementById('overviewUnboundKeyword');
|
||||
const keyword = keywordInput ? keywordInput.value.trim() : '';
|
||||
|
||||
const regionSelect = document.getElementById('overviewUnboundRegion');
|
||||
const regionId = regionSelect ? regionSelect.value : '';
|
||||
|
||||
const deptCheckboxes = document.querySelectorAll('input[name="overviewUnboundDepartmentFilter"]:checked');
|
||||
const deptIds = Array.from(deptCheckboxes).map(cb => cb.value);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (keyword) params.append('search_text', keyword);
|
||||
if (regionId) params.append('region_id', regionId);
|
||||
deptIds.forEach(id => params.append('department_ids[]', id));
|
||||
|
||||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/v2/unbound-permits?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
|
|
@ -7379,8 +7648,8 @@
|
|||
container.innerHTML = `
|
||||
<div style="padding: 40px; text-align: center; color: #059669; background: #ecfdf5; border-radius: 8px;">
|
||||
<div style="font-size: 24px; margin-bottom: 8px;">🎉</div>
|
||||
<div style="font-weight: 600;">所有区域的许可事项均已分配主题!</div>
|
||||
<div style="font-size: 12px; margin-top: 4px;">当前没有待处理的未分类数据。</div>
|
||||
<div style="font-weight: 600;">没有找到符合条件的未分类许可事项!</div>
|
||||
<div style="font-size: 12px; margin-top: 4px;">所有许可事项均已绑定主题或不符合当前筛选条件。</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
|
|
@ -7435,13 +7704,38 @@
|
|||
// 更新页面标题
|
||||
pageTitle.textContent = '🗃️ 管理员控制台';
|
||||
|
||||
// 权限检查:只有 superadmin (role='admin') 和 市级管理员 (username='fssjsj') 可见数据统计
|
||||
const isAuthorizedForOverview = currentUserProfile && (currentUserProfile.role === 'admin' || currentUserProfile.username === 'fssjsj');
|
||||
|
||||
const overviewTabBtn = document.querySelector('button[data-tab="overview-tab"]');
|
||||
if (overviewTabBtn && overviewTabBtn.parentElement) {
|
||||
overviewTabBtn.parentElement.style.display = isAuthorizedForOverview ? '' : 'none';
|
||||
}
|
||||
|
||||
// 检查点管理 Tab:完全隐藏,连 superadmin 也不显示
|
||||
const checkpointsTabBtn = document.querySelector('button[data-tab="checkpoints-tab"]');
|
||||
if (checkpointsTabBtn && checkpointsTabBtn.parentElement) {
|
||||
checkpointsTabBtn.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// 隐藏所有标签页
|
||||
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
// 检查 URL 参数是否有指定的 tab
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const tabParam = urlParams.get('tab');
|
||||
let tabParam = urlParams.get('tab');
|
||||
|
||||
// 如果用户没有权限访问 overview,且当前请求的是 overview (或默认),则重定向到 permits
|
||||
// 或者如果请求的是已隐藏的 checkpoints tab,也重定向到 permits
|
||||
if (((!tabParam || tabParam === 'overview') && !isAuthorizedForOverview) || tabParam === 'checkpoints') {
|
||||
tabParam = 'permits';
|
||||
// 更新URL,避免刷新后还是 old value
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('tab', 'permits');
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
|
||||
const targetTabId = tabParam ? (tabParam + '-tab') : 'overview-tab';
|
||||
|
||||
// 激活目标标签页
|
||||
|
|
@ -7454,9 +7748,16 @@
|
|||
|
||||
// 执行标签页对应的加载函数
|
||||
if (targetTabId === 'overview-tab') {
|
||||
loadOverviewTab();
|
||||
if (isAuthorizedForOverview) {
|
||||
loadOverviewTab();
|
||||
}
|
||||
} else if (targetTabId === 'permits-tab') {
|
||||
goToStep(1);
|
||||
} else if (targetTabId === 'files-tab') {
|
||||
// 确保文件管理也加载
|
||||
loadFileManager();
|
||||
} else if (targetTabId === 'checkpoints-tab') {
|
||||
loadCheckpointTab();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -0,0 +1,72 @@
|
|||
|
||||
import os
|
||||
import pg8000
|
||||
import sys
|
||||
|
||||
def load_env(env_path='.env'):
|
||||
config = {}
|
||||
if not os.path.exists(env_path):
|
||||
# try parent
|
||||
env_path = os.path.join('..', '.env')
|
||||
if not os.path.exists(env_path):
|
||||
print(f"Warning: {env_path} not found")
|
||||
return config
|
||||
|
||||
with open(env_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
config[key.strip()] = value.strip()
|
||||
return config
|
||||
|
||||
def inspect_remote_db():
|
||||
config = load_env()
|
||||
|
||||
with open('remote_inspection_output.txt', 'w', encoding='utf-8') as f:
|
||||
def log(msg):
|
||||
print(msg)
|
||||
f.write(str(msg) + "\n")
|
||||
|
||||
log("\nConnecting to licensing_risks database...")
|
||||
try:
|
||||
conn = pg8000.connect(
|
||||
user=config.get('LIC_PG_USER', 'postgres'),
|
||||
password=config.get('LIC_PG_PASSWORD'),
|
||||
host=config.get('LIC_PG_HOST', 'localhost'),
|
||||
port=int(config.get('LIC_PG_PORT', 5432)),
|
||||
database=config.get('LIC_PG_DATABASE', 'licensing_risks')
|
||||
)
|
||||
log("✅ Connection successful!")
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
log("\n--- Listing Tables ---\n")
|
||||
cursor.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
|
||||
tables = cursor.fetchall()
|
||||
for table in tables:
|
||||
log(table[0])
|
||||
|
||||
log("\n--- Checking Users/Accounts ---\n")
|
||||
log("Found table: auth_users")
|
||||
cursor.execute(f"SELECT * FROM auth_users") # Get all users to find the specific one
|
||||
users = cursor.fetchall()
|
||||
for user in users:
|
||||
log(user)
|
||||
|
||||
# Print columns
|
||||
cursor.execute(f"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'auth_users'")
|
||||
cols = cursor.fetchall()
|
||||
log("\nColumns:")
|
||||
for col in cols:
|
||||
log(col)
|
||||
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
log(f"❌ Database error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
inspect_remote_db()
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
db_path = 'law_risk.db'
|
||||
|
||||
def inspect_db():
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Database not found at {db_path}")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# List tables
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = cursor.fetchall()
|
||||
print("Tables:")
|
||||
for table in tables:
|
||||
print(table[0])
|
||||
|
||||
print("\n--- Schemas ---\n")
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = cursor.fetchall()
|
||||
print(f"Table: {table_name}")
|
||||
for col in columns:
|
||||
print(col)
|
||||
print("-" * 20)
|
||||
|
||||
print("\n--- Searching for Theme ---\n")
|
||||
try:
|
||||
# Assuming table is legal_risk_theme based on previous grep, checking anyway
|
||||
cursor.execute("SELECT * FROM themes WHERE name LIKE '%企业开办%'")
|
||||
themes = cursor.fetchall()
|
||||
print("Matching Themes:")
|
||||
for theme in themes:
|
||||
print(theme)
|
||||
|
||||
if themes:
|
||||
theme_id = themes[0][0] # Assuming first col is ID
|
||||
print(f"\nSearching for items with theme_id: {theme_id}")
|
||||
# Try to find table that links theme and items. legal_risk_licensing_item?
|
||||
print(f"Theme ID: {theme_id}")
|
||||
cursor.execute(f"SELECT * FROM region_theme_permits WHERE theme_id = '{theme_id}'")
|
||||
links = cursor.fetchall()
|
||||
print(f"Associated Links ({len(links)}):")
|
||||
for link in links:
|
||||
print(link)
|
||||
permit_id = link[2]
|
||||
cursor.execute(f"SELECT * FROM permits WHERE id = '{permit_id}'")
|
||||
permit = cursor.fetchone()
|
||||
print(f" -> Permit: {permit}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error querying data: {e}")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
inspect_db()
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import os
|
||||
import pg8000.dbapi as pg
|
||||
|
||||
def load_env():
|
||||
env_vars = {}
|
||||
try:
|
||||
with open('.env', 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
k, v = line.split('=', 1)
|
||||
env_vars[k.strip()] = v.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return env_vars
|
||||
|
||||
env = load_env()
|
||||
HOST = env.get("LIC_PG_HOST", "172.24.240.1")
|
||||
PORT = int(env.get("LIC_PG_PORT", "5432"))
|
||||
USER = env.get("LIC_PG_USER", "postgres")
|
||||
PASSWORD = env.get("LIC_PG_PASSWORD", "")
|
||||
DATABASE = env.get("LIC_PG_DATABASE", "licensing_risks")
|
||||
|
||||
print(f"Connecting to {HOST}:{PORT}/{DATABASE} as {USER}")
|
||||
try:
|
||||
conn = pg.connect(host=HOST, port=PORT, user=USER, password=PASSWORD, database=DATABASE)
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("Tables with 'theme' in name:")
|
||||
cursor.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name LIKE '%theme%'")
|
||||
for row in cursor.fetchall():
|
||||
print(f" - {row[0]}")
|
||||
|
||||
# Describe table
|
||||
print(f" Columns for {row[0]}:")
|
||||
cursor.execute(f"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{row[0]}'")
|
||||
for col in cursor.fetchall():
|
||||
print(f" - {col[0]} ({col[1]})")
|
||||
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import datetime
|
||||
import pg8000.dbapi as pg
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 1. Environment & Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
def load_env():
|
||||
env_vars = {}
|
||||
try:
|
||||
with open('.env', 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
k, v = line.split('=', 1)
|
||||
env_vars[k.strip()] = v.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return env_vars
|
||||
|
||||
env = load_env()
|
||||
HOST = env.get("LIC_PG_HOST", "172.24.240.1")
|
||||
PORT = int(env.get("LIC_PG_PORT", "5432"))
|
||||
USER = env.get("LIC_PG_USER", "postgres")
|
||||
PASSWORD = env.get("LIC_PG_PASSWORD", "")
|
||||
DATABASE = env.get("LIC_PG_DATABASE", "licensing_risks")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 2. Database Backup
|
||||
# -----------------------------------------------------------------------------
|
||||
def perform_backup():
|
||||
date_str = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
backup_file = os.path.abspath(f"data/backup_{date_str}.sql")
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(os.path.dirname(backup_file), exist_ok=True)
|
||||
|
||||
print(f"Starting backup to {backup_file}...")
|
||||
|
||||
# Set PGPASSWORD for pg_dump
|
||||
env_copy = os.environ.copy()
|
||||
env_copy["PGPASSWORD"] = PASSWORD
|
||||
|
||||
# Construct command
|
||||
# pg_dump -h host -p port -U user -f file dbname
|
||||
cmd = [
|
||||
"pg_dump",
|
||||
"-h", HOST,
|
||||
"-p", str(PORT),
|
||||
"-U", USER,
|
||||
"-f", backup_file,
|
||||
DATABASE
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, env=env_copy, check=True)
|
||||
print("Backup completed successfully.")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Backup failed: {e}")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("pg_dump not found in PATH. Skipping backup (RISKY!).")
|
||||
# In this specific task, if backup fails, we should probably stop or ask user.
|
||||
# But given I am an agent, I should try to warn and maybe proceed if trivial, but deleting data is dangerous.
|
||||
# I will stop if backup fails.
|
||||
return False
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 3. Data Cleanup
|
||||
# -----------------------------------------------------------------------------
|
||||
def perform_cleanup():
|
||||
print("Connecting to database...")
|
||||
try:
|
||||
conn = pg.connect(host=HOST, port=PORT, user=USER, password=PASSWORD, database=DATABASE)
|
||||
cursor = conn.cursor()
|
||||
|
||||
target_theme_name = "不涉及"
|
||||
|
||||
# 1. Find theme ID
|
||||
print(f"Finding theme '{target_theme_name}'...")
|
||||
cursor.execute("SELECT id FROM themes WHERE name = %s", (target_theme_name,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
print(f"Theme '{target_theme_name}' not found. Nothing to do.")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
theme_id = row[0]
|
||||
print(f"Found theme_id: {theme_id}")
|
||||
|
||||
# 2. Find associated permits (matters)
|
||||
# Using region_theme_permits as the binding table
|
||||
print("Finding associated permits...")
|
||||
cursor.execute("SELECT DISTINCT permit_id FROM region_theme_permits WHERE theme_id = %s", (theme_id,))
|
||||
permit_rows = cursor.fetchall()
|
||||
permit_ids = [r[0] for r in permit_rows]
|
||||
|
||||
print(f"Found {len(permit_ids)} permits bound to '{target_theme_name}'.")
|
||||
|
||||
if not permit_ids:
|
||||
print("No permits bound to this theme. Proceeding to delete theme only.")
|
||||
|
||||
# Start Transaction
|
||||
try:
|
||||
# 3. Clear TOPICS for these permits (Unbind ALL themes from these permits)
|
||||
if permit_ids:
|
||||
# Need to convert UUIDs to string for SQL IN clause or use execute with any
|
||||
# pg8000 handles list/tuple for IN nicely if formatted manually, or we can loop.
|
||||
# Batch delete is better.
|
||||
# DELETE FROM region_theme_permits WHERE permit_id IN (...)
|
||||
|
||||
# Format for IN clause
|
||||
permit_ids_tuple = tuple(permit_ids)
|
||||
if len(permit_ids) == 1:
|
||||
# distinct check ensures > 0, but tuple of 1 element needs comma
|
||||
pass
|
||||
|
||||
# Note: pg8000 might struggle with large IN clauses in param logic depending on version.
|
||||
# But let's try standard parameterized query.
|
||||
# Actually, to be safe with syntax: "IN %s" with a tuple works in some drivers,
|
||||
# but often 'IN (%s, %s)' is needed.
|
||||
# I'll generate placeholders.
|
||||
placeholders = ",".join(["%s"] * len(permit_ids))
|
||||
|
||||
print(f"Clearing ALL topic bindings for these {len(permit_ids)} permits...")
|
||||
|
||||
# Delete from region_theme_permits
|
||||
cursor.execute(f"DELETE FROM region_theme_permits WHERE permit_id IN ({placeholders})", permit_ids_tuple)
|
||||
print(f"Deleted {cursor.rowcount} rows from region_theme_permits.")
|
||||
|
||||
# Also check region_permit_theme_overrides if they exist for these permits?
|
||||
# The user said "clear topics". Overrides might not be "bindings" per se, but let's check table existence.
|
||||
# I'll skip overrides to avoid complexity unless requested, sticking to "bindings".
|
||||
|
||||
# 4. Delete the theme itself from all related tables
|
||||
print(f"Deleting theme '{target_theme_name}' references...")
|
||||
|
||||
# Delete from region_themes
|
||||
cursor.execute("DELETE FROM region_themes WHERE theme_id = %s", (theme_id,))
|
||||
print(f"Deleted {cursor.rowcount} rows from region_themes.")
|
||||
|
||||
# Delete from permit_theme_rules
|
||||
cursor.execute("DELETE FROM permit_theme_rules WHERE theme_id = %s", (theme_id,))
|
||||
print(f"Deleted {cursor.rowcount} rows from permit_theme_rules.")
|
||||
|
||||
# Delete from themes
|
||||
cursor.execute("DELETE FROM themes WHERE id = %s", (theme_id,))
|
||||
print(f"Deleted {cursor.rowcount} row from themes.")
|
||||
|
||||
# Commit
|
||||
conn.commit()
|
||||
print("Transaction committed successfully.")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"Error during transaction, rolled back. Error: {e}")
|
||||
raise
|
||||
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if perform_backup():
|
||||
perform_cleanup()
|
||||
else:
|
||||
print("Aborting cleanup because backup failed.")
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
|
||||
import os
|
||||
import pg8000
|
||||
import sys
|
||||
|
||||
def load_env(env_path='.env'):
|
||||
config = {}
|
||||
if not os.path.exists(env_path):
|
||||
env_path = os.path.join('..', '.env')
|
||||
if not os.path.exists(env_path):
|
||||
print(f"Warning: {env_path} not found")
|
||||
return config
|
||||
|
||||
with open(env_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
config[key.strip()] = value.strip()
|
||||
return config
|
||||
|
||||
def run_migration():
|
||||
config = load_env()
|
||||
print("\nConnecting to licensing_risks database...")
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = pg8000.connect(
|
||||
user=config.get('LIC_PG_USER', 'postgres'),
|
||||
password=config.get('LIC_PG_PASSWORD'),
|
||||
host=config.get('LIC_PG_HOST', 'localhost'),
|
||||
port=int(config.get('LIC_PG_PORT', 5432)),
|
||||
database=config.get('LIC_PG_DATABASE', 'licensing_risks')
|
||||
)
|
||||
print("✅ Connection successful!")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# IDs identified
|
||||
theme_id = 'fc4b7e18-de60-4f58-9805-bac84097c00e'
|
||||
unique_permit_id = 'fd21d74d-0626-46a3-9f98-e0658d2ca206' # 印章刻制业许可证核发
|
||||
|
||||
print("\n--- Starting Deletion Process ---\n")
|
||||
|
||||
# 1. Delete Theme Association
|
||||
print(f"Deleting associations for theme {theme_id}...")
|
||||
cursor.execute("DELETE FROM region_theme_permits WHERE theme_id = %s", (theme_id,))
|
||||
print(f"Deleted rows from region_theme_permits.")
|
||||
|
||||
# 2. Delete Theme
|
||||
print(f"Deleting theme {theme_id}...")
|
||||
cursor.execute("DELETE FROM themes WHERE id = %s", (theme_id,))
|
||||
print(f"Deleted row from themes.")
|
||||
|
||||
# 3. Delete Unique Permit and its details
|
||||
print(f"Deleting unique permit {unique_permit_id} (印章刻制业许可证核发)...")
|
||||
|
||||
tables_to_clean = [
|
||||
'region_permit_details',
|
||||
'region_permit_risks',
|
||||
'region_permit_scopes',
|
||||
'region_permit_subitems'
|
||||
]
|
||||
|
||||
for table in tables_to_clean:
|
||||
print(f"Cleaning {table}...")
|
||||
cursor.execute(f"DELETE FROM {table} WHERE permit_id = %s", (unique_permit_id,))
|
||||
|
||||
print(f"Deleting permit from permits table...")
|
||||
cursor.execute("DELETE FROM permits WHERE id = %s", (unique_permit_id,))
|
||||
|
||||
# Commit transaction
|
||||
conn.commit()
|
||||
print("\n✅ Migration completed successfully and committed.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Database error: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
print("Transaction rolled back.")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_migration()
|
||||
Loading…
Reference in New Issue