|
|
@@ -1,13 +1,58 @@
|
|
|
export const SEARCH_RESULTS_SORT = Object.freeze({
|
|
|
RELEVANCE: "relevance",
|
|
|
DATE_DESC: "date_desc",
|
|
|
- FILENAME_ASC: "filename_asc",
|
|
|
+ BRANCH_ASC: "branch_asc",
|
|
|
});
|
|
|
|
|
|
function pad2(value) {
|
|
|
return String(value || "").padStart(2, "0");
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Extract a comparable branch number from ids like "NL01", "NL200".
|
|
|
+ *
|
|
|
+ * Why this exists:
|
|
|
+ * - A lexicographic compare would sort "NL10" before "NL2" (wrong).
|
|
|
+ * - We want deterministic ordering that matches how humans read branch ids.
|
|
|
+ *
|
|
|
+ * @param {any} branchId
|
|
|
+ * @returns {number|null}
|
|
|
+ */
|
|
|
+function toBranchNumber(branchId) {
|
|
|
+ const raw = String(branchId || "").trim();
|
|
|
+ const match = /^NL(\d+)$/i.exec(raw);
|
|
|
+ if (!match) return null;
|
|
|
+
|
|
|
+ const n = Number(match[1]);
|
|
|
+ return Number.isInteger(n) ? n : null;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Compare two branch ids.
|
|
|
+ *
|
|
|
+ * Policy:
|
|
|
+ * - If both look like NL<number>, compare numerically (NL2 < NL10).
|
|
|
+ * - If only one looks like NL<number>, prefer the valid one first.
|
|
|
+ * - Otherwise fall back to a stable lexicographic compare.
|
|
|
+ *
|
|
|
+ * @param {any} a
|
|
|
+ * @param {any} b
|
|
|
+ * @returns {number}
|
|
|
+ */
|
|
|
+function compareBranchIds(a, b) {
|
|
|
+ const aa = String(a || "");
|
|
|
+ const bb = String(b || "");
|
|
|
+
|
|
|
+ const na = toBranchNumber(aa);
|
|
|
+ const nb = toBranchNumber(bb);
|
|
|
+
|
|
|
+ if (na !== null && nb !== null) return na - nb;
|
|
|
+ if (na !== null && nb === null) return -1;
|
|
|
+ if (na === null && nb !== null) return 1;
|
|
|
+
|
|
|
+ return aa.localeCompare(bb, "en");
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Build an ISO-like date key (YYYY-MM-DD) from a search item.
|
|
|
*
|
|
|
@@ -28,6 +73,10 @@ export function toSearchItemIsoDateKey(item) {
|
|
|
/**
|
|
|
* Format the search item date as German UI string: DD.MM.YYYY
|
|
|
*
|
|
|
+ * Important:
|
|
|
+ * - This is user-facing output (German).
|
|
|
+ * - We still keep it pure/testable and independent from UI frameworks.
|
|
|
+ *
|
|
|
* @param {any} item
|
|
|
* @returns {string}
|
|
|
*/
|
|
|
@@ -41,13 +90,21 @@ export function formatSearchItemDateDe(item) {
|
|
|
/**
|
|
|
* Sort search items according to the selected sort mode.
|
|
|
*
|
|
|
- * Policy:
|
|
|
- * - RELEVANCE: keep backend order (we return a shallow copy to avoid accidental mutation)
|
|
|
- * - DATE_DESC: newest date first, then filename asc (stable & predictable)
|
|
|
- * - FILENAME_ASC: filename asc
|
|
|
+ * UX/API policy:
|
|
|
+ * - RELEVANCE:
|
|
|
+ * Keep backend order (assumed relevance-ranked). We still return a shallow copy
|
|
|
+ * to avoid accidental mutations by callers.
|
|
|
+ *
|
|
|
+ * - DATE_DESC:
|
|
|
+ * Newest date first.
|
|
|
+ * Tie-breakers: branch (stable, numeric for NLxx), then filename asc.
|
|
|
+ *
|
|
|
+ * - BRANCH_ASC:
|
|
|
+ * Branch ascending (NL01..NLxx), then newest date first, then filename asc.
|
|
|
+ * This keeps multi/all searches easy to scan without introducing UI grouping headers.
|
|
|
*
|
|
|
* @param {any[]} items
|
|
|
- * @param {"relevance"|"date_desc"|"filename_asc"|string} sortMode
|
|
|
+ * @param {"relevance"|"date_desc"|"branch_asc"|string} sortMode
|
|
|
* @returns {any[]}
|
|
|
*/
|
|
|
export function sortSearchItems(items, sortMode) {
|
|
|
@@ -60,18 +117,38 @@ export function sortSearchItems(items, sortMode) {
|
|
|
const da = toSearchItemIsoDateKey(a);
|
|
|
const db = toSearchItemIsoDateKey(b);
|
|
|
|
|
|
+ // Newest first
|
|
|
if (da !== db) return da < db ? 1 : -1;
|
|
|
|
|
|
+ // Stable tie-breakers
|
|
|
+ const ba = a?.branch;
|
|
|
+ const bb = b?.branch;
|
|
|
+ const branchCmp = compareBranchIds(ba, bb);
|
|
|
+ if (branchCmp !== 0) return branchCmp;
|
|
|
+
|
|
|
const fa = String(a?.filename || "");
|
|
|
const fb = String(b?.filename || "");
|
|
|
return fa.localeCompare(fb, "de");
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- if (sortMode === SEARCH_RESULTS_SORT.FILENAME_ASC) {
|
|
|
- return arr.sort((a, b) =>
|
|
|
- String(a?.filename || "").localeCompare(String(b?.filename || ""), "de")
|
|
|
- );
|
|
|
+ if (sortMode === SEARCH_RESULTS_SORT.BRANCH_ASC) {
|
|
|
+ return arr.sort((a, b) => {
|
|
|
+ const ba = a?.branch;
|
|
|
+ const bb = b?.branch;
|
|
|
+
|
|
|
+ const branchCmp = compareBranchIds(ba, bb);
|
|
|
+ if (branchCmp !== 0) return branchCmp;
|
|
|
+
|
|
|
+ // Within the same branch: newest date first
|
|
|
+ const da = toSearchItemIsoDateKey(a);
|
|
|
+ const db = toSearchItemIsoDateKey(b);
|
|
|
+ if (da !== db) return da < db ? 1 : -1;
|
|
|
+
|
|
|
+ const fa = String(a?.filename || "");
|
|
|
+ const fb = String(b?.filename || "");
|
|
|
+ return fa.localeCompare(fb, "de");
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
// Unknown sort mode => fail-safe to backend order.
|