@open-mercato/core 0.6.6-develop.6402.1.080aab8ec9 → 0.6.6-develop.6404.1.e2f8d5025a

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.
Files changed (27) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/catalog/widgets/injection/product-seo/state.js +9 -0
  3. package/dist/modules/catalog/widgets/injection/product-seo/state.js.map +2 -2
  4. package/dist/modules/catalog/widgets/injection/product-seo/validation.js +29 -15
  5. package/dist/modules/catalog/widgets/injection/product-seo/validation.js.map +2 -2
  6. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +5 -1
  7. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  8. package/dist/modules/catalog/widgets/injection/product-seo/widget.js +2 -2
  9. package/dist/modules/catalog/widgets/injection/product-seo/widget.js.map +2 -2
  10. package/dist/modules/planner/api/availability-weekly.js +11 -1
  11. package/dist/modules/planner/api/availability-weekly.js.map +2 -2
  12. package/dist/modules/planner/commands/availability-weekly.js +34 -3
  13. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  14. package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -6
  15. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  16. package/package.json +7 -7
  17. package/src/modules/catalog/i18n/de.json +10 -0
  18. package/src/modules/catalog/i18n/en.json +10 -0
  19. package/src/modules/catalog/i18n/es.json +10 -0
  20. package/src/modules/catalog/i18n/pl.json +10 -0
  21. package/src/modules/catalog/widgets/injection/product-seo/state.ts +17 -0
  22. package/src/modules/catalog/widgets/injection/product-seo/validation.ts +38 -15
  23. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +7 -1
  24. package/src/modules/catalog/widgets/injection/product-seo/widget.ts +2 -2
  25. package/src/modules/planner/api/availability-weekly.ts +10 -0
  26. package/src/modules/planner/commands/availability-weekly.ts +46 -8
  27. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +15 -6
@@ -1,4 +1,4 @@
1
- [build:core] found 3449 entry points
1
+ [build:core] found 3450 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 186 entry points
4
4
  [build:core:generated] built successfully
@@ -1,3 +1,10 @@
1
+ let productSeoTranslator = null;
2
+ function setProductSeoTranslator(t) {
3
+ productSeoTranslator = t;
4
+ }
5
+ function getProductSeoTranslator() {
6
+ return productSeoTranslator ?? void 0;
7
+ }
1
8
  const listeners = /* @__PURE__ */ new Set();
2
9
  function publishProductSeoValidation(payload) {
3
10
  listeners.forEach((listener) => {
@@ -13,7 +20,9 @@ function subscribeProductSeoValidation(listener) {
13
20
  return () => listeners.delete(listener);
14
21
  }
15
22
  export {
23
+ getProductSeoTranslator,
16
24
  publishProductSeoValidation,
25
+ setProductSeoTranslator,
17
26
  subscribeProductSeoValidation
18
27
  };
19
28
  //# sourceMappingURL=state.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/catalog/widgets/injection/product-seo/state.ts"],
4
- "sourcesContent": ["export type ProductSeoValidationPayload = {\n ok: boolean\n issues: string[]\n message?: string\n}\n\ntype Listener = (payload: ProductSeoValidationPayload) => void\n\nconst listeners = new Set<Listener>()\n\nexport function publishProductSeoValidation(payload: ProductSeoValidationPayload) {\n listeners.forEach((listener) => {\n try {\n listener(payload)\n } catch (err) {\n console.error('[product-seo] Failed to notify listener', err)\n }\n })\n}\n\nexport function subscribeProductSeoValidation(listener: Listener): () => void {\n listeners.add(listener)\n return () => listeners.delete(listener)\n}\n"],
5
- "mappings": "AAQA,MAAM,YAAY,oBAAI,IAAc;AAE7B,SAAS,4BAA4B,SAAsC;AAChF,YAAU,QAAQ,CAAC,aAAa;AAC9B,QAAI;AACF,eAAS,OAAO;AAAA,IAClB,SAAS,KAAK;AACZ,cAAQ,MAAM,2CAA2C,GAAG;AAAA,IAC9D;AAAA,EACF,CAAC;AACH;AAEO,SAAS,8BAA8B,UAAgC;AAC5E,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM,UAAU,OAAO,QAAQ;AACxC;",
4
+ "sourcesContent": ["import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\n\nexport type ProductSeoValidationPayload = {\n ok: boolean\n issues: string[]\n message?: string\n}\n\n// The `onBeforeSave` validation hook (widget.ts) is a plain side-effect handler\n// with no React context, so it cannot call `useT`. The widget component (which\n// does have `useT`) publishes its translator here on mount; the hook reads it to\n// localize the save-block message (#3299). Undefined until the widget mounts \u2014\n// callers fall back to English via `evaluateProductSeo`'s default translator.\nlet productSeoTranslator: TranslateFn | null = null\n\nexport function setProductSeoTranslator(t: TranslateFn | null) {\n productSeoTranslator = t\n}\n\nexport function getProductSeoTranslator(): TranslateFn | undefined {\n return productSeoTranslator ?? undefined\n}\n\ntype Listener = (payload: ProductSeoValidationPayload) => void\n\nconst listeners = new Set<Listener>()\n\nexport function publishProductSeoValidation(payload: ProductSeoValidationPayload) {\n listeners.forEach((listener) => {\n try {\n listener(payload)\n } catch (err) {\n console.error('[product-seo] Failed to notify listener', err)\n }\n })\n}\n\nexport function subscribeProductSeoValidation(listener: Listener): () => void {\n listeners.add(listener)\n return () => listeners.delete(listener)\n}\n"],
5
+ "mappings": "AAaA,IAAI,uBAA2C;AAExC,SAAS,wBAAwB,GAAuB;AAC7D,yBAAuB;AACzB;AAEO,SAAS,0BAAmD;AACjE,SAAO,wBAAwB;AACjC;AAIA,MAAM,YAAY,oBAAI,IAAc;AAE7B,SAAS,4BAA4B,SAAsC;AAChF,YAAU,QAAQ,CAAC,aAAa;AAC9B,QAAI;AACF,eAAS,OAAO;AAAA,IAClB,SAAS,KAAK;AACZ,cAAQ,MAAM,2CAA2C,GAAG;AAAA,IAC9D;AAAA,EACF,CAAC;AACH;AAEO,SAAS,8BAA8B,UAAgC;AAC5E,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM,UAAU,OAAO,QAAQ;AACxC;",
6
6
  "names": []
7
7
  }
@@ -1,34 +1,48 @@
1
- const MESSAGE_PREFIX = "SEO helper: ";
2
- function buildSeoBlockMessage(issues) {
3
- if (issues.length === 1) return `${MESSAGE_PREFIX}${issues[0]}`;
4
- if (issues.length <= 3) return `${MESSAGE_PREFIX}${issues.join(" ")}`;
5
- return `${MESSAGE_PREFIX}${issues.length} issues found. ${issues.slice(0, 2).join(" ")}`;
1
+ const englishFallback = (_key, fallbackOrParams, params) => {
2
+ const template = typeof fallbackOrParams === "string" ? fallbackOrParams : "";
3
+ const resolvedParams = typeof fallbackOrParams === "string" ? params : fallbackOrParams;
4
+ if (!resolvedParams) return template;
5
+ return template.replace(/\{\{(\w+)\}\}|\{(\w+)\}/g, (match, doubleKey, singleKey) => {
6
+ const key = doubleKey ?? singleKey;
7
+ const value = resolvedParams[key];
8
+ return value === void 0 ? match : String(value);
9
+ });
10
+ };
11
+ const K = "catalog.products.create.seoWidget.validation";
12
+ function buildSeoBlockMessage(issues, t = englishFallback) {
13
+ if (issues.length <= 3) {
14
+ return t(`${K}.block.list`, "SEO helper: {{issues}}", { issues: issues.join(" ") });
15
+ }
16
+ return t(`${K}.block.summary`, "SEO helper: {{count}} issues found. {{issues}}", {
17
+ count: issues.length,
18
+ issues: issues.slice(0, 2).join(" ")
19
+ });
6
20
  }
7
- function evaluateProductSeo(data) {
21
+ function evaluateProductSeo(data, t = englishFallback) {
8
22
  const issues = [];
9
23
  const fieldErrors = {};
10
24
  const title = data?.title || data?.name;
11
25
  if (typeof title === "string" && title.length > 0) {
12
26
  if (title.length < 10) {
13
- issues.push("Title is too short (min 10 characters).");
14
- fieldErrors.title = "Title is too short for good SEO (min 10 characters).";
27
+ issues.push(t(`${K}.issue.titleTooShort`, "Title is too short (min 10 characters)."));
28
+ fieldErrors.title = t(`${K}.fieldError.titleTooShort`, "Title is too short for good SEO (min 10 characters).");
15
29
  } else if (title.length > 60) {
16
- issues.push("Title is too long (max 60 characters recommended).");
17
- fieldErrors.title = "Title is too long for optimal SEO (max 60 characters).";
30
+ issues.push(t(`${K}.issue.titleTooLong`, "Title is too long (max 60 characters recommended)."));
31
+ fieldErrors.title = t(`${K}.fieldError.titleTooLong`, "Title is too long for optimal SEO (max 60 characters).");
18
32
  }
19
33
  }
20
34
  const description = data?.description;
21
35
  if (typeof description === "string") {
22
36
  if (description.trim().length === 0) {
23
- issues.push("Add a product description for better SEO.");
24
- fieldErrors.description = "Provide a description to help search engines understand this product.";
37
+ issues.push(t(`${K}.issue.descriptionMissing`, "Add a product description for better SEO."));
38
+ fieldErrors.description = t(`${K}.fieldError.descriptionMissing`, "Provide a description to help search engines understand this product.");
25
39
  } else if (description.length < 50) {
26
- issues.push("Description is too short (min 50 characters).");
27
- fieldErrors.description = "Description is too short for good SEO (min 50 characters).";
40
+ issues.push(t(`${K}.issue.descriptionTooShort`, "Description is too short (min 50 characters)."));
41
+ fieldErrors.description = t(`${K}.fieldError.descriptionTooShort`, "Description is too short for good SEO (min 50 characters).");
28
42
  }
29
43
  }
30
44
  if (issues.length) {
31
- return { ok: false, issues, fieldErrors, message: buildSeoBlockMessage(issues) };
45
+ return { ok: false, issues, fieldErrors, message: buildSeoBlockMessage(issues, t) };
32
46
  }
33
47
  return { ok: true, issues: [], fieldErrors: {} };
34
48
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/catalog/widgets/injection/product-seo/validation.ts"],
4
- "sourcesContent": ["export type ProductSeoEvaluation = {\n ok: boolean\n issues: string[]\n fieldErrors: Record<string, string>\n message?: string\n}\n\nconst MESSAGE_PREFIX = 'SEO helper: '\n\nexport function buildSeoBlockMessage(issues: string[]): string {\n if (issues.length === 1) return `${MESSAGE_PREFIX}${issues[0]}`\n if (issues.length <= 3) return `${MESSAGE_PREFIX}${issues.join(' ')}`\n return `${MESSAGE_PREFIX}${issues.length} issues found. ${issues.slice(0, 2).join(' ')}`\n}\n\nexport function evaluateProductSeo(data: Record<string, unknown> | null | undefined): ProductSeoEvaluation {\n const issues: string[] = []\n const fieldErrors: Record<string, string> = {}\n\n const title = (data?.title as unknown) || (data?.name as unknown)\n if (typeof title === 'string' && title.length > 0) {\n if (title.length < 10) {\n issues.push('Title is too short (min 10 characters).')\n fieldErrors.title = 'Title is too short for good SEO (min 10 characters).'\n } else if (title.length > 60) {\n issues.push('Title is too long (max 60 characters recommended).')\n fieldErrors.title = 'Title is too long for optimal SEO (max 60 characters).'\n }\n }\n\n const description = data?.description\n if (typeof description === 'string') {\n if (description.trim().length === 0) {\n issues.push('Add a product description for better SEO.')\n fieldErrors.description = 'Provide a description to help search engines understand this product.'\n } else if (description.length < 50) {\n issues.push('Description is too short (min 50 characters).')\n fieldErrors.description = 'Description is too short for good SEO (min 50 characters).'\n }\n }\n\n if (issues.length) {\n return { ok: false, issues, fieldErrors, message: buildSeoBlockMessage(issues) }\n }\n\n return { ok: true, issues: [], fieldErrors: {} }\n}\n"],
5
- "mappings": "AAOA,MAAM,iBAAiB;AAEhB,SAAS,qBAAqB,QAA0B;AAC7D,MAAI,OAAO,WAAW,EAAG,QAAO,GAAG,cAAc,GAAG,OAAO,CAAC,CAAC;AAC7D,MAAI,OAAO,UAAU,EAAG,QAAO,GAAG,cAAc,GAAG,OAAO,KAAK,GAAG,CAAC;AACnE,SAAO,GAAG,cAAc,GAAG,OAAO,MAAM,kBAAkB,OAAO,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC;AACxF;AAEO,SAAS,mBAAmB,MAAwE;AACzG,QAAM,SAAmB,CAAC;AAC1B,QAAM,cAAsC,CAAC;AAE7C,QAAM,QAAS,MAAM,SAAsB,MAAM;AACjD,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,QAAI,MAAM,SAAS,IAAI;AACrB,aAAO,KAAK,yCAAyC;AACrD,kBAAY,QAAQ;AAAA,IACtB,WAAW,MAAM,SAAS,IAAI;AAC5B,aAAO,KAAK,oDAAoD;AAChE,kBAAY,QAAQ;AAAA,IACtB;AAAA,EACF;AAEA,QAAM,cAAc,MAAM;AAC1B,MAAI,OAAO,gBAAgB,UAAU;AACnC,QAAI,YAAY,KAAK,EAAE,WAAW,GAAG;AACnC,aAAO,KAAK,2CAA2C;AACvD,kBAAY,cAAc;AAAA,IAC5B,WAAW,YAAY,SAAS,IAAI;AAClC,aAAO,KAAK,+CAA+C;AAC3D,kBAAY,cAAc;AAAA,IAC5B;AAAA,EACF;AAEA,MAAI,OAAO,QAAQ;AACjB,WAAO,EAAE,IAAI,OAAO,QAAQ,aAAa,SAAS,qBAAqB,MAAM,EAAE;AAAA,EACjF;AAEA,SAAO,EAAE,IAAI,MAAM,QAAQ,CAAC,GAAG,aAAa,CAAC,EAAE;AACjD;",
4
+ "sourcesContent": ["import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\n\nexport type ProductSeoEvaluation = {\n ok: boolean\n issues: string[]\n fieldErrors: Record<string, string>\n message?: string\n}\n\n// English-fallback translator: keeps this pure module usable (and unit-testable)\n// when no real `t` is threaded in \u2014 every string still ships its English default,\n// interpolating `{{param}}` placeholders the same way the real translator does.\nconst englishFallback: TranslateFn = (_key, fallbackOrParams, params) => {\n const template = typeof fallbackOrParams === 'string' ? fallbackOrParams : ''\n const resolvedParams = typeof fallbackOrParams === 'string' ? params : fallbackOrParams\n if (!resolvedParams) return template\n return template.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (match, doubleKey, singleKey) => {\n const key = doubleKey ?? singleKey\n const value = resolvedParams[key]\n return value === undefined ? match : String(value)\n })\n}\n\nconst K = 'catalog.products.create.seoWidget.validation'\n\nexport function buildSeoBlockMessage(issues: string[], t: TranslateFn = englishFallback): string {\n if (issues.length <= 3) {\n return t(`${K}.block.list`, 'SEO helper: {{issues}}', { issues: issues.join(' ') })\n }\n return t(`${K}.block.summary`, 'SEO helper: {{count}} issues found. {{issues}}', {\n count: issues.length,\n issues: issues.slice(0, 2).join(' '),\n })\n}\n\nexport function evaluateProductSeo(\n data: Record<string, unknown> | null | undefined,\n t: TranslateFn = englishFallback,\n): ProductSeoEvaluation {\n const issues: string[] = []\n const fieldErrors: Record<string, string> = {}\n\n const title = (data?.title as unknown) || (data?.name as unknown)\n if (typeof title === 'string' && title.length > 0) {\n if (title.length < 10) {\n issues.push(t(`${K}.issue.titleTooShort`, 'Title is too short (min 10 characters).'))\n fieldErrors.title = t(`${K}.fieldError.titleTooShort`, 'Title is too short for good SEO (min 10 characters).')\n } else if (title.length > 60) {\n issues.push(t(`${K}.issue.titleTooLong`, 'Title is too long (max 60 characters recommended).'))\n fieldErrors.title = t(`${K}.fieldError.titleTooLong`, 'Title is too long for optimal SEO (max 60 characters).')\n }\n }\n\n const description = data?.description\n if (typeof description === 'string') {\n if (description.trim().length === 0) {\n issues.push(t(`${K}.issue.descriptionMissing`, 'Add a product description for better SEO.'))\n fieldErrors.description = t(`${K}.fieldError.descriptionMissing`, 'Provide a description to help search engines understand this product.')\n } else if (description.length < 50) {\n issues.push(t(`${K}.issue.descriptionTooShort`, 'Description is too short (min 50 characters).'))\n fieldErrors.description = t(`${K}.fieldError.descriptionTooShort`, 'Description is too short for good SEO (min 50 characters).')\n }\n }\n\n if (issues.length) {\n return { ok: false, issues, fieldErrors, message: buildSeoBlockMessage(issues, t) }\n }\n\n return { ok: true, issues: [], fieldErrors: {} }\n}\n"],
5
+ "mappings": "AAYA,MAAM,kBAA+B,CAAC,MAAM,kBAAkB,WAAW;AACvE,QAAM,WAAW,OAAO,qBAAqB,WAAW,mBAAmB;AAC3E,QAAM,iBAAiB,OAAO,qBAAqB,WAAW,SAAS;AACvE,MAAI,CAAC,eAAgB,QAAO;AAC5B,SAAO,SAAS,QAAQ,4BAA4B,CAAC,OAAO,WAAW,cAAc;AACnF,UAAM,MAAM,aAAa;AACzB,UAAM,QAAQ,eAAe,GAAG;AAChC,WAAO,UAAU,SAAY,QAAQ,OAAO,KAAK;AAAA,EACnD,CAAC;AACH;AAEA,MAAM,IAAI;AAEH,SAAS,qBAAqB,QAAkB,IAAiB,iBAAyB;AAC/F,MAAI,OAAO,UAAU,GAAG;AACtB,WAAO,EAAE,GAAG,CAAC,eAAe,0BAA0B,EAAE,QAAQ,OAAO,KAAK,GAAG,EAAE,CAAC;AAAA,EACpF;AACA,SAAO,EAAE,GAAG,CAAC,kBAAkB,kDAAkD;AAAA,IAC/E,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AAAA,EACrC,CAAC;AACH;AAEO,SAAS,mBACd,MACA,IAAiB,iBACK;AACtB,QAAM,SAAmB,CAAC;AAC1B,QAAM,cAAsC,CAAC;AAE7C,QAAM,QAAS,MAAM,SAAsB,MAAM;AACjD,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,QAAI,MAAM,SAAS,IAAI;AACrB,aAAO,KAAK,EAAE,GAAG,CAAC,wBAAwB,yCAAyC,CAAC;AACpF,kBAAY,QAAQ,EAAE,GAAG,CAAC,6BAA6B,sDAAsD;AAAA,IAC/G,WAAW,MAAM,SAAS,IAAI;AAC5B,aAAO,KAAK,EAAE,GAAG,CAAC,uBAAuB,oDAAoD,CAAC;AAC9F,kBAAY,QAAQ,EAAE,GAAG,CAAC,4BAA4B,wDAAwD;AAAA,IAChH;AAAA,EACF;AAEA,QAAM,cAAc,MAAM;AAC1B,MAAI,OAAO,gBAAgB,UAAU;AACnC,QAAI,YAAY,KAAK,EAAE,WAAW,GAAG;AACnC,aAAO,KAAK,EAAE,GAAG,CAAC,6BAA6B,2CAA2C,CAAC;AAC3F,kBAAY,cAAc,EAAE,GAAG,CAAC,kCAAkC,uEAAuE;AAAA,IAC3I,WAAW,YAAY,SAAS,IAAI;AAClC,aAAO,KAAK,EAAE,GAAG,CAAC,8BAA8B,+CAA+C,CAAC;AAChG,kBAAY,cAAc,EAAE,GAAG,CAAC,mCAAmC,4DAA4D;AAAA,IACjI;AAAA,EACF;AAEA,MAAI,OAAO,QAAQ;AACjB,WAAO,EAAE,IAAI,OAAO,QAAQ,aAAa,SAAS,qBAAqB,QAAQ,CAAC,EAAE;AAAA,EACpF;AAEA,SAAO,EAAE,IAAI,MAAM,QAAQ,CAAC,GAAG,aAAa,CAAC,EAAE;AACjD;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { subscribeProductSeoValidation } from "./state.js";
4
+ import { subscribeProductSeoValidation, setProductSeoTranslator } from "./state.js";
5
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
6
  import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
7
7
  import { Alert } from "@open-mercato/ui/primitives/alert";
@@ -22,6 +22,10 @@ function computeIssueKeys(title, description) {
22
22
  }
23
23
  function ProductSeoWidget({ data }) {
24
24
  const t = useT();
25
+ React.useEffect(() => {
26
+ setProductSeoTranslator(t);
27
+ return () => setProductSeoTranslator(null);
28
+ }, [t]);
25
29
  const title = (data?.title || data?.name || "") ?? "";
26
30
  const description = data?.description ?? "";
27
31
  const baselineIssueKeys = React.useMemo(() => computeIssueKeys(title, description), [title, description]);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/catalog/widgets/injection/product-seo/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { subscribeProductSeoValidation } from './state'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\n\ntype SeoData = {\n title?: string | null\n name?: string | null\n description?: string | null\n}\n\ntype ValidationState = { ok: boolean; issues: string[]; message?: string }\n\ntype IssueKey = 'addTitle' | 'titleTooShort' | 'titleTooLong' | 'addDescription' | 'descriptionTooShort'\n\ntype SeoScore = { text: string; variant: StatusBadgeVariant }\n\nfunction computeIssueKeys(title: string, description: string): IssueKey[] {\n const issues: IssueKey[] = []\n if (!title) {\n issues.push('addTitle')\n } else {\n if (title.length < 10) issues.push('titleTooShort')\n if (title.length > 60) issues.push('titleTooLong')\n }\n if (!description) {\n issues.push('addDescription')\n } else if (description.length < 50) {\n issues.push('descriptionTooShort')\n }\n return issues\n}\n\nexport default function ProductSeoWidget({ data }: InjectionWidgetComponentProps<unknown, SeoData>) {\n const t = useT()\n const title = (data?.title || data?.name || '') ?? ''\n const description = data?.description ?? ''\n const baselineIssueKeys = React.useMemo(() => computeIssueKeys(title, description), [title, description])\n const [validation, setValidation] = React.useState<ValidationState>({ ok: baselineIssueKeys.length === 0, issues: baselineIssueKeys })\n\n React.useEffect(() => {\n setValidation({ ok: baselineIssueKeys.length === 0, issues: baselineIssueKeys })\n }, [baselineIssueKeys])\n\n React.useEffect(() => {\n return subscribeProductSeoValidation((payload) => {\n setValidation({\n ok: payload.ok,\n issues: payload.issues,\n message: payload.message,\n })\n })\n }, [])\n\n const titleScore = React.useMemo<SeoScore>(() => {\n if (!title) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'), variant: 'error' }\n if (title.length < 10) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'), variant: 'warning' }\n if (title.length > 60) return { text: t('catalog.products.create.seoWidget.tooLong', 'Too long'), variant: 'warning' }\n return { text: t('catalog.products.create.seoWidget.good', 'Good'), variant: 'success' }\n }, [title, t])\n\n const descScore = React.useMemo<SeoScore>(() => {\n if (!description) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'), variant: 'error' }\n if (description.length < 50) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'), variant: 'warning' }\n return { text: t('catalog.products.create.seoWidget.good', 'Good'), variant: 'success' }\n }, [description, t])\n\n const statusBadge = validation.ok ? (\n <StatusBadge variant=\"success\">\n {t('catalog.products.create.seoWidget.ready', 'Ready')}\n </StatusBadge>\n ) : (\n <StatusBadge variant=\"warning\">\n {t('catalog.products.create.seoWidget.needsAttention', 'Needs attention')}\n </StatusBadge>\n )\n\n const translateIssue = (issueKey: string): string => {\n return t(`catalog.products.create.seoWidget.issues.${issueKey}`, issueKey)\n }\n\n return (\n <div className=\"mt-4 w-full space-y-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <div className=\"text-sm font-semibold text-foreground\">{t('catalog.products.create.seoWidget.title', 'SEO Optimization')}</div>\n <p className=\"text-xs text-muted-foreground\">{t('catalog.products.create.seoWidget.hint', 'Keep titles 10\u201360 chars and descriptions 50+ chars.')}</p>\n </div>\n {statusBadge}\n </div>\n\n {validation.message || validation.issues.length ? (\n <Alert\n status={validation.ok ? 'success' : 'warning'}\n style=\"lighter\"\n size=\"sm\"\n showIcon={false}\n className=\"text-xs\"\n >\n {validation.message ? <div className=\"font-medium\">{validation.message}</div> : null}\n {validation.issues.length ? (\n <ul className=\"ml-4 list-disc space-y-1 pt-1\">\n {validation.issues.map((issue) => (\n <li key={issue}>{translateIssue(issue)}</li>\n ))}\n </ul>\n ) : null}\n </Alert>\n ) : null}\n\n <div className=\"rounded border bg-muted/30 p-3 text-sm\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-muted-foreground\">{t('catalog.products.create.seoWidget.titleLabel', 'Title ({{count}} chars)', { count: title.length })}</span>\n <StatusBadge variant={titleScore.variant}>{titleScore.text}</StatusBadge>\n </div>\n <div className=\"mt-2 flex items-center justify-between\">\n <span className=\"text-muted-foreground\">{t('catalog.products.create.seoWidget.descriptionLabel', 'Description ({{count}} chars)', { count: description.length })}</span>\n <StatusBadge variant={descScore.variant}>{descScore.text}</StatusBadge>\n </div>\n </div>\n\n <p className=\"text-overline text-muted-foreground\">\n {t('catalog.products.create.seoWidget.footer', 'Example widget powered by the injection system.')}{' '}\n <a className=\"text-primary underline\" href=\"/docs/framework/admin-ui/widget-injection\" target=\"_blank\" rel=\"noreferrer\">\n {t('catalog.products.create.seoWidget.learnMore', 'Learn how to build your own')}\n </a>\n .\n </p>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { subscribeProductSeoValidation, setProductSeoTranslator } from './state'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\n\ntype SeoData = {\n title?: string | null\n name?: string | null\n description?: string | null\n}\n\ntype ValidationState = { ok: boolean; issues: string[]; message?: string }\n\ntype IssueKey = 'addTitle' | 'titleTooShort' | 'titleTooLong' | 'addDescription' | 'descriptionTooShort'\n\ntype SeoScore = { text: string; variant: StatusBadgeVariant }\n\nfunction computeIssueKeys(title: string, description: string): IssueKey[] {\n const issues: IssueKey[] = []\n if (!title) {\n issues.push('addTitle')\n } else {\n if (title.length < 10) issues.push('titleTooShort')\n if (title.length > 60) issues.push('titleTooLong')\n }\n if (!description) {\n issues.push('addDescription')\n } else if (description.length < 50) {\n issues.push('descriptionTooShort')\n }\n return issues\n}\n\nexport default function ProductSeoWidget({ data }: InjectionWidgetComponentProps<unknown, SeoData>) {\n const t = useT()\n // Expose the translator to the module's `onBeforeSave` hook (which has no React\n // context) so the save-block message is localized (#3299).\n React.useEffect(() => {\n setProductSeoTranslator(t)\n return () => setProductSeoTranslator(null)\n }, [t])\n const title = (data?.title || data?.name || '') ?? ''\n const description = data?.description ?? ''\n const baselineIssueKeys = React.useMemo(() => computeIssueKeys(title, description), [title, description])\n const [validation, setValidation] = React.useState<ValidationState>({ ok: baselineIssueKeys.length === 0, issues: baselineIssueKeys })\n\n React.useEffect(() => {\n setValidation({ ok: baselineIssueKeys.length === 0, issues: baselineIssueKeys })\n }, [baselineIssueKeys])\n\n React.useEffect(() => {\n return subscribeProductSeoValidation((payload) => {\n setValidation({\n ok: payload.ok,\n issues: payload.issues,\n message: payload.message,\n })\n })\n }, [])\n\n const titleScore = React.useMemo<SeoScore>(() => {\n if (!title) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'), variant: 'error' }\n if (title.length < 10) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'), variant: 'warning' }\n if (title.length > 60) return { text: t('catalog.products.create.seoWidget.tooLong', 'Too long'), variant: 'warning' }\n return { text: t('catalog.products.create.seoWidget.good', 'Good'), variant: 'success' }\n }, [title, t])\n\n const descScore = React.useMemo<SeoScore>(() => {\n if (!description) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'), variant: 'error' }\n if (description.length < 50) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'), variant: 'warning' }\n return { text: t('catalog.products.create.seoWidget.good', 'Good'), variant: 'success' }\n }, [description, t])\n\n const statusBadge = validation.ok ? (\n <StatusBadge variant=\"success\">\n {t('catalog.products.create.seoWidget.ready', 'Ready')}\n </StatusBadge>\n ) : (\n <StatusBadge variant=\"warning\">\n {t('catalog.products.create.seoWidget.needsAttention', 'Needs attention')}\n </StatusBadge>\n )\n\n const translateIssue = (issueKey: string): string => {\n return t(`catalog.products.create.seoWidget.issues.${issueKey}`, issueKey)\n }\n\n return (\n <div className=\"mt-4 w-full space-y-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <div className=\"text-sm font-semibold text-foreground\">{t('catalog.products.create.seoWidget.title', 'SEO Optimization')}</div>\n <p className=\"text-xs text-muted-foreground\">{t('catalog.products.create.seoWidget.hint', 'Keep titles 10\u201360 chars and descriptions 50+ chars.')}</p>\n </div>\n {statusBadge}\n </div>\n\n {validation.message || validation.issues.length ? (\n <Alert\n status={validation.ok ? 'success' : 'warning'}\n style=\"lighter\"\n size=\"sm\"\n showIcon={false}\n className=\"text-xs\"\n >\n {validation.message ? <div className=\"font-medium\">{validation.message}</div> : null}\n {validation.issues.length ? (\n <ul className=\"ml-4 list-disc space-y-1 pt-1\">\n {validation.issues.map((issue) => (\n <li key={issue}>{translateIssue(issue)}</li>\n ))}\n </ul>\n ) : null}\n </Alert>\n ) : null}\n\n <div className=\"rounded border bg-muted/30 p-3 text-sm\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-muted-foreground\">{t('catalog.products.create.seoWidget.titleLabel', 'Title ({{count}} chars)', { count: title.length })}</span>\n <StatusBadge variant={titleScore.variant}>{titleScore.text}</StatusBadge>\n </div>\n <div className=\"mt-2 flex items-center justify-between\">\n <span className=\"text-muted-foreground\">{t('catalog.products.create.seoWidget.descriptionLabel', 'Description ({{count}} chars)', { count: description.length })}</span>\n <StatusBadge variant={descScore.variant}>{descScore.text}</StatusBadge>\n </div>\n </div>\n\n <p className=\"text-overline text-muted-foreground\">\n {t('catalog.products.create.seoWidget.footer', 'Example widget powered by the injection system.')}{' '}\n <a className=\"text-primary underline\" href=\"/docs/framework/admin-ui/widget-injection\" target=\"_blank\" rel=\"noreferrer\">\n {t('catalog.products.create.seoWidget.learnMore', 'Learn how to build your own')}\n </a>\n .\n </p>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import ProductSeoWidget from "./widget.client.js";
2
- import { publishProductSeoValidation } from "./state.js";
2
+ import { publishProductSeoValidation, getProductSeoTranslator } from "./state.js";
3
3
  import { evaluateProductSeo } from "./validation.js";
4
4
  const widget = {
5
5
  metadata: {
@@ -14,7 +14,7 @@ const widget = {
14
14
  Widget: ProductSeoWidget,
15
15
  eventHandlers: {
16
16
  onBeforeSave: async (data) => {
17
- const evaluation = evaluateProductSeo(data);
17
+ const evaluation = evaluateProductSeo(data, getProductSeoTranslator());
18
18
  if (!evaluation.ok) {
19
19
  publishProductSeoValidation({ ok: false, issues: evaluation.issues, message: evaluation.message });
20
20
  return { ok: false, message: evaluation.message, fieldErrors: evaluation.fieldErrors };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/catalog/widgets/injection/product-seo/widget.ts"],
4
- "sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport ProductSeoWidget from './widget.client'\nimport { publishProductSeoValidation } from './state'\nimport { evaluateProductSeo } from './validation'\n\nconst widget: InjectionWidgetModule<any, any> = {\n metadata: {\n id: 'catalog.injection.product-seo',\n title: 'Product SEO Helper',\n description: 'Helps optimize product metadata for search engines',\n features: ['catalog.products.edit'],\n priority: 90,\n enabled: true,\n requiredFields: ['description'],\n },\n Widget: ProductSeoWidget,\n eventHandlers: {\n onBeforeSave: async (data) => {\n const evaluation = evaluateProductSeo(data as Record<string, unknown>)\n\n if (!evaluation.ok) {\n publishProductSeoValidation({ ok: false, issues: evaluation.issues, message: evaluation.message })\n return { ok: false, message: evaluation.message, fieldErrors: evaluation.fieldErrors }\n }\n\n publishProductSeoValidation({ ok: true, issues: [] })\n return { ok: true }\n },\n },\n}\n\nexport default widget\n"],
5
- "mappings": "AACA,OAAO,sBAAsB;AAC7B,SAAS,mCAAmC;AAC5C,SAAS,0BAA0B;AAEnC,MAAM,SAA0C;AAAA,EAC9C,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,uBAAuB;AAAA,IAClC,UAAU;AAAA,IACV,SAAS;AAAA,IACT,gBAAgB,CAAC,aAAa;AAAA,EAChC;AAAA,EACA,QAAQ;AAAA,EACR,eAAe;AAAA,IACb,cAAc,OAAO,SAAS;AAC5B,YAAM,aAAa,mBAAmB,IAA+B;AAErE,UAAI,CAAC,WAAW,IAAI;AAClB,oCAA4B,EAAE,IAAI,OAAO,QAAQ,WAAW,QAAQ,SAAS,WAAW,QAAQ,CAAC;AACjG,eAAO,EAAE,IAAI,OAAO,SAAS,WAAW,SAAS,aAAa,WAAW,YAAY;AAAA,MACvF;AAEA,kCAA4B,EAAE,IAAI,MAAM,QAAQ,CAAC,EAAE,CAAC;AACpD,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
4
+ "sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport ProductSeoWidget from './widget.client'\nimport { publishProductSeoValidation, getProductSeoTranslator } from './state'\nimport { evaluateProductSeo } from './validation'\n\nconst widget: InjectionWidgetModule<any, any> = {\n metadata: {\n id: 'catalog.injection.product-seo',\n title: 'Product SEO Helper',\n description: 'Helps optimize product metadata for search engines',\n features: ['catalog.products.edit'],\n priority: 90,\n enabled: true,\n requiredFields: ['description'],\n },\n Widget: ProductSeoWidget,\n eventHandlers: {\n onBeforeSave: async (data) => {\n const evaluation = evaluateProductSeo(data as Record<string, unknown>, getProductSeoTranslator())\n\n if (!evaluation.ok) {\n publishProductSeoValidation({ ok: false, issues: evaluation.issues, message: evaluation.message })\n return { ok: false, message: evaluation.message, fieldErrors: evaluation.fieldErrors }\n }\n\n publishProductSeoValidation({ ok: true, issues: [] })\n return { ok: true }\n },\n },\n}\n\nexport default widget\n"],
5
+ "mappings": "AACA,OAAO,sBAAsB;AAC7B,SAAS,6BAA6B,+BAA+B;AACrE,SAAS,0BAA0B;AAEnC,MAAM,SAA0C;AAAA,EAC9C,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,uBAAuB;AAAA,IAClC,UAAU;AAAA,IACV,SAAS;AAAA,IACT,gBAAgB,CAAC,aAAa;AAAA,EAChC;AAAA,EACA,QAAQ;AAAA,EACR,eAAe;AAAA,IACb,cAAc,OAAO,SAAS;AAC5B,YAAM,aAAa,mBAAmB,MAAiC,wBAAwB,CAAC;AAEhG,UAAI,CAAC,WAAW,IAAI;AAClB,oCAA4B,EAAE,IAAI,OAAO,QAAQ,WAAW,QAAQ,SAAS,WAAW,QAAQ,CAAC;AACjG,eAAO,EAAE,IAAI,OAAO,SAAS,WAAW,SAAS,aAAa,WAAW,YAAY;AAAA,MACvF;AAEA,kCAA4B,EAAE,IAAI,MAAM,QAAQ,CAAC,EAAE,CAAC;AACpD,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
@@ -109,7 +109,17 @@ const openApi = {
109
109
  { status: 200, description: "Weekly availability updated", schema: z.object({ ok: z.literal(true) }) },
110
110
  { status: 400, description: "Invalid payload", schema: z.object({ error: z.string() }) },
111
111
  { status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
112
- { status: 403, description: "Forbidden", schema: z.object({ error: z.string() }) }
112
+ { status: 403, description: "Forbidden", schema: z.object({ error: z.string() }) },
113
+ {
114
+ status: 409,
115
+ description: "Optimistic lock conflict",
116
+ schema: z.object({
117
+ error: z.string(),
118
+ code: z.literal("optimistic_lock_conflict"),
119
+ currentUpdatedAt: z.string(),
120
+ expectedUpdatedAt: z.string()
121
+ })
122
+ }
113
123
  ]
114
124
  }
115
125
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/planner/api/availability-weekly.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'\nimport { plannerAvailabilityWeeklyReplaceSchema } from '../data/validators'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { assertAvailabilityWriteAccess, resolveAvailabilityActorId } from './access'\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('planner.availability.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('planner.availability.errors.organizationRequired', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const input = parseScopedCommandInput(plannerAvailabilityWeeklyReplaceSchema, payload, ctx, translate)\n await assertAvailabilityWriteAccess(ctx, { subjectType: input.subjectType, subjectId: input.subjectId }, translate)\n const guardInput = {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: resolveAvailabilityActorId(ctx.auth),\n resourceKind: 'planner.availability',\n resourceId: input.subjectId,\n operation: 'custom' as const,\n requestMethod: req.method,\n requestHeaders: req.headers,\n }\n const guardResult = await validateCrudMutationGuard(ctx.container, { ...guardInput, mutationPayload: input })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute('planner.availability.weekly.replace', { input, ctx })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, { ...guardInput, metadata: guardResult.metadata ?? null })\n }\n const response = NextResponse.json({ ok: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'planner.availability',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('planner.availability.weekly.replace failed', err)\n return NextResponse.json(\n { error: translate('planner.availability.errors.updateWeekly', 'Failed to save weekly availability.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi = {\n tag: 'Planner',\n summary: 'Replace weekly availability',\n methods: {\n POST: {\n summary: 'Replace weekly availability',\n description: 'Replaces weekly availability rules for the subject in a single request.',\n requestBody: {\n contentType: 'application/json',\n schema: plannerAvailabilityWeeklyReplaceSchema,\n },\n responses: [\n { status: 200, description: 'Weekly availability updated', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'\nimport { plannerAvailabilityWeeklyReplaceSchema } from '../data/validators'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { assertAvailabilityWriteAccess, resolveAvailabilityActorId } from './access'\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('planner.availability.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('planner.availability.errors.organizationRequired', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const input = parseScopedCommandInput(plannerAvailabilityWeeklyReplaceSchema, payload, ctx, translate)\n await assertAvailabilityWriteAccess(ctx, { subjectType: input.subjectType, subjectId: input.subjectId }, translate)\n const guardInput = {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: resolveAvailabilityActorId(ctx.auth),\n resourceKind: 'planner.availability',\n resourceId: input.subjectId,\n operation: 'custom' as const,\n requestMethod: req.method,\n requestHeaders: req.headers,\n }\n const guardResult = await validateCrudMutationGuard(ctx.container, { ...guardInput, mutationPayload: input })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute('planner.availability.weekly.replace', { input, ctx })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, { ...guardInput, metadata: guardResult.metadata ?? null })\n }\n const response = NextResponse.json({ ok: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'planner.availability',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('planner.availability.weekly.replace failed', err)\n return NextResponse.json(\n { error: translate('planner.availability.errors.updateWeekly', 'Failed to save weekly availability.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi = {\n tag: 'Planner',\n summary: 'Replace weekly availability',\n methods: {\n POST: {\n summary: 'Replace weekly availability',\n description: 'Replaces weekly availability rules for the subject in a single request.',\n requestBody: {\n contentType: 'application/json',\n schema: plannerAvailabilityWeeklyReplaceSchema,\n },\n responses: [\n { status: 200, description: 'Weekly availability updated', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n {\n status: 409,\n description: 'Optimistic lock conflict',\n schema: z.object({\n error: z.string(),\n code: z.literal('optimistic_lock_conflict'),\n currentUpdatedAt: z.string(),\n expectedUpdatedAt: z.string(),\n }),\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5,12 +5,19 @@ import {
5
5
  plannerAvailabilityWeeklyReplaceSchema
6
6
  } from "../data/validators.js";
7
7
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
8
+ import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
8
9
  import {
9
10
  enforceCommandOptimisticLock,
10
11
  enforceRecordGoneIsConflict
11
12
  } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
12
13
  import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from "./shared.js";
14
+ import { plannerAvailabilityRuleSetCrudEvents } from "../lib/crud.js";
15
+ import { E } from "../../../generated/entities.ids.generated.js";
13
16
  const AVAILABILITY_RULE_RESOURCE_KIND = "planner.availability.rule";
17
+ const AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND = "planner.availability-rule-set";
18
+ const availabilityRuleSetCrudIndexer = {
19
+ entityType: E.planner.planner_availability_rule_set
20
+ };
14
21
  const AVAILABILITY_RULE_SET_RESOURCE_KIND = "planner.availability.rule.set";
15
22
  const DAY_CODES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
16
23
  function parseTimeInput(value) {
@@ -61,6 +68,13 @@ function toAvailabilityRuleSnapshot(record) {
61
68
  deletedAt: record.deletedAt ?? null
62
69
  };
63
70
  }
71
+ function nextRuleSetUpdatedAt(current, fallback) {
72
+ const currentMs = current instanceof Date ? current.getTime() : Number.NaN;
73
+ if (Number.isFinite(currentMs) && fallback.getTime() <= currentMs) {
74
+ return new Date(currentMs + 1);
75
+ }
76
+ return fallback;
77
+ }
64
78
  async function loadWeeklySnapshots(em, params) {
65
79
  const existing = await em.find(PlannerAvailabilityRule, {
66
80
  tenantId: params.tenantId,
@@ -125,7 +139,7 @@ const replaceWeeklyAvailabilityCommand = {
125
139
  ensureOrganizationScope(ctx, parsed.organizationId);
126
140
  const em = ctx.container.resolve("em").fork();
127
141
  const now = /* @__PURE__ */ new Date();
128
- await em.transactional(async (trx) => {
142
+ const touchedRuleSet = await em.transactional(async (trx) => {
129
143
  let ruleSet = null;
130
144
  if (parsed.subjectType === "ruleset") {
131
145
  ruleSet = await trx.findOne(PlannerAvailabilityRuleSet, {
@@ -188,11 +202,27 @@ const replaceWeeklyAvailabilityCommand = {
188
202
  trx.persist(record);
189
203
  });
190
204
  if (ruleSet) {
191
- ruleSet.updatedAt = now;
205
+ ruleSet.updatedAt = nextRuleSetUpdatedAt(ruleSet.updatedAt, now);
192
206
  trx.persist(ruleSet);
193
207
  }
194
208
  await trx.flush();
209
+ return ruleSet;
195
210
  });
211
+ if (touchedRuleSet) {
212
+ const dataEngine = ctx.container.resolve("dataEngine");
213
+ await emitCrudSideEffects({
214
+ dataEngine,
215
+ action: "updated",
216
+ entity: touchedRuleSet,
217
+ identifiers: {
218
+ id: touchedRuleSet.id,
219
+ organizationId: touchedRuleSet.organizationId,
220
+ tenantId: touchedRuleSet.tenantId
221
+ },
222
+ events: plannerAvailabilityRuleSetCrudEvents,
223
+ indexer: availabilityRuleSetCrudIndexer
224
+ });
225
+ }
196
226
  return { ok: true };
197
227
  },
198
228
  buildLog: async ({ input, snapshots, ctx }) => {
@@ -219,7 +249,8 @@ const replaceWeeklyAvailabilityCommand = {
219
249
  before,
220
250
  after
221
251
  }
222
- }
252
+ },
253
+ context: parsed.subjectType === "ruleset" ? { cacheAliases: [AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND] } : null
223
254
  };
224
255
  },
225
256
  undo: async ({ logEntry, ctx }) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/planner/commands/availability-weekly.ts"],
4
- "sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'\nimport { PlannerAvailabilityRule, PlannerAvailabilityRuleSet } from '../data/entities'\nimport {\n plannerAvailabilityWeeklyReplaceSchema,\n type PlannerAvailabilityWeeklyReplaceInput,\n} from '../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'\nimport {\n enforceCommandOptimisticLock,\n enforceRecordGoneIsConflict,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'\n\nconst AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'\n\n// Canonical resource kind for the parent rule set, matching the tag the CRUD\n// factory derives for `planner.availability-rule-sets.*` commands. Weekly\n// replace mutates the rule set's child `availability_rules`, so the parent is\n// the optimistic-lock consistency boundary (document-aggregate pattern).\nconst AVAILABILITY_RULE_SET_RESOURCE_KIND = 'planner.availability.rule.set'\n\nconst DAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']\n\ntype AvailabilityRuleSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n timezone: string\n rrule: string\n exdates: string[]\n kind: PlannerAvailabilityKind\n note: string | null\n deletedAt: Date | null\n}\n\ntype WeeklyUndoPayload = {\n before: AvailabilityRuleSnapshot[]\n after: AvailabilityRuleSnapshot[]\n}\n\nfunction parseTimeInput(value: string): { hours: number; minutes: number } | null {\n const [hours, minutes] = value.split(':').map((part) => Number(part))\n if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null\n if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null\n return { hours, minutes }\n}\n\nfunction toDateForWeekday(weekday: number, time: string): Date | null {\n const parsed = parseTimeInput(time)\n if (!parsed) return null\n const now = new Date()\n const base = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const diff = (weekday - base.getDay() + 7) % 7\n const target = new Date(base.getTime() + diff * 24 * 60 * 60 * 1000)\n target.setHours(parsed.hours, parsed.minutes, 0, 0)\n return target\n}\n\nfunction formatDuration(minutes: number): string {\n const clamped = Math.max(1, minutes)\n const hours = Math.floor(clamped / 60)\n const mins = clamped % 60\n if (hours > 0 && mins > 0) return `PT${hours}H${mins}M`\n if (hours > 0) return `PT${hours}H`\n return `PT${mins}M`\n}\n\nfunction buildWeeklyRrule(start: Date, end: Date): string {\n const dtStart = start.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'\n const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 60000))\n const duration = formatDuration(durationMinutes)\n const dayCode = DAY_CODES[start.getDay()] ?? 'MO'\n return `DTSTART:${dtStart}\\nDURATION:${duration}\\nRRULE:FREQ=WEEKLY;BYDAY=${dayCode}`\n}\n\nfunction toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): AvailabilityRuleSnapshot {\n return {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n subjectType: record.subjectType,\n subjectId: record.subjectId,\n timezone: record.timezone,\n rrule: record.rrule,\n exdates: [...(record.exdates ?? [])],\n kind: record.kind,\n note: record.note ?? null,\n deletedAt: record.deletedAt ?? null,\n }\n}\n\nasync function loadWeeklySnapshots(\n em: EntityManager,\n params: {\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n }\n): Promise<AvailabilityRuleSnapshot[]> {\n const existing = await em.find(PlannerAvailabilityRule, {\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n subjectType: params.subjectType,\n subjectId: params.subjectId,\n deletedAt: null,\n })\n return existing\n .filter((rule) => {\n const repeat = parseAvailabilityRuleWindow(rule).repeat\n return repeat === 'weekly' || repeat === 'daily'\n })\n .map(toAvailabilityRuleSnapshot)\n}\n\nasync function restoreAvailabilityRuleFromSnapshot(em: EntityManager, snapshot: AvailabilityRuleSnapshot): Promise<void> {\n let record = await em.findOne(PlannerAvailabilityRule, { id: snapshot.id })\n if (!record) {\n record = em.create(PlannerAvailabilityRule, {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n subjectType: snapshot.subjectType,\n subjectId: snapshot.subjectId,\n timezone: snapshot.timezone,\n rrule: snapshot.rrule,\n exdates: snapshot.exdates ?? [],\n kind: snapshot.kind ?? 'availability',\n note: snapshot.note ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: snapshot.deletedAt ?? null,\n })\n em.persist(record)\n } else {\n record.subjectType = snapshot.subjectType\n record.subjectId = snapshot.subjectId\n record.timezone = snapshot.timezone\n record.rrule = snapshot.rrule\n record.exdates = snapshot.exdates ?? []\n record.kind = snapshot.kind ?? 'availability'\n record.note = snapshot.note ?? null\n record.deletedAt = snapshot.deletedAt ?? null\n }\n}\n\nconst replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeeklyReplaceInput, { ok: true }> = {\n id: 'planner.availability.weekly.replace',\n async prepare(input, ctx) {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager)\n const before = await loadWeeklySnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n })\n return { before }\n },\n async execute(input, ctx) {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const now = new Date()\n\n await em.transactional(async (trx) => {\n // The weekly rules of a rule set are a sub-resource of that rule set: the\n // parent is the optimistic-lock consistency boundary. Guard the parent's\n // version (so a stale weekly save loses to a concurrent rule-set\n // change/delete) and bump its `updated_at` after the replace (so a\n // concurrent rule-set delete/update with a stale token conflicts). See #2927.\n let ruleSet: PlannerAvailabilityRuleSet | null = null\n if (parsed.subjectType === 'ruleset') {\n ruleSet = await trx.findOne(PlannerAvailabilityRuleSet, {\n id: parsed.subjectId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (ruleSet) {\n enforceCommandOptimisticLock({\n resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,\n resourceId: ruleSet.id,\n current: ruleSet.updatedAt,\n request: ctx.request ?? null,\n })\n } else {\n // The rule set was deleted concurrently. When the client opted into\n // optimistic locking, surface the unified conflict instead of\n // silently writing orphan rules; otherwise preserve legacy behavior.\n enforceRecordGoneIsConflict({\n resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,\n resourceId: parsed.subjectId,\n request: ctx.request ?? null,\n })\n }\n }\n\n const existing = await trx.find(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n deletedAt: null,\n })\n\n const toDelete = existing.filter((rule) => {\n const repeat = parseAvailabilityRuleWindow(rule).repeat\n return repeat === 'weekly' || repeat === 'daily'\n })\n\n toDelete.forEach((rule) => {\n rule.deletedAt = now\n rule.updatedAt = now\n })\n\n if (toDelete.length) {\n trx.persist(toDelete)\n }\n\n parsed.windows.forEach((window) => {\n const start = toDateForWeekday(window.weekday, window.start)\n const end = toDateForWeekday(window.weekday, window.end)\n if (!start || !end || start >= end) return\n const rrule = buildWeeklyRrule(start, end)\n const record = trx.create(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n timezone: parsed.timezone,\n rrule,\n exdates: [],\n kind: 'availability',\n note: null,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(record)\n })\n\n if (ruleSet) {\n ruleSet.updatedAt = now\n trx.persist(ruleSet)\n }\n\n await trx.flush()\n })\n\n return { ok: true }\n },\n buildLog: async ({ input, snapshots, ctx }) => {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadWeeklySnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n })\n const before = (snapshots.before as AvailabilityRuleSnapshot[] | undefined) ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('planner.audit.availability.weekly.replace', 'Replace weekly availability'),\n resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,\n resourceId: null,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: {\n before,\n after,\n } satisfies WeeklyUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<WeeklyUndoPayload>(logEntry)\n const before = payload?.before ?? []\n const after = payload?.after ?? []\n if (!before.length && !after.length) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.transactional(async (trx) => {\n if (after.length) {\n const ids = after.map((rule) => rule.id)\n const records = await trx.find(PlannerAvailabilityRule, { id: { $in: ids } })\n records.forEach((record) => {\n record.deletedAt = new Date()\n })\n if (records.length) trx.persist(records)\n }\n\n for (const snapshot of before) {\n await restoreAvailabilityRuleFromSnapshot(trx, { ...snapshot, deletedAt: null })\n }\n\n await trx.flush()\n })\n },\n}\n\nregisterCommand(replaceWeeklyAvailabilityCommand)\n"],
5
- "mappings": "AACA,SAAS,uBAAuB;AAEhC,SAAS,mCAAmC;AAC5C,SAAS,yBAAyB,kCAAkC;AACpE;AAAA,EACE;AAAA,OAEK;AACP,SAAS,2BAA2B;AAEpC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB,mBAAmB,0BAA0B;AAE/E,MAAM,kCAAkC;AAMxC,MAAM,sCAAsC;AAE5C,MAAM,YAAY,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,IAAI;AAqB3D,SAAS,eAAe,OAA0D;AAChF,QAAM,CAAC,OAAO,OAAO,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC;AACpE,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,OAAO,EAAG,QAAO;AACjE,MAAI,QAAQ,KAAK,QAAQ,MAAM,UAAU,KAAK,UAAU,GAAI,QAAO;AACnE,SAAO,EAAE,OAAO,QAAQ;AAC1B;AAEA,SAAS,iBAAiB,SAAiB,MAA2B;AACpE,QAAM,SAAS,eAAe,IAAI;AAClC,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,OAAO,IAAI,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,GAAG,IAAI,QAAQ,CAAC;AACtE,QAAM,QAAQ,UAAU,KAAK,OAAO,IAAI,KAAK;AAC7C,QAAM,SAAS,IAAI,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,KAAK,GAAI;AACnE,SAAO,SAAS,OAAO,OAAO,OAAO,SAAS,GAAG,CAAC;AAClD,SAAO;AACT;AAEA,SAAS,eAAe,SAAyB;AAC/C,QAAM,UAAU,KAAK,IAAI,GAAG,OAAO;AACnC,QAAM,QAAQ,KAAK,MAAM,UAAU,EAAE;AACrC,QAAM,OAAO,UAAU;AACvB,MAAI,QAAQ,KAAK,OAAO,EAAG,QAAO,KAAK,KAAK,IAAI,IAAI;AACpD,MAAI,QAAQ,EAAG,QAAO,KAAK,KAAK;AAChC,SAAO,KAAK,IAAI;AAClB;AAEA,SAAS,iBAAiB,OAAa,KAAmB;AACxD,QAAM,UAAU,MAAM,YAAY,EAAE,QAAQ,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,IAAI;AACzE,QAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,OAAO,IAAI,QAAQ,IAAI,MAAM,QAAQ,KAAK,GAAK,CAAC;AACzF,QAAM,WAAW,eAAe,eAAe;AAC/C,QAAM,UAAU,UAAU,MAAM,OAAO,CAAC,KAAK;AAC7C,SAAO,WAAW,OAAO;AAAA,WAAc,QAAQ;AAAA,0BAA6B,OAAO;AACrF;AAEA,SAAS,2BAA2B,QAA2D;AAC7F,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,UAAU,OAAO;AAAA,IACjB,gBAAgB,OAAO;AAAA,IACvB,aAAa,OAAO;AAAA,IACpB,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO;AAAA,IACjB,OAAO,OAAO;AAAA,IACd,SAAS,CAAC,GAAI,OAAO,WAAW,CAAC,CAAE;AAAA,IACnC,MAAM,OAAO;AAAA,IACb,MAAM,OAAO,QAAQ;AAAA,IACrB,WAAW,OAAO,aAAa;AAAA,EACjC;AACF;AAEA,eAAe,oBACb,IACA,QAMqC;AACrC,QAAM,WAAW,MAAM,GAAG,KAAK,yBAAyB;AAAA,IACtD,UAAU,OAAO;AAAA,IACjB,gBAAgB,OAAO;AAAA,IACvB,aAAa,OAAO;AAAA,IACpB,WAAW,OAAO;AAAA,IAClB,WAAW;AAAA,EACb,CAAC;AACD,SAAO,SACJ,OAAO,CAAC,SAAS;AAChB,UAAM,SAAS,4BAA4B,IAAI,EAAE;AACjD,WAAO,WAAW,YAAY,WAAW;AAAA,EAC3C,CAAC,EACA,IAAI,0BAA0B;AACnC;AAEA,eAAe,oCAAoC,IAAmB,UAAmD;AACvH,MAAI,SAAS,MAAM,GAAG,QAAQ,yBAAyB,EAAE,IAAI,SAAS,GAAG,CAAC;AAC1E,MAAI,CAAC,QAAQ;AACX,aAAS,GAAG,OAAO,yBAAyB;AAAA,MAC1C,IAAI,SAAS;AAAA,MACb,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,MACzB,aAAa,SAAS;AAAA,MACtB,WAAW,SAAS;AAAA,MACpB,UAAU,SAAS;AAAA,MACnB,OAAO,SAAS;AAAA,MAChB,SAAS,SAAS,WAAW,CAAC;AAAA,MAC9B,MAAM,SAAS,QAAQ;AAAA,MACvB,MAAM,SAAS,QAAQ;AAAA,MACvB,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,SAAS,aAAa;AAAA,IACnC,CAAC;AACD,OAAG,QAAQ,MAAM;AAAA,EACnB,OAAO;AACL,WAAO,cAAc,SAAS;AAC9B,WAAO,YAAY,SAAS;AAC5B,WAAO,WAAW,SAAS;AAC3B,WAAO,QAAQ,SAAS;AACxB,WAAO,UAAU,SAAS,WAAW,CAAC;AACtC,WAAO,OAAO,SAAS,QAAQ;AAC/B,WAAO,OAAO,SAAS,QAAQ;AAC/B,WAAO,YAAY,SAAS,aAAa;AAAA,EAC3C;AACF;AAEA,MAAM,mCAAwG;AAAA,EAC5G,IAAI;AAAA,EACJ,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,SAAS,uCAAuC,MAAM,KAAK;AACjE,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAClD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI;AACtC,UAAM,SAAS,MAAM,oBAAoB,IAAI;AAAA,MAC3C,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,aAAa,OAAO;AAAA,MACpB,WAAW,OAAO;AAAA,IACpB,CAAC;AACD,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EACA,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,SAAS,uCAAuC,MAAM,KAAK;AACjE,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,oBAAI,KAAK;AAErB,UAAM,GAAG,cAAc,OAAO,QAAQ;AAMpC,UAAI,UAA6C;AACjD,UAAI,OAAO,gBAAgB,WAAW;AACpC,kBAAU,MAAM,IAAI,QAAQ,4BAA4B;AAAA,UACtD,IAAI,OAAO;AAAA,UACX,UAAU,OAAO;AAAA,UACjB,gBAAgB,OAAO;AAAA,UACvB,WAAW;AAAA,QACb,CAAC;AACD,YAAI,SAAS;AACX,uCAA6B;AAAA,YAC3B,cAAc;AAAA,YACd,YAAY,QAAQ;AAAA,YACpB,SAAS,QAAQ;AAAA,YACjB,SAAS,IAAI,WAAW;AAAA,UAC1B,CAAC;AAAA,QACH,OAAO;AAIL,sCAA4B;AAAA,YAC1B,cAAc;AAAA,YACd,YAAY,OAAO;AAAA,YACnB,SAAS,IAAI,WAAW;AAAA,UAC1B,CAAC;AAAA,QACH;AAAA,MACF;AAEA,YAAM,WAAW,MAAM,IAAI,KAAK,yBAAyB;AAAA,QACvD,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,aAAa,OAAO;AAAA,QACpB,WAAW,OAAO;AAAA,QAClB,WAAW;AAAA,MACb,CAAC;AAED,YAAM,WAAW,SAAS,OAAO,CAAC,SAAS;AACzC,cAAM,SAAS,4BAA4B,IAAI,EAAE;AACjD,eAAO,WAAW,YAAY,WAAW;AAAA,MAC3C,CAAC;AAED,eAAS,QAAQ,CAAC,SAAS;AACzB,aAAK,YAAY;AACjB,aAAK,YAAY;AAAA,MACnB,CAAC;AAED,UAAI,SAAS,QAAQ;AACnB,YAAI,QAAQ,QAAQ;AAAA,MACtB;AAEA,aAAO,QAAQ,QAAQ,CAAC,WAAW;AACjC,cAAM,QAAQ,iBAAiB,OAAO,SAAS,OAAO,KAAK;AAC3D,cAAM,MAAM,iBAAiB,OAAO,SAAS,OAAO,GAAG;AACvD,YAAI,CAAC,SAAS,CAAC,OAAO,SAAS,IAAK;AACpC,cAAM,QAAQ,iBAAiB,OAAO,GAAG;AACzC,cAAM,SAAS,IAAI,OAAO,yBAAyB;AAAA,UACjD,UAAU,OAAO;AAAA,UACjB,gBAAgB,OAAO;AAAA,UACvB,aAAa,OAAO;AAAA,UACpB,WAAW,OAAO;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB;AAAA,UACA,SAAS,CAAC;AAAA,UACV,MAAM;AAAA,UACN,MAAM;AAAA,UACN,WAAW;AAAA,UACX,WAAW;AAAA,QACb,CAAC;AACD,YAAI,QAAQ,MAAM;AAAA,MACpB,CAAC;AAED,UAAI,SAAS;AACX,gBAAQ,YAAY;AACpB,YAAI,QAAQ,OAAO;AAAA,MACrB;AAEA,YAAM,IAAI,MAAM;AAAA,IAClB,CAAC;AAED,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AAAA,EACA,UAAU,OAAO,EAAE,OAAO,WAAW,IAAI,MAAM;AAC7C,UAAM,SAAS,uCAAuC,MAAM,KAAK;AACjE,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,QAAQ,MAAM,oBAAoB,IAAI;AAAA,MAC1C,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,aAAa,OAAO;AAAA,MACpB,WAAW,OAAO;AAAA,IACpB,CAAC;AACD,UAAM,SAAU,UAAU,UAAqD,CAAC;AAChF,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO;AAAA,MACL,aAAa,UAAU,6CAA6C,6BAA6B;AAAA,MACjG,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,SAAS;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAAsC,QAAQ;AAC9D,UAAM,SAAS,SAAS,UAAU,CAAC;AACnC,UAAM,QAAQ,SAAS,SAAS,CAAC;AACjC,QAAI,CAAC,OAAO,UAAU,CAAC,MAAM,OAAQ;AACrC,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,GAAG,cAAc,OAAO,QAAQ;AACpC,UAAI,MAAM,QAAQ;AAChB,cAAM,MAAM,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE;AACvC,cAAM,UAAU,MAAM,IAAI,KAAK,yBAAyB,EAAE,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;AAC5E,gBAAQ,QAAQ,CAAC,WAAW;AAC1B,iBAAO,YAAY,oBAAI,KAAK;AAAA,QAC9B,CAAC;AACD,YAAI,QAAQ,OAAQ,KAAI,QAAQ,OAAO;AAAA,MACzC;AAEA,iBAAW,YAAY,QAAQ;AAC7B,cAAM,oCAAoC,KAAK,EAAE,GAAG,UAAU,WAAW,KAAK,CAAC;AAAA,MACjF;AAEA,YAAM,IAAI,MAAM;AAAA,IAClB,CAAC;AAAA,EACH;AACF;AAEA,gBAAgB,gCAAgC;",
4
+ "sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'\nimport { PlannerAvailabilityRule, PlannerAvailabilityRuleSet } from '../data/entities'\nimport {\n plannerAvailabilityWeeklyReplaceSchema,\n type PlannerAvailabilityWeeklyReplaceInput,\n} from '../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport {\n enforceCommandOptimisticLock,\n enforceRecordGoneIsConflict,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'\nimport { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'\nimport { plannerAvailabilityRuleSetCrudEvents } from '../lib/crud'\nimport { E } from '#generated/entities.ids.generated'\n\nconst AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'\nconst AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND = 'planner.availability-rule-set'\n\nconst availabilityRuleSetCrudIndexer: CrudIndexerConfig<PlannerAvailabilityRuleSet> = {\n entityType: E.planner.planner_availability_rule_set,\n}\n\n// Canonical resource kind for the parent rule set, matching the tag the CRUD\n// factory derives for `planner.availability-rule-sets.*` commands. Weekly\n// replace mutates the rule set's child `availability_rules`, so the parent is\n// the optimistic-lock consistency boundary (document-aggregate pattern).\nconst AVAILABILITY_RULE_SET_RESOURCE_KIND = 'planner.availability.rule.set'\n\nconst DAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']\n\ntype AvailabilityRuleSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n timezone: string\n rrule: string\n exdates: string[]\n kind: PlannerAvailabilityKind\n note: string | null\n deletedAt: Date | null\n}\n\ntype WeeklyUndoPayload = {\n before: AvailabilityRuleSnapshot[]\n after: AvailabilityRuleSnapshot[]\n}\n\nfunction parseTimeInput(value: string): { hours: number; minutes: number } | null {\n const [hours, minutes] = value.split(':').map((part) => Number(part))\n if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null\n if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null\n return { hours, minutes }\n}\n\nfunction toDateForWeekday(weekday: number, time: string): Date | null {\n const parsed = parseTimeInput(time)\n if (!parsed) return null\n const now = new Date()\n const base = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const diff = (weekday - base.getDay() + 7) % 7\n const target = new Date(base.getTime() + diff * 24 * 60 * 60 * 1000)\n target.setHours(parsed.hours, parsed.minutes, 0, 0)\n return target\n}\n\nfunction formatDuration(minutes: number): string {\n const clamped = Math.max(1, minutes)\n const hours = Math.floor(clamped / 60)\n const mins = clamped % 60\n if (hours > 0 && mins > 0) return `PT${hours}H${mins}M`\n if (hours > 0) return `PT${hours}H`\n return `PT${mins}M`\n}\n\nfunction buildWeeklyRrule(start: Date, end: Date): string {\n const dtStart = start.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'\n const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 60000))\n const duration = formatDuration(durationMinutes)\n const dayCode = DAY_CODES[start.getDay()] ?? 'MO'\n return `DTSTART:${dtStart}\\nDURATION:${duration}\\nRRULE:FREQ=WEEKLY;BYDAY=${dayCode}`\n}\n\nfunction toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): AvailabilityRuleSnapshot {\n return {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n subjectType: record.subjectType,\n subjectId: record.subjectId,\n timezone: record.timezone,\n rrule: record.rrule,\n exdates: [...(record.exdates ?? [])],\n kind: record.kind,\n note: record.note ?? null,\n deletedAt: record.deletedAt ?? null,\n }\n}\n\nfunction nextRuleSetUpdatedAt(current: Date | null | undefined, fallback: Date): Date {\n const currentMs = current instanceof Date ? current.getTime() : Number.NaN\n if (Number.isFinite(currentMs) && fallback.getTime() <= currentMs) {\n return new Date(currentMs + 1)\n }\n return fallback\n}\n\nasync function loadWeeklySnapshots(\n em: EntityManager,\n params: {\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n }\n): Promise<AvailabilityRuleSnapshot[]> {\n const existing = await em.find(PlannerAvailabilityRule, {\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n subjectType: params.subjectType,\n subjectId: params.subjectId,\n deletedAt: null,\n })\n return existing\n .filter((rule) => {\n const repeat = parseAvailabilityRuleWindow(rule).repeat\n return repeat === 'weekly' || repeat === 'daily'\n })\n .map(toAvailabilityRuleSnapshot)\n}\n\nasync function restoreAvailabilityRuleFromSnapshot(em: EntityManager, snapshot: AvailabilityRuleSnapshot): Promise<void> {\n let record = await em.findOne(PlannerAvailabilityRule, { id: snapshot.id })\n if (!record) {\n record = em.create(PlannerAvailabilityRule, {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n subjectType: snapshot.subjectType,\n subjectId: snapshot.subjectId,\n timezone: snapshot.timezone,\n rrule: snapshot.rrule,\n exdates: snapshot.exdates ?? [],\n kind: snapshot.kind ?? 'availability',\n note: snapshot.note ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: snapshot.deletedAt ?? null,\n })\n em.persist(record)\n } else {\n record.subjectType = snapshot.subjectType\n record.subjectId = snapshot.subjectId\n record.timezone = snapshot.timezone\n record.rrule = snapshot.rrule\n record.exdates = snapshot.exdates ?? []\n record.kind = snapshot.kind ?? 'availability'\n record.note = snapshot.note ?? null\n record.deletedAt = snapshot.deletedAt ?? null\n }\n}\n\nconst replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeeklyReplaceInput, { ok: true }> = {\n id: 'planner.availability.weekly.replace',\n async prepare(input, ctx) {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager)\n const before = await loadWeeklySnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n })\n return { before }\n },\n async execute(input, ctx) {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const now = new Date()\n\n // The weekly rules of a rule set are a sub-resource of that rule set: the\n // parent is the optimistic-lock consistency boundary. Guard the parent's\n // version (so a stale weekly save loses to a concurrent rule-set\n // change/delete) and bump its `updated_at` after the replace (so a\n // concurrent rule-set delete/update with a stale token conflicts). See #2927.\n const touchedRuleSet = await em.transactional(async (trx): Promise<PlannerAvailabilityRuleSet | null> => {\n let ruleSet: PlannerAvailabilityRuleSet | null = null\n if (parsed.subjectType === 'ruleset') {\n ruleSet = await trx.findOne(PlannerAvailabilityRuleSet, {\n id: parsed.subjectId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (ruleSet) {\n enforceCommandOptimisticLock({\n resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,\n resourceId: ruleSet.id,\n current: ruleSet.updatedAt,\n request: ctx.request ?? null,\n })\n } else {\n // The rule set was deleted concurrently. When the client opted into\n // optimistic locking, surface the unified conflict instead of\n // silently writing orphan rules; otherwise preserve legacy behavior.\n enforceRecordGoneIsConflict({\n resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,\n resourceId: parsed.subjectId,\n request: ctx.request ?? null,\n })\n }\n }\n\n const existing = await trx.find(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n deletedAt: null,\n })\n\n const toDelete = existing.filter((rule) => {\n const repeat = parseAvailabilityRuleWindow(rule).repeat\n return repeat === 'weekly' || repeat === 'daily'\n })\n\n toDelete.forEach((rule) => {\n rule.deletedAt = now\n rule.updatedAt = now\n })\n\n if (toDelete.length) {\n trx.persist(toDelete)\n }\n\n parsed.windows.forEach((window) => {\n const start = toDateForWeekday(window.weekday, window.start)\n const end = toDateForWeekday(window.weekday, window.end)\n if (!start || !end || start >= end) return\n const rrule = buildWeeklyRrule(start, end)\n const record = trx.create(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n timezone: parsed.timezone,\n rrule,\n exdates: [],\n kind: 'availability',\n note: null,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(record)\n })\n\n if (ruleSet) {\n ruleSet.updatedAt = nextRuleSetUpdatedAt(ruleSet.updatedAt, now)\n trx.persist(ruleSet)\n }\n\n await trx.flush()\n return ruleSet\n })\n\n if (touchedRuleSet) {\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: touchedRuleSet,\n identifiers: {\n id: touchedRuleSet.id,\n organizationId: touchedRuleSet.organizationId,\n tenantId: touchedRuleSet.tenantId,\n },\n events: plannerAvailabilityRuleSetCrudEvents,\n indexer: availabilityRuleSetCrudIndexer,\n })\n }\n\n return { ok: true }\n },\n buildLog: async ({ input, snapshots, ctx }) => {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadWeeklySnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n })\n const before = (snapshots.before as AvailabilityRuleSnapshot[] | undefined) ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('planner.audit.availability.weekly.replace', 'Replace weekly availability'),\n resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,\n resourceId: null,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: {\n before,\n after,\n } satisfies WeeklyUndoPayload,\n },\n context: parsed.subjectType === 'ruleset'\n ? { cacheAliases: [AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND] }\n : null,\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<WeeklyUndoPayload>(logEntry)\n const before = payload?.before ?? []\n const after = payload?.after ?? []\n if (!before.length && !after.length) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.transactional(async (trx) => {\n if (after.length) {\n const ids = after.map((rule) => rule.id)\n const records = await trx.find(PlannerAvailabilityRule, { id: { $in: ids } })\n records.forEach((record) => {\n record.deletedAt = new Date()\n })\n if (records.length) trx.persist(records)\n }\n\n for (const snapshot of before) {\n await restoreAvailabilityRuleFromSnapshot(trx, { ...snapshot, deletedAt: null })\n }\n\n await trx.flush()\n })\n },\n}\n\nregisterCommand(replaceWeeklyAvailabilityCommand)\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -19,6 +19,7 @@ import { createCrud, deleteCrud, updateCrud } from "@open-mercato/ui/backend/uti
19
19
  import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
20
20
  import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
21
21
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
22
+ import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
22
23
  import { Spinner } from "@open-mercato/ui/primitives/spinner";
23
24
  import { ComboboxInput, TimePicker } from "@open-mercato/ui/backend/inputs";
24
25
  import { DictionaryEntrySelect } from "@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect";
@@ -29,7 +30,6 @@ import {
29
30
  import { useT } from "@open-mercato/shared/lib/i18n/context";
30
31
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
31
32
  import { normalizeCrudServerError } from "@open-mercato/ui/backend/utils/serverErrors";
32
- import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
33
33
  import { parseAvailabilityRuleWindow } from "@open-mercato/core/modules/planner/lib/availabilitySchedule";
34
34
  import { deleteAvailabilityRuleSet } from "@open-mercato/core/modules/planner/lib/deleteAvailabilityRuleSet";
35
35
  import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
@@ -650,6 +650,11 @@ function AvailabilityRulesEditor({
650
650
  React.useEffect(() => {
651
651
  void refreshBookedEvents();
652
652
  }, [refreshBookedEvents]);
653
+ const refreshAfterRuleSetConflict = React.useCallback(() => {
654
+ void refreshRuleSets();
655
+ void refreshRuleSetRules();
656
+ void refreshAvailability();
657
+ }, [refreshAvailability, refreshRuleSetRules, refreshRuleSets]);
653
658
  const weeklyDraft = React.useMemo(() => buildWeeklyDraft(activeRules), [activeRules]);
654
659
  const [weeklyWindows, setWeeklyWindows] = React.useState(() => cloneWeeklyWindows(weeklyDraft));
655
660
  const weeklyWindowsRef = React.useRef(cloneWeeklyWindows(weeklyDraft));
@@ -746,14 +751,15 @@ function AvailabilityRulesEditor({
746
751
  if (!shouldSkipRefresh) {
747
752
  await refreshAvailability();
748
753
  await refreshRuleSetRules();
754
+ if (usingRuleSet) {
755
+ await refreshRuleSets();
756
+ }
749
757
  }
750
758
  if (parentRuleSet) {
751
759
  await refreshRuleSets();
752
760
  }
753
761
  } catch (error2) {
754
- if (surfaceRecordConflict(error2, t)) {
755
- return;
756
- }
762
+ if (surfaceRecordConflict(error2, t, { onRefresh: refreshAfterRuleSetConflict })) return;
757
763
  const message = error2 instanceof Error ? error2.message : listLabels.saveWeeklyError;
758
764
  flash(message, "error");
759
765
  } finally {
@@ -765,8 +771,9 @@ function AvailabilityRulesEditor({
765
771
  refreshAvailability,
766
772
  refreshRuleSetRules,
767
773
  refreshRuleSets,
768
- ruleSets,
774
+ refreshAfterRuleSetConflict,
769
775
  effectiveRulesetId,
776
+ ruleSets,
770
777
  subjectId,
771
778
  subjectType,
772
779
  t,
@@ -1010,7 +1017,7 @@ function AvailabilityRulesEditor({
1010
1017
  refreshRuleSets,
1011
1018
  onSuccess: () => flash(listLabels.ruleSetDeleteSuccess, "success"),
1012
1019
  onError: (error2) => {
1013
- if (surfaceRecordConflict(error2, t)) return;
1020
+ if (surfaceRecordConflict(error2, t, { onRefresh: refreshAfterRuleSetConflict })) return;
1014
1021
  console.error("planner.availability-rule-sets.delete", error2);
1015
1022
  const normalized = normalizeCrudServerError(error2);
1016
1023
  flash(normalized.message ?? listLabels.ruleSetDeleteError, "error");
@@ -1025,6 +1032,7 @@ function AvailabilityRulesEditor({
1025
1032
  listLabels.ruleSetDeleteSuccess,
1026
1033
  onRulesetChange,
1027
1034
  refreshAvailability,
1035
+ refreshAfterRuleSetConflict,
1028
1036
  refreshRuleSets,
1029
1037
  ruleSets,
1030
1038
  rulesetId,