import { Alpine } from "./alpine"; import { showToast } from "./toast"; // ============================================================ // Collection Rules Page - Auto-Assign Rule Management // ============================================================ let collectionId = ""; function initCollectionRules(id: string): void { collectionId = id; loadRules(); setupEventDelegation(); } function setupEventDelegation(): void { const container = document.getElementById("rules-container"); if (!container) return; container.addEventListener("click", (e) => { const target = e.target as HTMLElement; const button = target.closest("button") as HTMLButtonElement; if (!button) return; const action = button.dataset.action; const ruleId = button.dataset.ruleId; if (action === "toggle" && ruleId) { const enabled = button.dataset.enabled === "true"; toggleRule(ruleId, enabled); } else if (action === "delete" && ruleId) { deleteRule(ruleId); } }); } function backToCollection(): void { if (!collectionId) return; window.location.href = `/collections/${collectionId}`; } function getFieldLabel(field: string): string { const labels: Record = { genre: "Genre", series: "Series", author: "Author", language: "Language", publisher: "Publisher", copyright_year: "Copyright Year", tags: "Tags", }; return labels[field] || field; } function getOperatorLabel(operator: string): string { const labels: Record = { equals: "equals", not_equals: "does not equal", contains: "contains", not_contains: "does not contain", starts_with: "starts with", ends_with: "ends with", greater_than: "greater than", less_than: "less than", }; return labels[operator] || operator; } async function loadRules(): Promise { const token = localStorage.getItem("token"); if (!token || !collectionId) return; try { const response = await fetch(`/api/collections/${collectionId}`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const data = await response.json(); renderRules(data.auto_assign_rules || []); } } catch (error) { console.error("Failed to load rules", error); } } function renderRules(rules: CollectionRule[]): void { const container = document.getElementById("rules-container"); const noRulesDiv = document.getElementById("no-rules"); if (!container || !noRulesDiv) return; if (rules && rules.length > 0) { noRulesDiv.classList.add("hidden"); container.innerHTML = rules .map( (rule) => `
${getFieldLabel(rule.field)} ${rule.enabled ? "Enabled" : "Disabled"} Priority ${rule.priority}
${getOperatorLabel(rule.operator)} "${rule.value}"
`, ) .join(""); } else { noRulesDiv.classList.remove("hidden"); } } async function handleCreateRule(event: Event): Promise { event.preventDefault(); const token = localStorage.getItem("token"); if (!token || !collectionId) return; const fieldInput = document.getElementById("rule-field") as HTMLSelectElement; const operatorInput = document.getElementById( "rule-operator", ) as HTMLSelectElement; const valueInput = document.getElementById("rule-value") as HTMLInputElement; const enabledInput = document.getElementById( "rule-enabled", ) as HTMLInputElement; const priorityInput = document.querySelector( 'input[name="priority"]:checked', ) as HTMLInputElement; if ( !fieldInput || !operatorInput || !valueInput || !enabledInput || !priorityInput ) { showToast("Missing form fields", "error"); return; } const data = { field: fieldInput.value, operator: operatorInput.value, value: valueInput.value, enabled: enabledInput.checked, priority: parseInt(priorityInput.value, 10), }; try { const response = await fetch(`/api/collections/${collectionId}/rules`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(data), }); if (response.ok) { showToast("Rule created successfully", "success"); clearForm(); loadRules(); } else { showToast("Failed to create rule", "error"); } } catch (error) { console.error("Failed to create rule", error); showToast("Failed to create rule", "error"); } } async function testRule(): Promise { const token = localStorage.getItem("token"); if (!token) return; const fieldInput = document.getElementById("rule-field") as HTMLSelectElement; const operatorInput = document.getElementById( "rule-operator", ) as HTMLSelectElement; const valueInput = document.getElementById("rule-value") as HTMLInputElement; if (!fieldInput || !operatorInput || !valueInput) { showToast("Missing form fields", "error"); return; } const field = fieldInput.value; const operator = operatorInput.value; const value = valueInput.value; if (!field || !operator || !value) { showToast("Please fill in all rule fields", "error"); return; } const testResultsDiv = document.getElementById("test-results"); const resultsList = document.getElementById("test-results-list"); if (!testResultsDiv || !resultsList) return; testResultsDiv.classList.remove("hidden"); resultsList.innerHTML = '

Testing rule...

'; const rules = [ { field: field, operator: operator, value: value, }, ]; try { const response = await fetch("/api/collections/test-rules", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ rules: rules }), }); if (response.ok) { const result = await response.json(); if (result.matches && result.matches.length > 0) { let html = '

Found ' + result.matches.length + " matching books:

"; html += '
'; result.matches.slice(0, 20).forEach((book: TestRuleMatch) => { html += '
'; html += 'Cover'; html += '
'; html += '
' + book.title + "
"; if (book.author) { html += '
' + book.author + "
"; } if (book.match_reason) { html += '
' + book.match_reason + "
"; } html += "
"; html += "
"; }); if (result.matches.length > 20) { html += '

...and ' + (result.matches.length - 20) + " more

"; } html += "
"; resultsList.innerHTML = html; } else { resultsList.innerHTML = '

No books match this rule

'; } } else { resultsList.innerHTML = '

Failed to test rule

'; } } catch (error) { console.error("Failed to test rule", error); resultsList.innerHTML = '

Failed to test rule

'; } } async function toggleRule(ruleId: string, enabled: boolean): Promise { const token = localStorage.getItem("token"); if (!token || !collectionId) return; try { const response = await fetch( `/api/collections/${collectionId}/rules/${ruleId}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ enabled }), }, ); if (response.ok) { showToast("Rule updated", "success"); loadRules(); } else { showToast("Failed to update rule", "error"); } } catch (error) { console.error("Failed to update rule", error); showToast("Failed to update rule", "error"); } } async function deleteRule(ruleId: string): Promise { if (!confirm("Are you sure you want to delete this rule?")) return; const token = localStorage.getItem("token"); if (!token || !collectionId) return; try { const response = await fetch( `/api/collections/${collectionId}/rules/${ruleId}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }, ); if (response.ok) { showToast("Rule deleted", "success"); loadRules(); } else { showToast("Failed to delete rule", "error"); } } catch (error) { console.error("Failed to delete rule", error); showToast("Failed to delete rule", "error"); } } function clearForm(): void { const fieldInput = document.getElementById("rule-field") as HTMLSelectElement; const operatorInput = document.getElementById( "rule-operator", ) as HTMLSelectElement; const valueInput = document.getElementById("rule-value") as HTMLInputElement; const enabledInput = document.getElementById( "rule-enabled", ) as HTMLInputElement; const priorityInput = document.querySelector( 'input[name="priority"][value="2"]', ) as HTMLInputElement; const testResultsDiv = document.getElementById("test-results"); if (fieldInput) fieldInput.value = ""; if (operatorInput) operatorInput.value = ""; if (valueInput) valueInput.value = ""; if (enabledInput) enabledInput.checked = true; if (priorityInput) priorityInput.checked = true; if (testResultsDiv) testResultsDiv.classList.add("hidden"); } function logout(): void { localStorage.removeItem("token"); window.location.href = "/login"; } // ============================================================ // Exports // ============================================================ export { backToCollection, clearForm, deleteRule, getFieldLabel, getOperatorLabel, handleCreateRule, initCollectionRules, loadRules, logout, renderRules, setupEventDelegation, testRule, toggleRule, }; Alpine.store("collectionRules", { backToCollection, clearForm, deleteRule, getFieldLabel, getOperatorLabel, handleCreateRule, initCollectionRules, loadRules, logout, renderRules, setupEventDelegation, testRule, toggleRule, });