@7365admin1/core 3.41.0 → 3.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -11249,8 +11249,9 @@ function useSiteRepo() {
11249
11249
  } catch (error2) {
11250
11250
  throw new BadRequestError18("Invalid org ID format.");
11251
11251
  }
11252
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11252
11253
  const query = {
11253
- name: { $regex: new RegExp(`^${name}$`, "i") },
11254
+ name: { $regex: new RegExp(`^${escapedName}$`, "i") },
11254
11255
  // Case-insensitive exact match
11255
11256
  orgId,
11256
11257
  status: { $ne: "deleted" }
@@ -32346,7 +32347,57 @@ import {
32346
32347
  useAtlas as useAtlas46,
32347
32348
  NotFoundError as NotFoundError20
32348
32349
  } from "@7365admin1/node-server-utils";
32350
+
32351
+ // src/utils/site-name.util.ts
32349
32352
  import levenshtein from "fast-levenshtein";
32353
+ var ROMAN = /^(?=[ivx])(x{0,3})(ix|iv|v?i{0,3})$/;
32354
+ var ROMAN_VALUE = { i: 1, v: 5, x: 10 };
32355
+ function normalizeSiteName(name) {
32356
+ return String(name ?? "").normalize("NFKC").toLowerCase().replace(/[‘’']/g, "").replace(/[^a-z0-9]+/g, " ").trim();
32357
+ }
32358
+ function romanToNumber(token) {
32359
+ let total = 0;
32360
+ for (let i = 0; i < token.length; i++) {
32361
+ const value = ROMAN_VALUE[token[i]];
32362
+ const next = ROMAN_VALUE[token[i + 1]];
32363
+ total += next && next > value ? -value : value;
32364
+ }
32365
+ return total;
32366
+ }
32367
+ function distinguishingTokens(normalized) {
32368
+ if (!normalized)
32369
+ return [];
32370
+ return normalized.split(" ").filter((t) => /^\d+$/.test(t) || ROMAN.test(t) || /^[a-z]$/.test(t)).map((t) => ROMAN.test(t) ? String(romanToNumber(t)) : t);
32371
+ }
32372
+ function matchSiteName(candidate, existing) {
32373
+ const a = normalizeSiteName(candidate);
32374
+ const b = normalizeSiteName(existing);
32375
+ if (!a || !b)
32376
+ return null;
32377
+ if (a === b)
32378
+ return "duplicate";
32379
+ const tokensA = distinguishingTokens(a).join(" ");
32380
+ const tokensB = distinguishingTokens(b).join(" ");
32381
+ if (tokensA !== tokensB)
32382
+ return null;
32383
+ return levenshtein.get(a, b) <= 2 ? "near-duplicate" : null;
32384
+ }
32385
+ function findSiteNameClash(candidate, existingNames) {
32386
+ for (const name of existingNames) {
32387
+ const match = matchSiteName(candidate, name);
32388
+ if (match)
32389
+ return { name, match };
32390
+ }
32391
+ return null;
32392
+ }
32393
+ function siteNameClashMessage(existingName, match) {
32394
+ if (match === "duplicate") {
32395
+ return `A site called "${existingName}" already exists in this organisation. Give the new site a different name, or edit "${existingName}" instead.`;
32396
+ }
32397
+ return `This name is within a character or two of "${existingName}", which already exists in this organisation. If it is the same property, edit "${existingName}" instead. If it is a different one, include what tells them apart \u2014 the block, tower, phase, or number \u2014 and save again.`;
32398
+ }
32399
+
32400
+ // src/services/customer-site.service.ts
32350
32401
  function useCustomerSiteService() {
32351
32402
  const { add: _add, updateCustomerSiteById, getById: _getById } = useCustomerSiteRepo();
32352
32403
  const {
@@ -32368,20 +32419,18 @@ function useCustomerSiteService() {
32368
32419
  const exactMatches = await getSiteByExactName(value.name, value.siteOrg);
32369
32420
  if (exactMatches && exactMatches.length > 0) {
32370
32421
  throw new BadRequestError91(
32371
- "Site with this name already exists in the organization."
32422
+ siteNameClashMessage(exactMatches[0].name, "duplicate")
32372
32423
  );
32373
32424
  }
32374
- const threshold = 2;
32375
32425
  const sites = await getSiteByName(value.name);
32376
- if (sites && sites.length > 0) {
32377
- const similar = sites.filter((doc) => {
32378
- return doc.orgId && doc.name && doc.orgId.toString() === value.siteOrg.toString() && levenshtein.get(value.name.toLowerCase(), doc.name.toLowerCase()) <= threshold;
32379
- });
32380
- if (similar.length > 0) {
32381
- throw new BadRequestError91(
32382
- "Failed to add site, site with closely similar name found. Please contact seven365 support."
32383
- );
32384
- }
32426
+ const candidates = (sites ?? []).filter(
32427
+ (doc) => doc.orgId && doc.name && doc.orgId.toString() === value.siteOrg.toString()
32428
+ ).map((doc) => doc.name);
32429
+ const clash = findSiteNameClash(value.name, candidates);
32430
+ if (clash) {
32431
+ throw new BadRequestError91(
32432
+ siteNameClashMessage(clash.name, clash.match)
32433
+ );
32385
32434
  }
32386
32435
  const siteId = await createSite(
32387
32436
  {