diff --git a/lawrisk/services/licensing_repo.py b/lawrisk/services/licensing_repo.py
index be78160..2a1ae7f 100644
--- a/lawrisk/services/licensing_repo.py
+++ b/lawrisk/services/licensing_repo.py
@@ -45,7 +45,8 @@ COLON_NEWLINE_RE = re.compile(r":\s*\n")
TRAILING_SPACE_RE = re.compile(r"[ \t]+\n")
EXTRA_NEWLINES_RE = re.compile(r"\n{3,}")
-TEXT_SPLIT_PATTERN = re.compile(r"[,\uff0c;\uff1b、\n\r]+")
+TEXT_SPLIT_PATTERN = re.compile(r"[,\uff0c;\uff1b\n\r]+")
+TEXT_SPLIT_PATTERN_WITH_DUNHAO = re.compile(r"[,\uff0c;\uff1b、\n\r]+")
PERMIT_IMPORT_TTL_SECONDS = 1800
MAX_PERMIT_FILE_SIZE_BYTES = 500 * 1024 # 500 KB limit for uploaded Excel files
@@ -260,12 +261,19 @@ def _score_import_header(canonical: str, cell_text: str, col_idx: int) -> float:
return score
-def _split_multi_value(value: Any) -> List[str]:
- """Split multi-value cells using common Chinese punctuation."""
+def _split_multi_value(value: Any, *, allow_dunhao: bool = False) -> List[str]:
+ """Split multi-value cells using common punctuation characters.
+
+ 默认不把中文顿号(、)视作分隔符,以避免误拆“文化、旅游”等合法的
+ 许可名称。对于确实需要用顿号分隔的字段(如主题、经营范围等),调用
+ 方可以显式传入 allow_dunhao=True。
+ """
+
text = _clean_text(value)
if not text:
return []
- return [item.strip() for item in TEXT_SPLIT_PATTERN.split(text) if item.strip()]
+ pattern = TEXT_SPLIT_PATTERN_WITH_DUNHAO if allow_dunhao else TEXT_SPLIT_PATTERN
+ return [item.strip() for item in pattern.split(text) if item.strip()]
def _clean_empty(value: Any) -> Optional[str]:
@@ -311,9 +319,15 @@ def _normalize_import_row(
jurisdiction_scope = _clean_empty(
raw_row.get("jurisdiction_scope") or sheet_defaults.get("jurisdiction_scope")
)
- theme_names = _split_multi_value(raw_row.get("theme_names") or sheet_defaults.get("theme_names"))
- scope_descriptions = _split_multi_value(raw_row.get("scope_text") or sheet_defaults.get("scope_text"))
- subitem_names = _split_multi_value(raw_row.get("subitem_text") or sheet_defaults.get("subitem_text"))
+ theme_names = _split_multi_value(
+ raw_row.get("theme_names") or sheet_defaults.get("theme_names"), allow_dunhao=True
+ )
+ scope_descriptions = _split_multi_value(
+ raw_row.get("scope_text") or sheet_defaults.get("scope_text"), allow_dunhao=True
+ )
+ subitem_names = _split_multi_value(
+ raw_row.get("subitem_text") or sheet_defaults.get("subitem_text"), allow_dunhao=True
+ )
return {
"row_index": int(row_index) if isinstance(row_index, int) else row_index,
@@ -1700,6 +1714,68 @@ def list_permits_for_region(region: str) -> List[Dict[str, str]]:
return permits
+def list_region_permit_catalog(region_id: str) -> List[Dict[str, Any]]:
+ """Return permit entries for a region, including owning theme and risk count."""
+ sql = """
+ SELECT
+ rtp.permit_id,
+ p.name AS permit_name,
+ rtp.theme_id,
+ COALESCE(t.name, '') AS theme_name,
+ COUNT(rpr.risk_id) AS risk_count
+ 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_risks rpr
+ ON rpr.region_id = rtp.region_id
+ AND rpr.permit_id = rtp.permit_id
+ WHERE rtp.region_id = %s
+ GROUP BY rtp.permit_id, p.name, rtp.theme_id, t.name
+ ORDER BY LOWER(p.name), LOWER(COALESCE(t.name, ''))
+ """
+ catalog: List[Dict[str, Any]] = []
+ with _lic_pg_conn() as conn:
+ cur = conn.cursor()
+ cur.execute(sql, (region_id,))
+ for permit_id, permit_name, theme_id, theme_name, risk_count in cur.fetchall():
+ catalog.append(
+ {
+ "id": str(permit_id),
+ "name": str(permit_name),
+ "theme": {
+ "id": str(theme_id) if theme_id else "",
+ "name": str(theme_name) if theme_name else "",
+ },
+ "risk_count": int(risk_count or 0),
+ }
+ )
+ return catalog
+
+
+def resolve_region_permit_theme(region_id: str, permit_id: str) -> Optional[Dict[str, str]]:
+ """Return the first theme associated with a region-permit pair (if any)."""
+ sql = """
+ SELECT rtp.theme_id, t.name
+ FROM region_theme_permits rtp
+ LEFT JOIN themes t ON t.id = rtp.theme_id
+ WHERE rtp.region_id = %s
+ AND rtp.permit_id = %s
+ ORDER BY t.name NULLS LAST
+ LIMIT 1
+ """
+ with _lic_pg_conn() as conn:
+ cur = conn.cursor()
+ cur.execute(sql, (region_id, permit_id))
+ row = cur.fetchone()
+ if not row:
+ return None
+ theme_id, theme_name = row
+ return {
+ "id": str(theme_id) if theme_id else "",
+ "name": str(theme_name) if theme_name else "",
+ }
+
+
def _load_permit_scopes_for_region(
conn: pg.Connection, region_id: str, permit_ids: List[str]
) -> Dict[str, List[Dict[str, str]]]:
@@ -1766,9 +1842,9 @@ def _load_permit_sources_for_region(
def load_permits_and_risks(
- region_id: str, theme_id: str, permit_id: Optional[str] = None
+ region_id: str, theme_id: Optional[str] = None, permit_id: Optional[str] = None
) -> List[Dict[str, object]]:
- """Return permits with attached risk entries for a region-theme pair."""
+ """Return permits with attached risk entries for a region (optionally filtered by theme)."""
# Ensure optional permit file tables exist before running user queries.
try:
_ensure_permit_file_schema()
@@ -1776,6 +1852,8 @@ def load_permits_and_risks(
logger.warning("[PERMIT-FILES] Failed to ensure permit file schema before loading permits: %s", exc)
sql = """
SELECT
+ rtp.theme_id,
+ t.name AS theme_name,
p.id AS permit_id,
p.name AS permit_name,
rk.id AS risk_id,
@@ -1789,6 +1867,7 @@ def load_permits_and_risks(
rpd.jurisdiction_scope
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_risks rpr
ON rpr.region_id = rtp.region_id
AND rpr.permit_id = rtp.permit_id
@@ -1796,9 +1875,12 @@ def load_permits_and_risks(
LEFT JOIN region_permit_details rpd
ON rpd.region_id = rtp.region_id
AND rpd.permit_id = rtp.permit_id
- WHERE rtp.region_id = %s AND rtp.theme_id = %s
+ WHERE rtp.region_id = %s
"""
- params: List[Any] = [region_id, theme_id]
+ params: List[Any] = [region_id]
+ if theme_id:
+ sql += " AND rtp.theme_id = %s"
+ params.append(theme_id)
if permit_id is not None:
sql += " AND rtp.permit_id = %s"
params.append(permit_id)
@@ -1812,6 +1894,8 @@ def load_permits_and_risks(
cur.execute(sql, tuple(params))
for row in cur.fetchall():
(
+ row_theme_id,
+ row_theme_name,
permit_id,
permit_name,
risk_id,
@@ -1825,6 +1909,8 @@ def load_permits_and_risks(
jurisdiction_scope,
) = row
pid = str(permit_id)
+ theme_id_value = str(row_theme_id) if row_theme_id else ""
+ theme_name_value = str(row_theme_name) if row_theme_name else ""
entry = permits.setdefault(
pid,
{
@@ -1836,8 +1922,37 @@ def load_permits_and_risks(
"subitem_summary": None,
"responsible_contact": None,
"jurisdiction_scope": None,
+ "theme": {
+ "id": theme_id_value,
+ "name": theme_name_value,
+ },
+ "themes": [],
},
)
+ if theme_id_value and not entry["theme"].get("id"):
+ entry["theme"]["id"] = theme_id_value
+ if theme_name_value and not entry["theme"].get("name"):
+ entry["theme"]["name"] = theme_name_value
+ if theme_id_value or theme_name_value:
+ theme_list = entry.get("themes") or []
+ duplicate = False
+ for theme_entry in theme_list:
+ if theme_id_value:
+ if theme_entry.get("id") == theme_id_value:
+ duplicate = True
+ break
+ else:
+ if not theme_entry.get("id") and theme_entry.get("name") == theme_name_value:
+ duplicate = True
+ break
+ if not duplicate:
+ theme_list.append(
+ {
+ "id": theme_id_value,
+ "name": theme_name_value,
+ }
+ )
+ entry["themes"] = theme_list
if entry["permit_status"] is None and permit_status:
entry["permit_status"] = permit_status.strip() or None
if entry["subitem_summary"] is None and subitem_summary:
@@ -1895,6 +2010,8 @@ def load_permits_and_risks(
"created_at": None,
"uploaded_by": "",
}
+ if "themes" not in permits[pid] or permits[pid]["themes"] is None:
+ permits[pid]["themes"] = []
return list(permits.values())
diff --git a/static/db_admin.html b/static/db_admin.html
index 4d60a35..6420c5b 100644
--- a/static/db_admin.html
+++ b/static/db_admin.html
@@ -366,6 +366,23 @@
font-weight: 500;
}
+ .item-tag {
+ display: inline-flex;
+ align-items: center;
+ margin-left: 8px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ background: rgba(102, 126, 234, 0.15);
+ font-size: 11px;
+ color: #4f46e5;
+ }
+
+ .theme-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ }
+
.item-count {
font-size: 12px;
background: rgba(102, 126, 234, 0.1);
@@ -1508,21 +1525,16 @@
@@ -1540,7 +1552,7 @@
- 选择区域
+ 选择区划
@@ -1558,7 +1570,7 @@
- 请选择区域开始导航
+
请选择区划开始导航
@@ -1701,10 +1713,9 @@
}
// 导航状态管理
- let currentStep = 1; // 1=区域, 2=主题, 3=许可, 4=详情
+ let currentStep = 1; // 1=区划, 2=事项, 3=详情
let historyStack = []; // 历史记录栈
let currentRegion = null;
- let currentTheme = null;
let currentPermit = null;
let currentPermitDetails = null;
let pendingDangerOperation = null; // 待执行的危险操作
@@ -1791,16 +1802,15 @@
// 步骤配置
const steps = {
- 1: { title: '选择区域', loadData: loadRegions },
- 2: { title: '选择主题', loadData: (region) => loadThemes(region.id, region.name) },
- 3: { title: '选择许可', loadData: (theme) => loadPermits(theme.id, theme.name) },
- 4: { title: '许可详情', loadData: null }
+ 1: { title: '选择区划' },
+ 2: { title: '选择事项' },
+ 3: { title: '事项详情' }
};
- // 加载地区列表
+ // 加载区划列表
async function loadRegions() {
const navList = document.getElementById('navList');
- navList.innerHTML = '加载地区列表...';
+ navList.innerHTML = '加载区划列表...';
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/regions');
@@ -1810,7 +1820,7 @@
navList.innerHTML = '';
if (data.data.regions.length === 0) {
- navList.innerHTML = '未找到地区数据
';
+ navList.innerHTML = '未找到区划数据
';
return;
}
@@ -1824,72 +1834,54 @@
navList.appendChild(li);
});
} else {
- navList.innerHTML = `加载地区失败:${data.message}
`;
+ navList.innerHTML = `加载区划失败:${data.message}
`;
}
} catch (error) {
navList.innerHTML = `网络错误:${error.message}
`;
}
}
- // 加载主题列表
- async function loadThemes(regionId, regionName) {
- const navList = document.getElementById('navList');
- navList.innerHTML = '加载主题列表...';
-
- try {
- const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/themes?region=${regionId}`);
- const data = await response.json();
-
- if (data.success) {
- const themes = data.data.themes;
-
- if (themes.length === 0) {
- navList.innerHTML = `地区 "${regionName}" 下没有可用的主题
`;
- return;
- }
-
- let html = '';
- themes.forEach(theme => {
- html += `
-
- ${theme.name}
- 点击选择
-
- `;
- });
- html += '';
- navList.innerHTML = html;
- } else {
- navList.innerHTML = `加载主题失败:${data.message}
`;
- }
- } catch (error) {
- navList.innerHTML = `网络错误:${error.message}
`;
+ // 加载许可列表(直接按区划聚合事项)
+ async function loadPermitsForRegion() {
+ if (!currentRegion) {
+ return;
}
- }
-
- // 加载许可列表
- async function loadPermits(themeId, themeName) {
const navList = document.getElementById('navList');
- navList.innerHTML = '加载许可列表...';
+ navList.innerHTML = '加载事项列表...';
try {
- const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}&theme=${themeId}`);
+ const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}`);
const data = await response.json();
if (data.success) {
const permits = data.data.permits;
if (permits.length === 0) {
- navList.innerHTML = `主题 "${themeName}" 下没有可用的许可
`;
+ navList.innerHTML = `区划 "${currentRegion.name}" 暂无可维护的事项
`;
return;
}
let html = '';
permits.forEach(permit => {
- const riskCount = permit.risks ? permit.risks.length : 0;
+ const rawRiskCount = typeof permit.risk_count === 'number'
+ ? permit.risk_count
+ : (Array.isArray(permit.risks) ? permit.risks.length : 0);
+ const riskCount = Number.isFinite(rawRiskCount) ? rawRiskCount : 0;
+ const themeId = permit.theme && permit.theme.id
+ ? permit.theme.id
+ : (permit.theme_id || '');
+ const themeName = permit.theme && permit.theme.name
+ ? permit.theme.name
+ : (permit.theme_name || '');
+ const themeBadge = themeName
+ ? `${themeName}`
+ : '';
+ const escapedName = permit.name ? permit.name.replace(/'/g, "\\'") : '';
+ const escapedTheme = themeName ? themeName.replace(/'/g, "\\'") : '';
+ const escapedThemeId = themeId ? themeId.replace(/'/g, "\\'") : '';
html += `
-
- ${permit.name}
+
+ ${permit.name}${themeBadge}
${riskCount} 个风险
`;
@@ -1897,7 +1889,7 @@
html += '';
navList.innerHTML = html;
} else {
- navList.innerHTML = `加载许可失败:${data.message}
`;
+ navList.innerHTML = `加载事项失败:${data.message}
`;
}
} catch (error) {
navList.innerHTML = `网络错误:${error.message}
`;
@@ -1910,7 +1902,6 @@
historyStack.push({ step: currentStep, region: currentRegion });
currentRegion = { id: regionId, name: regionName };
- currentTheme = null;
currentPermit = null;
currentPermitDetails = null;
@@ -1918,31 +1909,24 @@
goToStep(2);
}
- // 选择主题
- async function selectTheme(themeId, themeName) {
+ // 选择许可
+ async function selectPermit(permitId, permitName, themeId, themeName, riskCount = 0) {
// 保存到历史栈
- historyStack.push({ step: currentStep, theme: currentTheme });
+ historyStack.push({ step: currentStep, permit: currentPermit });
- currentTheme = { id: themeId, name: themeName };
- currentPermit = null;
+ currentPermit = {
+ id: permitId,
+ name: permitName,
+ themeId: themeId || '',
+ themeName: themeName || '',
+ riskCount: typeof riskCount === 'number' ? riskCount : 0
+ };
currentPermitDetails = null;
// 更新步骤
goToStep(3);
}
- // 选择许可
- async function selectPermit(permitId, permitName, themeId) {
- // 保存到历史栈
- historyStack.push({ step: currentStep, permit: currentPermit });
-
- currentPermit = { id: permitId, name: permitName, themeId: themeId };
- currentPermitDetails = null;
-
- // 更新步骤
- goToStep(4);
- }
-
// 跳转到指定步骤
async function goToStep(step) {
currentStep = step;
@@ -1977,10 +1961,8 @@
if (step === 1) {
await loadRegions();
} else if (step === 2) {
- await loadThemes(currentRegion.id, currentRegion.name);
+ await loadPermitsForRegion();
} else if (step === 3) {
- await loadPermits(currentTheme.id, currentTheme.name);
- } else if (step === 4) {
await showPermitDetails();
}
}
@@ -1988,16 +1970,12 @@
// 更新面包屑导航
function updateBreadcrumb() {
const breadcrumb = document.getElementById('breadcrumb');
- let html = '';
-
- // 总是显示"首页"
- html += `
+ let html = `
首页
`;
- // 显示当前选择的路径
if (currentRegion) {
html += '›';
if (currentStep > 2) {
@@ -2015,35 +1993,21 @@
}
}
- if (currentTheme) {
- html += '›';
- if (currentStep > 3) {
- html += `
-
- ${currentTheme.name}
-
- `;
- } else {
- html += `
-
- ${currentTheme.name}
-
- `;
- }
- }
-
if (currentPermit) {
+ const permitLabel = currentPermit.themeName
+ ? `${currentPermit.themeName} · ${currentPermit.name}`
+ : currentPermit.name;
html += '›';
- if (currentStep > 4) {
+ if (currentStep >= 3) {
html += `
- ${currentPermit.name}
+ ${permitLabel}
`;
} else {
html += `
- ${currentPermit.name}
+ ${permitLabel}
`;
}
@@ -2061,9 +2025,6 @@
// 清理后续状态
if (targetStep <= 2) {
- currentTheme = null;
- }
- if (targetStep <= 3) {
currentPermit = null;
currentPermitDetails = null;
}
@@ -2074,7 +2035,6 @@
// 返回首页
function goHome() {
currentRegion = null;
- currentTheme = null;
currentPermit = null;
currentPermitDetails = null;
historyStack = [];
@@ -2087,7 +2047,14 @@
detailsArea.innerHTML = '加载许可详情...';
try {
- const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?region=${currentRegion.id}&theme=${currentPermit.themeId}&permit=${currentPermit.id}`);
+ if (!currentRegion || !currentPermit) {
+ detailsArea.innerHTML = '请选择区划和事项后查看详情
';
+ return;
+ }
+ const themePart = currentPermit.themeId
+ ? `&theme=${encodeURIComponent(currentPermit.themeId)}`
+ : '';
+ const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?region=${currentRegion.id}&permit=${currentPermit.id}${themePart}`);
const data = await response.json();
if (data.success) {
@@ -2108,7 +2075,6 @@
// 恢复状态
if (prev.region) currentRegion = prev.region;
- if (prev.theme) currentTheme = prev.theme;
if (prev.permit) currentPermit = prev.permit;
// 跳转到上一步
@@ -2120,8 +2086,14 @@
const detailsArea = document.querySelector('.details-area');
currentPermitDetails = permit;
const riskCount = Array.isArray(permit.risks) ? permit.risks.length : 0;
+ const permitTheme = permit && permit.theme ? permit.theme : {};
if (currentPermit) {
- currentPermit = { ...currentPermit, riskCount };
+ currentPermit = {
+ ...currentPermit,
+ riskCount,
+ themeId: permitTheme.id || currentPermit.themeId || '',
+ themeName: permitTheme.name || currentPermit.themeName || ''
+ };
}
const deleteButtonLabel = isDeletingPermit ? '删除中...' : '删除许可';
const deleteButtonDisabled = isDeletingPermit ? 'disabled' : '';
@@ -2141,6 +2113,40 @@
const fileInfoText = hasPermitFile
? `${escapeHtml(permitFile.filename || '原始文件')}(${formatFileSize(permitFile.file_size)}${fileUploadedAt ? ` | ${fileUploadedAt}` : ''}${fileUploadedBy ? ` | 上传:${fileUploadedBy}` : ''})`
: '暂无关联文件';
+ const rawThemeList = Array.isArray(permit.themes) ? permit.themes.filter(Boolean) : [];
+ if (permitTheme && (permitTheme.id || permitTheme.name)) {
+ rawThemeList.push(permitTheme);
+ }
+ if (currentPermit && (currentPermit.themeId || currentPermit.themeName)) {
+ rawThemeList.push({
+ id: currentPermit.themeId || '',
+ name: currentPermit.themeName || '',
+ });
+ }
+ const themeList = [];
+ const seenThemeKeys = new Set();
+ rawThemeList.forEach(themeItem => {
+ if (!themeItem) {
+ return;
+ }
+ const id = typeof themeItem.id === 'string' ? themeItem.id.trim() : (themeItem.id ? String(themeItem.id) : '');
+ const name = typeof themeItem.name === 'string' ? themeItem.name.trim() : (themeItem.name ? String(themeItem.name) : '');
+ if (!id && !name) {
+ return;
+ }
+ const key = id || `name:${name}`;
+ if (seenThemeKeys.has(key)) {
+ return;
+ }
+ seenThemeKeys.add(key);
+ themeList.push({
+ id,
+ name: name || id || '未命名主题',
+ });
+ });
+ const themeListDisplay = themeList.length
+ ? `${themeList.map(themeItem => `${escapeHtml(themeItem.name)}`).join('')}
`
+ : '暂无主题关联
';
let html = '';
html += `
@@ -2171,6 +2177,15 @@
`;
+ html += `
+
+
所属主题
+
+ ${themeListDisplay}
+
+
+ `;
+
// 经营范围
html += '经营范围
';
if (permit.business_scopes && permit.business_scopes.length > 0) {
@@ -2223,7 +2238,7 @@
if (isDeletingPermit) {
return;
}
- if (!currentRegion || !currentTheme || !currentPermit) {
+ if (!currentRegion || !currentPermit) {
alert('请先选择要删除的许可');
return;
}
@@ -2231,7 +2246,12 @@
const riskCount = currentPermit.riskCount !== undefined
? currentPermit.riskCount
: (currentPermitDetails && Array.isArray(currentPermitDetails.risks) ? currentPermitDetails.risks.length : 0);
- const confirmMessage = `确定要删除「${currentRegion.name} › ${currentTheme.name} › ${currentPermit.name}」吗?\n\n` +
+ const pathParts = [currentRegion.name];
+ if (currentPermit.themeName) {
+ pathParts.push(currentPermit.themeName);
+ }
+ pathParts.push(currentPermit.name);
+ const confirmMessage = `确定要删除「${pathParts.join(' › ')}」吗?\n\n` +
`此操作会删除 ${riskCount} 条风险关联(如存在)。系统会备份当前风险快照,但删除后需通过快照管理页面手动恢复。`;
if (!confirm(confirmMessage)) {
@@ -2251,7 +2271,7 @@
if (isDeletingPermit) {
return;
}
- if (!currentRegion || !currentTheme || !currentPermit) {
+ if (!currentRegion || !currentPermit) {
alert('当前上下文缺失,无法删除');
return;
}
@@ -2262,9 +2282,11 @@
try {
const payload = {
region_id: currentRegion.id,
- theme_id: currentTheme.id,
permit_id: currentPermit.id
};
+ if (currentPermit.themeId) {
+ payload.theme_id = currentPermit.themeId;
+ }
if (changeSummary) {
payload.change_summary = changeSummary;
}
@@ -2333,8 +2355,11 @@
// 更新步骤指示器
function updateStepIndicator(step) {
- for (let i = 1; i <= 4; i++) {
+ for (let i = 1; i <= 3; i++) {
const stepElement = document.getElementById(`step${i}`);
+ if (!stepElement) {
+ continue;
+ }
if (i <= step) {
stepElement.classList.add('active');
} else {
@@ -3526,8 +3551,8 @@
const targetRegionId = groupItems[0].region_id;
const targetPermitId = groupItems[0].permit_id;
if (currentRegion && currentRegion.id === targetRegionId) {
- if (currentTheme) {
- await loadPermits(currentTheme.id, currentTheme.name);
+ if (currentStep >= 2) {
+ await loadPermitsForRegion();
}
if (currentPermit && currentPermit.id === targetPermitId) {
await showPermitDetails();