Fix case tabs fallback and harden sag email-links loading

This commit is contained in:
Christian 2026-04-02 21:44:56 +02:00
parent 9be8b57303
commit ae6217b976
2 changed files with 138 additions and 41 deletions

View File

@ -3342,50 +3342,120 @@ async def add_sag_email_link(sag_id: int, payload: dict):
@router.get("/sag/{sag_id}/email-links")
async def get_sag_emails(sag_id: int):
"""Get emails linked to a case."""
query = """
WITH linked_emails AS (
if not _table_exists("sag_emails") or not _table_exists("email_messages"):
logger.warning("⚠️ Email links requested for SAG-%s but required tables are missing", sag_id)
return []
has_thread_key = table_has_column("email_messages", "thread_key")
has_email_references = table_has_column("email_messages", "email_references")
has_in_reply_to = table_has_column("email_messages", "in_reply_to")
has_subject = table_has_column("email_messages", "subject")
has_message_id = table_has_column("email_messages", "message_id")
has_folder = table_has_column("email_messages", "folder")
has_status = table_has_column("email_messages", "status")
has_received_date = table_has_column("email_messages", "received_date")
thread_key_expr = "e.thread_key" if has_thread_key else "NULL"
email_references_expr = "e.email_references" if has_email_references else "NULL"
in_reply_to_expr = "e.in_reply_to" if has_in_reply_to else "NULL"
subject_expr = "e.subject" if has_subject else "NULL"
message_id_expr = "e.message_id" if has_message_id else "NULL"
outgoing_checks = []
if has_folder:
outgoing_checks.append("LOWER(COALESCE(linked_emails.folder, '')) LIKE 'sent%%'")
if has_status:
outgoing_checks.append("LOWER(COALESCE(linked_emails.status, '')) = 'sent'")
is_outgoing_expr = " OR ".join(outgoing_checks) if outgoing_checks else "FALSE"
if has_received_date:
query = f"""
WITH linked_emails AS (
SELECT
e.*,
COALESCE(
NULLIF(REGEXP_REPLACE(TRIM(COALESCE({thread_key_expr}, '')), '[<>\\s]', '', 'g'), ''),
NULLIF(REGEXP_REPLACE((REGEXP_SPLIT_TO_ARRAY(COALESCE({email_references_expr}, ''), E'[\\s,]+'))[1], '[<>\\s]', '', 'g'), ''),
NULLIF(
REGEXP_REPLACE(
(REGEXP_SPLIT_TO_ARRAY(COALESCE({in_reply_to_expr}, ''), E'[\\s,]+'))[1],
'[<>\\s]',
'',
'g'
),
''
),
NULLIF(
REGEXP_REPLACE(
LOWER(TRIM(COALESCE({subject_expr}, ''))),
'^(?:(?:re|fw|fwd|sv|aw)\\s*:\\s*)+',
'',
'i'
),
''
),
NULLIF(REGEXP_REPLACE(TRIM(COALESCE({message_id_expr}, '')), '[<>\\s]', '', 'g'), ''),
CONCAT('email-', e.id::text)
) AS resolved_thread_key
FROM email_messages e
JOIN sag_emails se ON e.id = se.email_id
WHERE se.sag_id = %s
)
SELECT
e.*,
COALESCE(
NULLIF(REGEXP_REPLACE(TRIM(COALESCE(e.thread_key, '')), '[<>\\s]', '', 'g'), ''),
NULLIF(REGEXP_REPLACE((REGEXP_SPLIT_TO_ARRAY(COALESCE(e.email_references, ''), E'[\\s,]+'))[1], '[<>\\s]', '', 'g'), ''),
NULLIF(
REGEXP_REPLACE(
(REGEXP_SPLIT_TO_ARRAY(COALESCE(e.in_reply_to, ''), E'[\\s,]+'))[1],
'[<>\\s]',
'',
'g'
linked_emails.*,
({is_outgoing_expr}) AS is_outgoing,
COUNT(*) OVER (PARTITION BY linked_emails.resolved_thread_key) AS thread_message_count,
MAX(linked_emails.received_date) OVER (PARTITION BY linked_emails.resolved_thread_key) AS thread_last_received_date
FROM linked_emails
ORDER BY thread_last_received_date DESC NULLS LAST, received_date DESC
"""
else:
query = f"""
WITH linked_emails AS (
SELECT
e.*,
COALESCE(
NULLIF(REGEXP_REPLACE(TRIM(COALESCE({thread_key_expr}, '')), '[<>\\s]', '', 'g'), ''),
NULLIF(REGEXP_REPLACE((REGEXP_SPLIT_TO_ARRAY(COALESCE({email_references_expr}, ''), E'[\\s,]+'))[1], '[<>\\s]', '', 'g'), ''),
NULLIF(
REGEXP_REPLACE(
(REGEXP_SPLIT_TO_ARRAY(COALESCE({in_reply_to_expr}, ''), E'[\\s,]+'))[1],
'[<>\\s]',
'',
'g'
),
''
),
''
),
NULLIF(
REGEXP_REPLACE(
LOWER(TRIM(COALESCE(e.subject, ''))),
'^(?:(?:re|fw|fwd|sv|aw)\\s*:\\s*)+',
'',
'i'
NULLIF(
REGEXP_REPLACE(
LOWER(TRIM(COALESCE({subject_expr}, ''))),
'^(?:(?:re|fw|fwd|sv|aw)\\s*:\\s*)+',
'',
'i'
),
''
),
''
),
NULLIF(REGEXP_REPLACE(TRIM(COALESCE(e.message_id, '')), '[<>\\s]', '', 'g'), ''),
CONCAT('email-', e.id::text)
) AS resolved_thread_key
FROM email_messages e
JOIN sag_emails se ON e.id = se.email_id
WHERE se.sag_id = %s
)
SELECT
linked_emails.*,
(
LOWER(COALESCE(linked_emails.folder, '')) LIKE 'sent%%'
OR LOWER(COALESCE(linked_emails.status, '')) = 'sent'
) AS is_outgoing,
COUNT(*) OVER (PARTITION BY linked_emails.resolved_thread_key) AS thread_message_count,
MAX(linked_emails.received_date) OVER (PARTITION BY linked_emails.resolved_thread_key) AS thread_last_received_date
FROM linked_emails
ORDER BY thread_last_received_date DESC NULLS LAST, received_date DESC
"""
return execute_query(query, (sag_id,)) or []
NULLIF(REGEXP_REPLACE(TRIM(COALESCE({message_id_expr}, '')), '[<>\\s]', '', 'g'), ''),
CONCAT('email-', e.id::text)
) AS resolved_thread_key
FROM email_messages e
JOIN sag_emails se ON e.id = se.email_id
WHERE se.sag_id = %s
)
SELECT
linked_emails.*,
({is_outgoing_expr}) AS is_outgoing,
COUNT(*) OVER (PARTITION BY linked_emails.resolved_thread_key) AS thread_message_count,
NULL::timestamp AS thread_last_received_date
FROM linked_emails
ORDER BY linked_emails.id DESC
"""
try:
return execute_query(query, (sag_id,)) or []
except Exception as exc:
logger.error("❌ Failed loading linked emails for SAG-%s: %s", sag_id, exc)
return []
@router.delete("/sag/{sag_id}/email-links/{email_id}")
async def remove_sag_email_link(sag_id: int, email_id: int):

View File

@ -2497,6 +2497,33 @@
</div>
<!-- ═══════════════ END CASE HEADER ═══════════════ -->
<script>
// Defensive bootstrap: keep core tab/module globals available even if a later
// inline script fails to parse due template edge-cases.
var caseTypeModuleDefaults = window.caseTypeModuleDefaults || {};
window.caseTypeModuleDefaults = caseTypeModuleDefaults;
window.forceCaseTabActivation = window.forceCaseTabActivation || function(tabId) {
if (!tabId) return;
const tabContent = document.getElementById('caseTabsContent');
const targetPane = document.getElementById(tabId);
if (!tabContent || !targetPane) return;
tabContent.querySelectorAll(':scope > .tab-pane').forEach((pane) => {
pane.classList.remove('show', 'active');
pane.style.display = 'none';
});
targetPane.classList.add('show', 'active');
targetPane.style.display = 'block';
const tabButtons = document.querySelectorAll('#caseTabs [data-bs-target]');
tabButtons.forEach((btn) => {
btn.classList.toggle('active', btn.getAttribute('data-bs-target') === `#${tabId}`);
});
};
</script>
<!-- Tabs Navigation -->
<ul class="nav nav-tabs mb-4" id="caseTabs" role="tablist">
<li class="nav-item" role="presentation">