+FEATURE: double entry info

This commit is contained in:
Simon Martens
2026-01-11 12:02:05 +01:00
parent 6175e9c99a
commit ce7e03e983
10 changed files with 590 additions and 310 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -18,6 +18,7 @@ type AlmanachResult struct {
-->
<almanach-edit-page>
<duplicate-warning-checker></duplicate-warning-checker>
<div class="flex container-normal bg-slate-100 mx-auto px-8">
<div class="flex flex-row w-full justify-between">
@@ -166,7 +167,11 @@ type AlmanachResult struct {
<div class="inputlabelrow">
<label for="preferred_title" class="inputlabel">Kurztitel</label>
</div>
<textarea name="preferred_title" id="preferred_title" class="inputinput no-enter" placeholder="" required autocomplete="off" rows="1">{{- $model.result.Entry.PreferredTitle -}}</textarea>
<textarea name="preferred_title" id="preferred_title" class="inputinput no-enter" placeholder="" required autocomplete="off" rows="1" data-duplicate-check data-duplicate-endpoint="/api/entries/search" data-duplicate-result-key="entries" data-duplicate-current-id="{{ if not $model.is_new }}{{ $model.result.Entry.Id }}{{ end }}">{{- $model.result.Entry.PreferredTitle -}}</textarea>
<div class="duplicate-warning hidden" data-duplicate-warning-for="preferred_title">
<i class="ri-information-line"></i>
<span data-duplicate-count></span>
</div>
</div>
<div class="mt-3">

View File

@@ -2,6 +2,7 @@
{{ $place := $model.result.Place }}
<edit-page>
<duplicate-warning-checker></duplicate-warning-checker>
<div class="flex container-normal bg-slate-100 mx-auto px-8">
<div class="flex flex-row w-full justify-between">
<div class="flex flex-col justify-end-safe flex-2/5">
@@ -115,7 +116,11 @@
<div class="inputlabelrow">
<label for="name" class="inputlabel">Name</label>
</div>
<textarea name="name" id="name" class="inputinput no-enter" autocomplete="off" rows="1">{{- $place.Name -}}</textarea>
<textarea name="name" id="name" class="inputinput no-enter" autocomplete="off" rows="1" data-duplicate-check data-duplicate-endpoint="/api/places/search" data-duplicate-result-key="places" data-duplicate-current-id="{{ if not $model.is_new }}{{ $place.Id }}{{ end }}">{{- $place.Name -}}</textarea>
<div class="duplicate-warning hidden" data-duplicate-warning-for="name">
<i class="ri-information-line"></i>
<span data-duplicate-count></span>
</div>
</div>
<div class="inputwrapper">
<div class="inputlabelrow">

View File

@@ -2,6 +2,7 @@
{{ $agent := $model.result.Agent }}
<edit-page>
<duplicate-warning-checker></duplicate-warning-checker>
<div class="flex container-normal bg-slate-100 mx-auto px-8">
<div class="flex flex-row w-full justify-between">
<div class="flex flex-col justify-end-safe flex-2/5">
@@ -117,7 +118,11 @@
<div class="inputlabelrow">
<label for="name" class="inputlabel">Name</label>
</div>
<textarea name="name" id="name" class="inputinput no-enter" autocomplete="off" rows="1">{{- $agent.Name -}}</textarea>
<textarea name="name" id="name" class="inputinput no-enter" autocomplete="off" rows="1" data-duplicate-check data-duplicate-endpoint="/api/agents/search" data-duplicate-result-key="agents" data-duplicate-current-id="{{ if not $model.is_new }}{{ $agent.Id }}{{ end }}">{{- $agent.Name -}}</textarea>
<div class="duplicate-warning hidden" data-duplicate-warning-for="name">
<i class="ri-information-line"></i>
<span data-duplicate-count></span>
</div>
</div>
<div class="inputwrapper">
<div class="inputlabelrow">

View File

@@ -2,6 +2,7 @@
{{ $series := $model.result.Series }}
<edit-page>
<duplicate-warning-checker></duplicate-warning-checker>
<div class="flex container-normal bg-slate-100 mx-auto px-8">
<div class="flex flex-row w-full justify-between">
<div class="flex flex-col justify-end-safe flex-2/5">
@@ -117,7 +118,11 @@
<div class="inputlabelrow">
<label for="title" class="inputlabel">Reihentitel</label>
</div>
<textarea name="title" id="title" class="inputinput no-enter" autocomplete="off" rows="1">{{- $series.Title -}}</textarea>
<textarea name="title" id="title" class="inputinput no-enter" autocomplete="off" rows="1" data-duplicate-check data-duplicate-endpoint="/api/series/search" data-duplicate-result-key="series" data-duplicate-current-id="{{ if not $model.is_new }}{{ $series.Id }}{{ end }}">{{- $series.Title -}}</textarea>
<div class="duplicate-warning hidden" data-duplicate-warning-for="title">
<i class="ri-information-line"></i>
<span data-duplicate-count></span>
</div>
</div>
<div class="inputwrapper">
<div class="inputlabelrow">

View File

@@ -0,0 +1,96 @@
const DEBOUNCE_DELAY_MS = 100;
export class DuplicateWarningChecker extends HTMLElement {
constructor() {
super();
this._fields = null;
this._boundHandlers = new Map();
}
connectedCallback() {
// Find all fields marked for duplicate checking
this._fields = document.querySelectorAll("[data-duplicate-check]");
this._fields.forEach((field) => {
const handler = this._createHandler(field);
this._boundHandlers.set(field, handler);
field.addEventListener("input", handler);
});
}
disconnectedCallback() {
this._boundHandlers.forEach((handler, field) => {
field.removeEventListener("input", handler);
});
this._boundHandlers.clear();
}
_createHandler(field) {
let timeout = null;
return (event) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
this._checkDuplicates(field);
}, DEBOUNCE_DELAY_MS);
};
}
async _checkDuplicates(field) {
const value = field.value.trim();
const endpoint = field.getAttribute("data-duplicate-endpoint");
const resultKey = field.getAttribute("data-duplicate-result-key");
const currentId = field.getAttribute("data-duplicate-current-id") || "";
const warningEl = document.querySelector(`[data-duplicate-warning-for="${field.id}"]`);
if (!warningEl || !endpoint || !resultKey) {
return;
}
// Hide warning if field is empty
if (value === "") {
warningEl.classList.add("hidden");
return;
}
// Fetch duplicates
try {
const url = new URL(endpoint, window.location.origin);
url.searchParams.set("q", value);
url.searchParams.set("limit", "100"); // Get all to filter
const response = await fetch(url.toString());
if (!response.ok) {
return;
}
const data = await response.json();
const results = data[resultKey] || [];
// Filter out current item if editing
let filtered = results;
if (currentId) {
filtered = results.filter((item) => item.id !== currentId);
}
// Filter for exact matches only (case-insensitive)
const exactMatches = filtered.filter((item) => item.name && item.name.toLowerCase() === value.toLowerCase());
// Show or hide warning
if (exactMatches.length > 0) {
const countEl = warningEl.querySelector("[data-duplicate-count]");
if (countEl) {
const plural = exactMatches.length === 1 ? "" : "e";
countEl.textContent = `Der Name ist bereits vorhanden (${exactMatches.length} Treffer${plural})`;
}
warningEl.classList.remove("hidden");
} else {
warningEl.classList.add("hidden");
}
} catch (err) {
// Silently fail - don't show errors to user
console.error("Duplicate check failed:", err);
}
}
}

View File

@@ -45,6 +45,10 @@
disabled:opacity-50 py-1 px-3;
}
.dbform .duplicate-warning {
@apply text-sm text-sky-600 mt-0.5 pl-2.5 pb-1.5 flex items-center gap-1;
}
/* Status select color coding */
.status-select[data-status="Edited"] {
@apply bg-green-100 text-green-900;

View File

@@ -27,6 +27,7 @@ import { AlmanachEditPage } from "./almanach-edit.js";
import { RelationsEditor } from "./relations-editor.js";
import { EditPage } from "./edit-page.js";
import { FabMenu } from "./fab-menu.js";
import { DuplicateWarningChecker } from "./duplicate-warning.js";
const FILTER_LIST_ELEMENT = "filter-list";
const FAB_MENU_ELEMENT = "fab-menu";
@@ -47,6 +48,7 @@ const ITEMS_EDITOR_ELEMENT = "items-editor";
const ALMANACH_EDIT_PAGE_ELEMENT = "almanach-edit-page";
const RELATIONS_EDITOR_ELEMENT = "relations-editor";
const EDIT_PAGE_ELEMENT = "edit-page";
const DUPLICATE_WARNING_ELEMENT = "duplicate-warning-checker";
customElements.define(INT_LINK_ELEMENT, IntLink);
customElements.define(ABBREV_TOOLTIPS_ELEMENT, AbbreviationTooltips);
@@ -67,6 +69,7 @@ customElements.define(ALMANACH_EDIT_PAGE_ELEMENT, AlmanachEditPage);
customElements.define(RELATIONS_EDITOR_ELEMENT, RelationsEditor);
customElements.define(EDIT_PAGE_ELEMENT, EditPage);
customElements.define(FAB_MENU_ELEMENT, FabMenu);
customElements.define(DUPLICATE_WARNING_ELEMENT, DuplicateWarningChecker);
function PathPlusQuery() {
const path = window.location.pathname;