@koda-plus/medusa-plugin-subiekt-nexo 0.0.0-stage → 0.2.0

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 (94) hide show
  1. package/.medusa/server/src/admin/index.js +3588 -0
  2. package/.medusa/server/src/admin/index.mjs +3589 -0
  3. package/.medusa/server/src/api/admin/subiekt/check/route.js +22 -0
  4. package/.medusa/server/src/api/admin/subiekt/documents/route.js +35 -0
  5. package/.medusa/server/src/api/admin/subiekt/helpers.js +187 -0
  6. package/.medusa/server/src/api/admin/subiekt/orders/[id]/documents/route.js +62 -0
  7. package/.medusa/server/src/api/admin/subiekt/orders/[id]/route.js +29 -0
  8. package/.medusa/server/src/api/admin/subiekt/orders/[id]/send/route.js +41 -0
  9. package/.medusa/server/src/api/admin/subiekt/products/release/route.js +23 -0
  10. package/.medusa/server/src/api/admin/subiekt/products/route.js +77 -0
  11. package/.medusa/server/src/api/admin/subiekt/route.js +16 -0
  12. package/.medusa/server/src/api/admin/subiekt/runs/route.js +16 -0
  13. package/.medusa/server/src/api/admin/subiekt/sync/route.js +52 -0
  14. package/.medusa/server/src/api/admin/subiekt/tasks/[id]/retry/route.js +42 -0
  15. package/.medusa/server/src/api/admin/subiekt/tasks/route.js +42 -0
  16. package/.medusa/server/src/api/admin/subiekt/writers/route.js +25 -0
  17. package/.medusa/server/src/api/hooks/subiekt/route.js +67 -0
  18. package/.medusa/server/src/api/middlewares.js +17 -0
  19. package/.medusa/server/src/jobs/subiekt-process-tasks.js +24 -0
  20. package/.medusa/server/src/jobs/subiekt-pull-events.js +35 -0
  21. package/.medusa/server/src/jobs/subiekt-sync-products.js +26 -0
  22. package/.medusa/server/src/jobs/subiekt-sync-stock.js +26 -0
  23. package/.medusa/server/src/modules/subiekt/index.js +14 -0
  24. package/.medusa/server/src/modules/subiekt/lib/backoff.js +28 -0
  25. package/.medusa/server/src/modules/subiekt/lib/bridge-client.js +215 -0
  26. package/.medusa/server/src/modules/subiekt/lib/capabilities.js +60 -0
  27. package/.medusa/server/src/modules/subiekt/lib/constants.js +116 -0
  28. package/.medusa/server/src/modules/subiekt/lib/contract.js +10 -0
  29. package/.medusa/server/src/modules/subiekt/lib/demo.js +469 -0
  30. package/.medusa/server/src/modules/subiekt/lib/diagnostics.js +52 -0
  31. package/.medusa/server/src/modules/subiekt/lib/documents.js +59 -0
  32. package/.medusa/server/src/modules/subiekt/lib/dto.js +88 -0
  33. package/.medusa/server/src/modules/subiekt/lib/guide.js +79 -0
  34. package/.medusa/server/src/modules/subiekt/lib/nip.js +120 -0
  35. package/.medusa/server/src/modules/subiekt/lib/numbers.js +41 -0
  36. package/.medusa/server/src/modules/subiekt/lib/options.js +109 -0
  37. package/.medusa/server/src/modules/subiekt/lib/order-payload.js +319 -0
  38. package/.medusa/server/src/modules/subiekt/lib/payment.js +111 -0
  39. package/.medusa/server/src/modules/subiekt/lib/products.js +236 -0
  40. package/.medusa/server/src/modules/subiekt/lib/references.js +135 -0
  41. package/.medusa/server/src/modules/subiekt/lib/signature.js +94 -0
  42. package/.medusa/server/src/modules/subiekt/lib/stock.js +173 -0
  43. package/.medusa/server/src/modules/subiekt/lib/writers.js +85 -0
  44. package/.medusa/server/src/modules/subiekt/migrations/Migration20261004180000.js +112 -0
  45. package/.medusa/server/src/modules/subiekt/migrations/Migration20261006120000.js +95 -0
  46. package/.medusa/server/src/modules/subiekt/models/subiekt-catalog-change.js +40 -0
  47. package/.medusa/server/src/modules/subiekt/models/subiekt-catalog-quarantine.js +21 -0
  48. package/.medusa/server/src/modules/subiekt/models/subiekt-connection.js +29 -0
  49. package/.medusa/server/src/modules/subiekt/models/subiekt-document.js +35 -0
  50. package/.medusa/server/src/modules/subiekt/models/subiekt-sync-run.js +23 -0
  51. package/.medusa/server/src/modules/subiekt/models/subiekt-task.js +43 -0
  52. package/.medusa/server/src/modules/subiekt/models/subiekt-writer.js +21 -0
  53. package/.medusa/server/src/modules/subiekt/service.js +73 -0
  54. package/.medusa/server/src/subscribers/subiekt-fulfillment-created.js +52 -0
  55. package/.medusa/server/src/subscribers/subiekt-order-canceled.js +30 -0
  56. package/.medusa/server/src/subscribers/subiekt-order-placed.js +43 -0
  57. package/.medusa/server/src/subscribers/subiekt-payment-captured.js +41 -0
  58. package/.medusa/server/src/workflows/index.js +50 -0
  59. package/.medusa/server/src/workflows/subiekt/cancel-order-in-subiekt.js +23 -0
  60. package/.medusa/server/src/workflows/subiekt/catalog.js +355 -0
  61. package/.medusa/server/src/workflows/subiekt/check-subiekt-connection.js +14 -0
  62. package/.medusa/server/src/workflows/subiekt/create-subiekt-wz.js +22 -0
  63. package/.medusa/server/src/workflows/subiekt/events.js +131 -0
  64. package/.medusa/server/src/workflows/subiekt/health.js +81 -0
  65. package/.medusa/server/src/workflows/subiekt/issue-subiekt-document.js +25 -0
  66. package/.medusa/server/src/workflows/subiekt/orders.js +281 -0
  67. package/.medusa/server/src/workflows/subiekt/process-subiekt-tasks.js +14 -0
  68. package/.medusa/server/src/workflows/subiekt/pull-subiekt-events.js +14 -0
  69. package/.medusa/server/src/workflows/subiekt/queue.js +126 -0
  70. package/.medusa/server/src/workflows/subiekt/runtime.js +193 -0
  71. package/.medusa/server/src/workflows/subiekt/sales-documents.js +93 -0
  72. package/.medusa/server/src/workflows/subiekt/send-order-to-subiekt.js +70 -0
  73. package/.medusa/server/src/workflows/subiekt/stock-plan.js +103 -0
  74. package/.medusa/server/src/workflows/subiekt/sync-subiekt-products.js +88 -0
  75. package/.medusa/server/src/workflows/subiekt/sync-subiekt-stock.js +78 -0
  76. package/.medusa/server/src/workflows/subiekt/tasks.js +208 -0
  77. package/.medusa/server/src/workflows/subiekt/writers.js +46 -0
  78. package/CHANGELOG.md +38 -0
  79. package/LICENSE +21 -0
  80. package/README.md +282 -2
  81. package/contract/README.md +39 -0
  82. package/contract/examples/document-create.request.json +5 -0
  83. package/contract/examples/document-create.response.json +20 -0
  84. package/contract/examples/error.unmatched-lines.json +17 -0
  85. package/contract/examples/events.response.json +96 -0
  86. package/contract/examples/health.json +45 -0
  87. package/contract/examples/order-create-b2b.request.json +101 -0
  88. package/contract/examples/order-create.request.json +100 -0
  89. package/contract/examples/order-create.response.json +20 -0
  90. package/contract/examples/products.response.json +53 -0
  91. package/contract/examples/signature-vectors.json +38 -0
  92. package/contract/examples/stock.response.json +11 -0
  93. package/contract/openapi.yaml +912 -0
  94. package/package.json +97 -4
@@ -0,0 +1,3588 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+ const jsxRuntime = require("react/jsx-runtime");
6
+ const reactI18next = require("react-i18next");
7
+ const adminSdk = require("@medusajs/admin-sdk");
8
+ const ui = require("@medusajs/ui");
9
+ const reactQuery = require("@tanstack/react-query");
10
+ const reactRouterDom = require("react-router-dom");
11
+ const icons = require("@medusajs/icons");
12
+ const react = require("react");
13
+ function backendUrl() {
14
+ try {
15
+ if (typeof __BACKEND_URL__ !== "undefined" && __BACKEND_URL__) return String(__BACKEND_URL__).replace(/\/+$/, "");
16
+ } catch {
17
+ }
18
+ return "";
19
+ }
20
+ class SubiektRequestError extends Error {
21
+ constructor(status, message) {
22
+ super(message);
23
+ __publicField(this, "status");
24
+ this.name = "SubiektRequestError";
25
+ this.status = status;
26
+ }
27
+ }
28
+ async function subiektFetch(path, init) {
29
+ const hasBody = (init == null ? void 0 : init.body) !== void 0;
30
+ const res = await fetch(`${backendUrl()}${path}`, {
31
+ method: (init == null ? void 0 : init.method) ?? "GET",
32
+ credentials: "include",
33
+ headers: { Accept: "application/json", ...hasBody ? { "Content-Type": "application/json" } : {} },
34
+ body: hasBody ? JSON.stringify(init == null ? void 0 : init.body) : void 0
35
+ });
36
+ const text = await res.text();
37
+ let json = null;
38
+ try {
39
+ json = text ? JSON.parse(text) : null;
40
+ } catch {
41
+ json = null;
42
+ }
43
+ if (!res.ok) {
44
+ const message = json && typeof json === "object" && "message" in json ? String(json.message) : `HTTP ${res.status}`;
45
+ throw new SubiektRequestError(res.status, message);
46
+ }
47
+ return json;
48
+ }
49
+ function errorMessage(err) {
50
+ return err instanceof Error ? err.message : String(err);
51
+ }
52
+ const subiektKeys = {
53
+ all: ["subiekt"],
54
+ status: ["subiekt", "status"],
55
+ tasks: (filter, q, offset, limit) => ["subiekt", "tasks", filter, q, offset, limit],
56
+ documents: (kind, q, offset, limit) => ["subiekt", "documents", kind, q, offset, limit],
57
+ runs: ["subiekt", "runs"],
58
+ order: (id) => ["subiekt", "order", id],
59
+ products: (kind, status, q, offset, limit) => ["subiekt", "products", kind, status, q, offset, limit]
60
+ };
61
+ function useSubiektStatus(pollUntil) {
62
+ return reactQuery.useQuery({
63
+ queryKey: subiektKeys.status,
64
+ queryFn: () => subiektFetch("/admin/subiekt"),
65
+ refetchInterval: (query) => {
66
+ var _a;
67
+ const data = query.state.data;
68
+ if ((((_a = data == null ? void 0 : data.running) == null ? void 0 : _a.length) ?? 0) > 0 || Date.now() < pollUntil) return 3e3;
69
+ return 3e4;
70
+ }
71
+ });
72
+ }
73
+ function useSubiektTasks(filter, q, offset, limit, poll) {
74
+ const params = new URLSearchParams({ filter, limit: String(limit), offset: String(offset) });
75
+ if (q) params.set("q", q);
76
+ return reactQuery.useQuery({
77
+ queryKey: subiektKeys.tasks(filter, q, offset, limit),
78
+ queryFn: () => subiektFetch(`/admin/subiekt/tasks?${params.toString()}`),
79
+ refetchInterval: poll ? 3e3 : 3e4
80
+ });
81
+ }
82
+ function useSubiektDocuments(kind, q, offset, limit, poll) {
83
+ const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
84
+ if (kind && kind !== "all") params.set("kind", kind);
85
+ return reactQuery.useQuery({
86
+ queryKey: subiektKeys.documents(kind, q, offset, limit),
87
+ queryFn: () => subiektFetch(`/admin/subiekt/documents?${params.toString()}`),
88
+ refetchInterval: poll ? 3e3 : 3e4
89
+ });
90
+ }
91
+ function useSubiektRuns(poll) {
92
+ return reactQuery.useQuery({
93
+ queryKey: subiektKeys.runs,
94
+ queryFn: () => subiektFetch("/admin/subiekt/runs?limit=15"),
95
+ refetchInterval: poll ? 3e3 : 3e4
96
+ });
97
+ }
98
+ function useSubiektOrder(orderId) {
99
+ return reactQuery.useQuery({
100
+ queryKey: subiektKeys.order(orderId),
101
+ queryFn: () => subiektFetch(`/admin/subiekt/orders/${encodeURIComponent(orderId)}`),
102
+ refetchInterval: (query) => {
103
+ var _a;
104
+ const tasks = ((_a = query.state.data) == null ? void 0 : _a.tasks) ?? [];
105
+ return tasks.some((t) => t.status === "pending" || t.status === "running" || t.status === "unknown") ? 3e3 : 3e4;
106
+ }
107
+ });
108
+ }
109
+ function useSubiektProducts(kind, status, q, offset, limit, poll) {
110
+ const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
111
+ if (kind && kind !== "all") params.set("kind", kind);
112
+ if (status && status !== "all") params.set("status", status);
113
+ if (q) params.set("q", q);
114
+ return reactQuery.useQuery({
115
+ queryKey: subiektKeys.products(kind, status, q, offset, limit),
116
+ queryFn: () => subiektFetch(`/admin/subiekt/products?${params.toString()}`),
117
+ refetchInterval: poll ? 3e3 : 6e4
118
+ });
119
+ }
120
+ function useSubiektWriter() {
121
+ const client = reactQuery.useQueryClient();
122
+ return reactQuery.useMutation({
123
+ mutationFn: (body) => subiektFetch("/admin/subiekt/writers", { method: "POST", body }),
124
+ onSuccess: () => client.invalidateQueries({ queryKey: subiektKeys.all })
125
+ });
126
+ }
127
+ function useSubiektRelease() {
128
+ const client = reactQuery.useQueryClient();
129
+ return reactQuery.useMutation({
130
+ mutationFn: (id) => subiektFetch("/admin/subiekt/products/release", { method: "POST", body: { id } }),
131
+ onSuccess: () => client.invalidateQueries({ queryKey: subiektKeys.all })
132
+ });
133
+ }
134
+ function useSubiektIssueDocument(orderId) {
135
+ const client = reactQuery.useQueryClient();
136
+ return reactQuery.useMutation({
137
+ mutationFn: (kind) => subiektFetch(`/admin/subiekt/orders/${encodeURIComponent(orderId)}/documents`, { method: "POST", body: kind ? { kind } : {} }),
138
+ onSuccess: () => client.invalidateQueries({ queryKey: subiektKeys.order(orderId) })
139
+ });
140
+ }
141
+ function useSubiektSync() {
142
+ const client = reactQuery.useQueryClient();
143
+ return reactQuery.useMutation({
144
+ mutationFn: (what) => subiektFetch("/admin/subiekt/sync", { method: "POST", body: { what } }),
145
+ onSuccess: () => client.invalidateQueries({ queryKey: subiektKeys.status })
146
+ });
147
+ }
148
+ function useSubiektCheck() {
149
+ const client = reactQuery.useQueryClient();
150
+ return reactQuery.useMutation({
151
+ mutationFn: () => subiektFetch("/admin/subiekt/check", { method: "POST", body: {} }),
152
+ onSuccess: (data) => client.setQueryData(subiektKeys.status, data.status)
153
+ });
154
+ }
155
+ function useSubiektRetry() {
156
+ const client = reactQuery.useQueryClient();
157
+ return reactQuery.useMutation({
158
+ mutationFn: (id) => subiektFetch(`/admin/subiekt/tasks/${encodeURIComponent(id)}/retry`, { method: "POST", body: {} }),
159
+ onSuccess: () => client.invalidateQueries({ queryKey: subiektKeys.all })
160
+ });
161
+ }
162
+ function useSubiektSendOrder(orderId) {
163
+ const client = reactQuery.useQueryClient();
164
+ return reactQuery.useMutation({
165
+ mutationFn: () => subiektFetch(`/admin/subiekt/orders/${encodeURIComponent(orderId)}/send`, { method: "POST", body: {} }),
166
+ onSuccess: () => client.invalidateQueries({ queryKey: subiektKeys.order(orderId) })
167
+ });
168
+ }
169
+ function fmtDateTime(value, lang) {
170
+ if (!value) return "";
171
+ const d = new Date(value);
172
+ if (!Number.isFinite(d.getTime())) return "";
173
+ try {
174
+ return new Intl.DateTimeFormat(lang, { dateStyle: "medium", timeStyle: "short" }).format(d);
175
+ } catch {
176
+ return d.toISOString().slice(0, 16).replace("T", " ");
177
+ }
178
+ }
179
+ function fmtDuration(ms) {
180
+ if (!Number.isFinite(ms) || ms <= 0) return "0 s";
181
+ return ms < 1e4 ? `${(ms / 1e3).toFixed(1)} s` : `${Math.round(ms / 1e3)} s`;
182
+ }
183
+ function fmtNumber(value, lang) {
184
+ const n = typeof value === "number" ? value : Number(value);
185
+ if (!Number.isFinite(n)) return "0";
186
+ try {
187
+ return new Intl.NumberFormat(lang).format(n);
188
+ } catch {
189
+ return String(n);
190
+ }
191
+ }
192
+ const TASK_TONE = {
193
+ waiting: "purple",
194
+ pending: "blue",
195
+ running: "blue",
196
+ unknown: "orange",
197
+ succeeded: "green",
198
+ failed: "red",
199
+ canceled: "grey"
200
+ };
201
+ function TaskStatusBadge({ status }) {
202
+ const { t } = reactI18next.useTranslation("subiekt");
203
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: TASK_TONE[status] ?? "grey", children: t(`tasks.statuses.${status}`) });
204
+ }
205
+ const RUN_TONE = { success: "green", partial: "orange", error: "red", skipped: "grey" };
206
+ function RunStatusBadge({ status }) {
207
+ const { t } = reactI18next.useTranslation("subiekt");
208
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: RUN_TONE[status] ?? "grey", children: t(`runs.statuses.${status}`) });
209
+ }
210
+ function DocumentStatusBadge({ status }) {
211
+ const { t } = reactI18next.useTranslation("subiekt");
212
+ const tone = status === "canceled" ? "grey" : status === "completed" ? "green" : "blue";
213
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: tone, children: t(`documents.statuses.${status}`, { defaultValue: status }) });
214
+ }
215
+ const CATALOG_TONE = {
216
+ planned: "blue",
217
+ applied: "green",
218
+ simulated: "purple",
219
+ over_cap: "grey",
220
+ stale: "orange",
221
+ failed: "red",
222
+ quarantined: "red",
223
+ skipped: "grey"
224
+ };
225
+ function CatalogStatusBadge({ status }) {
226
+ const { t } = reactI18next.useTranslation("subiekt");
227
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: CATALOG_TONE[status] ?? "grey", children: t(`products.statuses.${status}`, { defaultValue: status }) });
228
+ }
229
+ function SampleList({ label, items }) {
230
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
231
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: label }),
232
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", className: "font-mono", children: item }, item)) })
233
+ ] });
234
+ }
235
+ function RichText({ text }) {
236
+ const parts = text.split("`");
237
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts.map(
238
+ (part, i) => i % 2 === 1 ? /* @__PURE__ */ jsxRuntime.jsx("code", { className: "txt-compact-xsmall rounded border border-ui-border-base bg-ui-bg-subtle px-1 font-mono text-ui-fg-base", children: part }, i) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: part }, i)
239
+ ) });
240
+ }
241
+ function connectionState(s) {
242
+ var _a;
243
+ if (!s) return { key: "unknown", tone: "grey" };
244
+ if (s.mode === "demo") return { key: "demo", tone: "purple" };
245
+ if (!s.configured) return { key: "notConfigured", tone: "orange" };
246
+ if (!s.connection.checkedAt && !s.connection.lastErrorAt) return { key: "unknown", tone: "grey" };
247
+ if (!s.connection.reachable) return { key: "unreachable", tone: "red" };
248
+ if (s.connection.health && !((_a = s.connection.health.subiekt) == null ? void 0 : _a.connected)) return { key: "subiektDown", tone: "orange" };
249
+ return { key: "connected", tone: "green" };
250
+ }
251
+ function ConnectionBadge({ status }) {
252
+ const { t } = reactI18next.useTranslation("subiekt");
253
+ const state = connectionState(status);
254
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: state.tone, children: t(`mode.${state.key}`) });
255
+ }
256
+ function StatTile({ label, value, tone }) {
257
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1 px-6 py-4", children: [
258
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle", children: label }),
259
+ /* @__PURE__ */ jsxRuntime.jsx(
260
+ ui.Text,
261
+ {
262
+ size: "xlarge",
263
+ weight: "plus",
264
+ leading: "compact",
265
+ className: ui.clx("tabular-nums", tone === "attention" ? "text-ui-tag-red-text" : tone === "muted" ? "text-ui-fg-muted" : "text-ui-fg-base"),
266
+ children: value
267
+ }
268
+ )
269
+ ] });
270
+ }
271
+ function Field({ label, children }) {
272
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[minmax(120px,1fr)_2fr] items-start gap-x-4 px-6 py-2", children: [
273
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: label }),
274
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "txt-compact-small text-ui-fg-base min-w-0 break-words", children })
275
+ ] });
276
+ }
277
+ function FilterPills({ value, options, onChange }) {
278
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center gap-2", children: options.map((o) => /* @__PURE__ */ jsxRuntime.jsxs(
279
+ "button",
280
+ {
281
+ type: "button",
282
+ onClick: () => onChange(o.value),
283
+ className: ui.clx(
284
+ "txt-compact-small-plus rounded-full border px-3 py-1 transition-colors",
285
+ value === o.value ? "border-ui-border-interactive bg-ui-bg-highlight text-ui-fg-interactive" : "border-ui-border-base text-ui-fg-subtle hover:bg-ui-bg-base-hover"
286
+ ),
287
+ children: [
288
+ o.label,
289
+ typeof o.count === "number" && o.count > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", className: "ml-2", children: o.count }) : null
290
+ ]
291
+ },
292
+ o.value
293
+ )) });
294
+ }
295
+ function Pager({ offset, limit, count, onChange }) {
296
+ const { t } = reactI18next.useTranslation("subiekt");
297
+ if (count <= limit) return null;
298
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-3", children: [
299
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle tabular-nums", children: t("tasks.page", { from: offset + 1, to: Math.min(offset + limit, count), count }) }),
300
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-x-2", children: [
301
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: offset === 0, onClick: () => onChange(Math.max(0, offset - limit)), children: t("tasks.prev") }),
302
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: offset + limit >= count, onClick: () => onChange(offset + limit), children: t("tasks.next") })
303
+ ] })
304
+ ] });
305
+ }
306
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
307
+ function runSummary(run, t) {
308
+ const s = run.stats ?? {};
309
+ if (run.status === "error" || run.status === "skipped" || !run.stats) return run.message ?? "";
310
+ if (run.kind === "stock") {
311
+ const changes = num(s.toUpdate) + num(s.toCreate);
312
+ return t(run.dryRun ? "runs.summary.stockDry" : "runs.summary.stock", { changes, unchanged: num(s.unchanged) });
313
+ }
314
+ if (run.kind === "tasks") {
315
+ const vars = { succeeded: num(s.succeeded), retry: num(s.retry), failed: num(s.failed) };
316
+ return t(vars.retry || vars.failed ? "runs.summary.tasksIssues" : "runs.summary.tasks", vars);
317
+ }
318
+ if (run.kind === "events") return num(s.read) > 0 ? t("runs.summary.events", { applied: num(s.applied) }) : t("runs.summary.eventsNone");
319
+ if (run.kind === "health" && run.status === "success") return t("runs.summary.health");
320
+ if (run.kind === "products") {
321
+ const applied = num(s.applied) + num(s.simulated);
322
+ return t(run.dryRun ? "runs.summary.productsPlan" : "runs.summary.productsApplied", {
323
+ prices: num(s.priceChanges),
324
+ creates: num(s.toCreate),
325
+ applied,
326
+ failed: num(s.failed)
327
+ });
328
+ }
329
+ return run.message ?? "";
330
+ }
331
+ function fmtMoney(value, currency, lang) {
332
+ if (typeof value !== "number" || !Number.isFinite(value)) return "-";
333
+ try {
334
+ return new Intl.NumberFormat(lang, { style: "currency", currency: currency.toUpperCase() }).format(value);
335
+ } catch {
336
+ return `${value.toFixed(2)} ${currency.toUpperCase()}`;
337
+ }
338
+ }
339
+ function fmtMs(ms) {
340
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return "-";
341
+ const abs = Math.abs(ms);
342
+ return abs < 1e3 ? `${Math.round(ms)} ms` : `${(ms / 1e3).toFixed(abs < 1e4 ? 1 : 0)} s`;
343
+ }
344
+ function resolveText(value, lang) {
345
+ if (!value) return void 0;
346
+ if (typeof value === "string") return value;
347
+ const pl2 = lang.toLowerCase().startsWith("pl");
348
+ return (pl2 ? value.pl ?? value.en : value.en ?? value.pl) || void 0;
349
+ }
350
+ const PL_GENITIVE = ["stycznia", "lutego", "marca", "kwietnia", "maja", "czerwca", "lipca", "sierpnia", "września", "października", "listopada", "grudnia"];
351
+ function monthYear(since, lang) {
352
+ const m = /^(\d{4})-(\d{2})$/.exec(since);
353
+ if (!m) return since;
354
+ const year = Number(m[1]);
355
+ const month = Number(m[2]);
356
+ if (lang.toLowerCase().startsWith("pl")) return `${PL_GENITIVE[month - 1] ?? m[2]} ${year}`;
357
+ try {
358
+ return new Intl.DateTimeFormat(lang, { month: "long", year: "numeric", timeZone: "UTC" }).format(new Date(Date.UTC(year, month - 1, 15)));
359
+ } catch {
360
+ return since;
361
+ }
362
+ }
363
+ function referencesFor(list, lang) {
364
+ return (list ?? []).map((r) => ({
365
+ name: r.name,
366
+ url: r.url,
367
+ icon: r.icon,
368
+ description: resolveText(r.description, lang),
369
+ since: r.since,
370
+ metrics: r.metrics.map((m) => ({ label: resolveText(m.label, lang) ?? "", value: m.value })).filter((m) => m.label),
371
+ links: r.links.map((l) => ({ label: resolveText(l.label, lang) ?? l.url, url: l.url })),
372
+ review: r.review ? { ...r.review, quote: resolveText(r.review.quote ?? void 0, lang) } : null
373
+ }));
374
+ }
375
+ function fmtRating(value, lang) {
376
+ try {
377
+ return new Intl.NumberFormat(lang, { minimumFractionDigits: 1, maximumFractionDigits: 1 }).format(value);
378
+ } catch {
379
+ return value.toFixed(1);
380
+ }
381
+ }
382
+ function MedusaMark({ className }) {
383
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 36 38", fill: "none", "aria-hidden": true, className, children: /* @__PURE__ */ jsxRuntime.jsx(
384
+ "path",
385
+ {
386
+ d: "M30.85 6.16832L22.2453 1.21782C19.4299 -0.405941 15.9801 -0.405941 13.1648 1.21782L4.52043 6.16832C1.74473 7.79208 0 10.802 0 14.0099V23.9505C0 27.198 1.74473 30.1683 4.52043 31.7921L13.1251 36.7822C15.9405 38.4059 19.3903 38.4059 22.2056 36.7822L30.8103 31.7921C33.6257 30.1683 35.3307 27.198 35.3307 23.9505V14.0099C35.41 10.802 33.6653 7.79208 30.85 6.16832ZM17.6852 27.8317C12.8079 27.8317 8.8426 23.8713 8.8426 19C8.8426 14.1287 12.8079 10.1683 17.6852 10.1683C22.5625 10.1683 26.5674 14.1287 26.5674 19C26.5674 23.8713 22.6022 27.8317 17.6852 27.8317Z",
387
+ fill: "currentColor"
388
+ }
389
+ ) });
390
+ }
391
+ function StoreColumn({ label, hint }) {
392
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-x-1.5", title: hint, children: [
393
+ /* @__PURE__ */ jsxRuntime.jsx(MedusaMark, { className: "h-3.5 w-3.5 text-ui-fg-muted" }),
394
+ label
395
+ ] });
396
+ }
397
+ function StoreLink({ to, title, mono, monoTitle, action }) {
398
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactRouterDom.Link, { to, className: "group flex items-center gap-x-2.5", title: action, children: [
399
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-ui-bg-component shadow-borders-base transition-fg group-hover:bg-ui-bg-component-hover", children: /* @__PURE__ */ jsxRuntime.jsx(MedusaMark, { className: "h-4 w-4 text-ui-fg-base" }) }),
400
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col", children: [
401
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small-plus truncate text-ui-fg-base group-hover:text-ui-fg-interactive", children: title }),
402
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-x-1.5", children: [
403
+ mono ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-mono text-ui-fg-muted txt-compact-xsmall", title: monoTitle, children: mono }) : null,
404
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "txt-compact-xsmall-plus inline-flex shrink-0 items-center gap-x-0.5 text-ui-fg-interactive", children: [
405
+ action,
406
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightMini, {})
407
+ ] })
408
+ ] })
409
+ ] })
410
+ ] });
411
+ }
412
+ function StoreUnlinked({ label, hint, mono }) {
413
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start gap-y-1", title: hint, children: [
414
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "orange", children: label }),
415
+ mono ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-ui-fg-muted txt-compact-xsmall", children: mono }) : null
416
+ ] });
417
+ }
418
+ function StoreOrderCell({ orderId, displayId }) {
419
+ const { t } = reactI18next.useTranslation("subiekt");
420
+ if (!orderId) return /* @__PURE__ */ jsxRuntime.jsx(StoreUnlinked, { label: t("store.noOrder"), hint: t("store.noOrderHint") });
421
+ return /* @__PURE__ */ jsxRuntime.jsx(
422
+ StoreLink,
423
+ {
424
+ to: `/orders/${orderId}`,
425
+ title: displayId ? `#${displayId}` : orderId,
426
+ mono: displayId ? orderId : null,
427
+ monoTitle: orderId,
428
+ action: t("store.openOrder")
429
+ }
430
+ );
431
+ }
432
+ function SubiektIcon({ width = 15, height = 15, ...props }) {
433
+ return /* @__PURE__ */ jsxRuntime.jsxs(
434
+ "svg",
435
+ {
436
+ width,
437
+ height,
438
+ viewBox: "0 0 192 192",
439
+ fill: "none",
440
+ xmlns: "http://www.w3.org/2000/svg",
441
+ "aria-hidden": "true",
442
+ ...props,
443
+ children: [
444
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "192", height: "192", rx: "40", fill: "#0071B8" }),
445
+ /* @__PURE__ */ jsxRuntime.jsx(
446
+ "path",
447
+ {
448
+ fill: "#FFFFFF",
449
+ d: "M38.4 134.71L38.4 123.08Q40.4 124.84 43.19 126.25 45.98 127.66 49.06 128.63 52.15 129.6 55.26 130.13 58.37 130.66 61.02 130.66 70.12 130.66 74.62 127.28 79.11 123.9 79.11 117.56 79.11 114.15 77.61 111.63 76.12 109.1 73.47 107.02 70.83 104.93 67.21 103.02 63.6 101.11 59.43 99 55.03 96.76 51.21 94.47 47.39 92.18 44.57 89.42 41.75 86.66 40.13 83.16 38.52 79.67 38.52 74.97 38.52 69.21 41.04 64.95 43.57 60.69 47.68 57.93 51.79 55.17 57.05 53.82 62.31 52.47 67.77 52.47 80.23 52.47 85.93 55.47L85.93 66.57Q78.46 61.4 66.77 61.4 63.54 61.4 60.31 62.07 57.08 62.75 54.56 64.28 52.03 65.8 50.44 68.21 48.86 70.62 48.86 74.09 48.86 77.32 50.06 79.67 51.27 82.02 53.62 83.96 55.97 85.9 59.34 87.72 62.72 89.54 67.13 91.71 71.65 93.94 75.7 96.41 79.76 98.88 82.81 101.87 85.87 104.87 87.66 108.51 89.45 112.15 89.45 116.85 89.45 123.08 87.01 127.4 84.57 131.72 80.43 134.42 76.29 137.12 70.89 138.33 65.48 139.53 59.49 139.53 57.49 139.53 54.56 139.21 51.62 138.88 48.56 138.27 45.51 137.65 42.78 136.74 40.04 135.83 38.4 134.71M153.6 77.96L153.6 138.12 143.97 138.12 143.97 128.6 143.73 128.6Q137.74 139.53 125.17 139.53 103.67 139.53 103.67 113.92L103.67 77.96 113.24 77.96 113.24 112.39Q113.24 131.42 127.81 131.42 134.86 131.42 139.41 126.22 143.97 121.03 143.97 112.63L143.97 77.96"
450
+ }
451
+ )
452
+ ]
453
+ }
454
+ );
455
+ }
456
+ const SubiektOrderWidget = ({ data }) => {
457
+ var _a, _b, _c, _d;
458
+ const { t, i18n } = reactI18next.useTranslation("subiekt");
459
+ const lang = i18n.language || "en";
460
+ const q = useSubiektOrder(data.id);
461
+ const send = useSubiektSendOrder(data.id);
462
+ const issue = useSubiektIssueDocument(data.id);
463
+ const retry = useSubiektRetry();
464
+ const documents = ((_a = q.data) == null ? void 0 : _a.documents) ?? [];
465
+ const tasks = ((_b = q.data) == null ? void 0 : _b.tasks) ?? [];
466
+ const sales = (_c = q.data) == null ? void 0 : _c.salesDocument;
467
+ const create = tasks.find((task) => task.kind === "order.create");
468
+ const documentTask = tasks.find((task) => task.kind === "order.document");
469
+ const hasZk = documents.some((d) => d.kind === "ZK" && d.status !== "canceled");
470
+ const hasSales = documents.some((d) => (d.kind === "FS" || d.kind === "PA") && d.status !== "canceled");
471
+ const busy = (create == null ? void 0 : create.status) === "pending" || (create == null ? void 0 : create.status) === "running";
472
+ const documentBusy = documentTask ? ["pending", "running", "unknown"].includes(documentTask.status) : false;
473
+ const canceled = data.status === "canceled";
474
+ const offerIssue = Boolean(sales && sales.option !== "none" && sales.supported && hasZk && !hasSales && !documentBusy && !canceled);
475
+ const onSend = async () => {
476
+ try {
477
+ await send.mutateAsync();
478
+ ui.toast.success(t("widget.sent"));
479
+ } catch (err) {
480
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
481
+ }
482
+ };
483
+ const onIssue = async (kind) => {
484
+ try {
485
+ await issue.mutateAsync(kind);
486
+ ui.toast.success(t("widget.issued"));
487
+ } catch (err) {
488
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
489
+ }
490
+ };
491
+ const onRetry = async (id) => {
492
+ try {
493
+ await retry.mutateAsync(id);
494
+ ui.toast.success(t("toast.retried"));
495
+ void q.refetch();
496
+ } catch (err) {
497
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
498
+ }
499
+ };
500
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
501
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
502
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
503
+ /* @__PURE__ */ jsxRuntime.jsx(SubiektIcon, { width: 18, height: 18, className: "shrink-0" }),
504
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: t("widget.title") }),
505
+ ((_d = q.data) == null ? void 0 : _d.mode) === "demo" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "purple", children: t("widget.demo") }) : null
506
+ ] }),
507
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: "/app/subiekt", className: "txt-compact-small text-ui-fg-interactive hover:text-ui-fg-interactive-hover", children: t("widget.more") })
508
+ ] }),
509
+ q.isLoading ? null : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
510
+ documents.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-y-2 px-6 py-4", children: documents.map((d) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-x-2", children: [
511
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col", children: [
512
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "font-mono", children: d.number }),
513
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
514
+ fmtDateTime(d.issuedAt, lang),
515
+ ", ",
516
+ t(`documents.sources.${d.source}`, { defaultValue: d.source })
517
+ ] }),
518
+ d.kind === "FS" && d.status !== "canceled" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: d.ksefNumber ? "break-all font-mono text-ui-fg-subtle" : "text-ui-fg-muted", children: d.ksefNumber ? t("widget.ksef", { number: d.ksefNumber }) : t("widget.ksefWaiting") }) : null
519
+ ] }),
520
+ /* @__PURE__ */ jsxRuntime.jsx(DocumentStatusBadge, { status: d.status })
521
+ ] }, d.id)) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: (create == null ? void 0 : create.status) === "waiting" ? t("widget.waiting") : t("widget.none") }) }),
522
+ tasks.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: tasks.map((task) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
523
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-x-2", children: [
524
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: task.kind === "order.document" && (task.documentKind === "fs" || task.documentKind === "pa") ? t(`tasks.documentKinds.${task.documentKind}`) : t(`tasks.kinds.${task.kind}`) }),
525
+ /* @__PURE__ */ jsxRuntime.jsx(TaskStatusBadge, { status: task.status })
526
+ ] }),
527
+ task.buyer ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: task.buyer.source === "retail" ? t("tasks.buyer.retail") : t(`tasks.buyer.${task.buyer.source}`, { name: task.buyer.name ?? task.buyer.symbol ?? "?", nip: task.buyer.nip ?? "?" }) }) : null,
528
+ task.warnings.map((w) => /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-tag-orange-text", children: w }, w)),
529
+ task.lastError && task.status !== "succeeded" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: task.status === "failed" ? "text-ui-tag-red-text" : "text-ui-fg-muted", children: task.lastError }) : null,
530
+ task.status === "unknown" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-tag-orange-text", children: t("tasks.unknownHint") }) : null,
531
+ task.manualAction ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-tag-orange-text", children: t("tasks.manual") }) : null,
532
+ task.status === "failed" ? /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: retry.isPending && retry.variables === task.id, onClick: () => onRetry(task.id), children: t("actions.retry") }) }) : null
533
+ ] }, task.id)) }) : null,
534
+ !hasZk && !busy && !canceled && (create == null ? void 0 : create.status) !== "failed" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: send.isPending, onClick: onSend, children: t("widget.send") }) }) : null,
535
+ offerIssue && sales ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 py-4", children: [
536
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: sales.writerActive ? t("widget.autoOn", { after: sales.after.toUpperCase() }) : t("widget.autoOff") }),
537
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
538
+ sales.option === "fs" || sales.option === "auto" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: issue.isPending && issue.variables === "fs", onClick: () => onIssue("fs"), children: t("widget.issueFs") }) : null,
539
+ sales.option === "pa" || sales.option === "auto" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: issue.isPending && issue.variables === "pa", onClick: () => onIssue("pa"), children: t("widget.issuePa") }) : null
540
+ ] })
541
+ ] }) : null
542
+ ] })
543
+ ] });
544
+ };
545
+ adminSdk.defineWidgetConfig({
546
+ zone: "order.details.side.after"
547
+ });
548
+ const NBSP = " ";
549
+ const NBHY = "‑";
550
+ const SHORT = /(^|[\s("„])(oraz|albo|lub|ale|że|bo|czy|gdy|aby|by|więc|jak|na|do|za|ze|we|od|po|to|[aiouwze])[ \t]+/gi;
551
+ function nb(text) {
552
+ let out = text;
553
+ for (let k = 0; k < 2; k++) out = out.replace(SHORT, `$1$2${NBSP}`);
554
+ out = out.replace(/(\d) (?=\d{3}\b)/g, `$1${NBSP}`);
555
+ out = out.replace(/\b([eE])-(?=mail)/g, `$1${NBHY}`);
556
+ return out.replace(/(\d) (zł|€|Kč|EUR|USD|PLN|CZK|mln|tys\.|cm|mm|m²|%)/g, `$1${NBSP}$2`);
557
+ }
558
+ function typeset(value) {
559
+ if (typeof value === "string") return nb(value);
560
+ if (Array.isArray(value)) return value.map((v) => typeset(v));
561
+ if (value && typeof value === "object") {
562
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, typeset(v)]));
563
+ }
564
+ return value;
565
+ }
566
+ function usePageNav(tabs) {
567
+ const [params, setParams] = reactRouterDom.useSearchParams();
568
+ const raw = params.get("view");
569
+ const view = raw === "guide" || raw === "settings" ? raw : "panel";
570
+ const asked = params.get("tab") ?? "";
571
+ const tab = tabs.includes(asked) ? asked : tabs[0];
572
+ const go = (next, nextTab) => {
573
+ const p = new URLSearchParams(params);
574
+ if (next === "panel") p.delete("view");
575
+ else p.set("view", next);
576
+ if (next === "settings") p.set("tab", nextTab ?? tab);
577
+ else p.delete("tab");
578
+ setParams(p, { replace: true });
579
+ };
580
+ return { view, tab, go };
581
+ }
582
+ const VIEWS = ["panel", "guide", "settings"];
583
+ const VIEW_ICON = { panel: /* @__PURE__ */ jsxRuntime.jsx(icons.ChartBar, {}), guide: /* @__PURE__ */ jsxRuntime.jsx(icons.BookOpen, {}), settings: /* @__PURE__ */ jsxRuntime.jsx(icons.CogSixTooth, {}) };
584
+ function PageTabs({ value, onChange, labels }) {
585
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tablist", className: "flex flex-wrap items-center gap-1", children: VIEWS.map((v) => {
586
+ const on = v === value;
587
+ return /* @__PURE__ */ jsxRuntime.jsxs(
588
+ "button",
589
+ {
590
+ type: "button",
591
+ role: "tab",
592
+ "aria-selected": on,
593
+ onClick: () => onChange(v),
594
+ className: ui.clx(
595
+ "txt-compact-small-plus inline-flex items-center gap-x-1.5 rounded-md px-2.5 py-1.5 outline-none transition-fg focus-visible:shadow-borders-focus",
596
+ on ? "bg-ui-bg-base text-ui-fg-base shadow-borders-base" : "text-ui-fg-subtle hover:bg-ui-bg-base-hover hover:text-ui-fg-base"
597
+ ),
598
+ children: [
599
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.clx("flex", on ? "text-ui-fg-interactive" : "text-ui-fg-muted"), children: VIEW_ICON[v] }),
600
+ labels[v]
601
+ ]
602
+ },
603
+ v
604
+ );
605
+ }) });
606
+ }
607
+ function OtherActions({ items, label }) {
608
+ if (items.length === 1) {
609
+ const a = items[0];
610
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "secondary", isLoading: a.loading, disabled: a.disabled, onClick: a.onClick, children: [
611
+ a.icon,
612
+ a.label
613
+ ] });
614
+ }
615
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu, { children: [
616
+ /* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "secondary", children: [
617
+ label,
618
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ChevronDownMini, {})
619
+ ] }) }),
620
+ /* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Content, { align: "end", children: items.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu.Item, { disabled: a.disabled || a.loading, onClick: a.onClick, className: "gap-x-2", children: [
621
+ a.icon,
622
+ a.label
623
+ ] }, a.key)) })
624
+ ] });
625
+ }
626
+ function IntegrationHeader({
627
+ icon,
628
+ title,
629
+ by,
630
+ badges,
631
+ description,
632
+ social,
633
+ help,
634
+ view,
635
+ onView,
636
+ labels,
637
+ primary,
638
+ actions = []
639
+ }) {
640
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
641
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 pb-5 pt-5", children: [
642
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between", children: [
643
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2.5 gap-y-1.5", children: [
644
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0", children: icon }),
645
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: title }),
646
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: by }),
647
+ badges ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex flex-wrap items-center gap-1.5", children: badges }) : null
648
+ ] }),
649
+ help ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0", children: help }) : null
650
+ ] }),
651
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "max-w-3xl text-ui-fg-subtle", children: description }),
652
+ social ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center gap-2 pt-0.5", children: social }) : null
653
+ ] }),
654
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 border-t border-ui-border-base bg-ui-bg-subtle px-6 py-2.5 md:flex-row md:items-center md:justify-between", children: [
655
+ /* @__PURE__ */ jsxRuntime.jsx(PageTabs, { value: view, onChange: onView, labels }),
656
+ primary || actions.length ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
657
+ actions.length ? /* @__PURE__ */ jsxRuntime.jsx(OtherActions, { items: actions, label: labels.more }) : null,
658
+ primary ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "primary", isLoading: primary.loading, disabled: primary.disabled, onClick: primary.onClick, children: [
659
+ primary.icon,
660
+ primary.label
661
+ ] }) : null
662
+ ] }) : null
663
+ ] })
664
+ ] });
665
+ }
666
+ function ModeBadge({ color, label, title, children }) {
667
+ if (!children) return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color, children: label });
668
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Popover, { children: [
669
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "inline-flex rounded-full outline-none transition-fg hover:opacity-80 focus-visible:shadow-borders-focus", "aria-label": title ?? label, children: /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color, className: "cursor-pointer", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-x-1", children: [
670
+ label,
671
+ /* @__PURE__ */ jsxRuntime.jsx(icons.InformationCircleSolid, { className: "h-3 w-3 opacity-70" })
672
+ ] }) }) }) }),
673
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Popover.Content, { align: "start", sideOffset: 8, className: "w-[min(440px,calc(100vw-32px))] p-0", children: [
674
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-ui-border-base px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: title ?? label }) }),
675
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "txt-small flex max-h-[60vh] flex-col gap-y-3 overflow-y-auto px-4 py-3 text-ui-fg-subtle", children })
676
+ ] })
677
+ ] });
678
+ }
679
+ const host = (url) => {
680
+ try {
681
+ return new URL(url).host.replace(/^www\./, "");
682
+ } catch {
683
+ return url;
684
+ }
685
+ };
686
+ const STAR = "M10 1.5l2.6 5.3 5.9.9-4.25 4.1 1 5.85L10 14.9l-5.25 2.75 1-5.85L1.5 7.7l5.9-.9L10 1.5z";
687
+ function Stars({ rating, scale = 5, size = 12 }) {
688
+ const fill = Math.max(0, Math.min(1, rating / (scale || 5))) * 100;
689
+ const row = (color) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex gap-x-px", style: { color }, children: [0, 1, 2, 3, 4].map((i) => /* @__PURE__ */ jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 20 20", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: STAR, fill: "currentColor" }) }, i)) });
690
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative inline-flex shrink-0", "aria-hidden": true, children: [
691
+ row("var(--fg-disabled)"),
692
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inset-y-0 left-0 overflow-hidden", style: { width: `${fill}%` }, children: row("#F5A524") })
693
+ ] });
694
+ }
695
+ function SourceMark({ review, height = 10 }) {
696
+ if (!review.icon) {
697
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", weight: "plus", className: "text-ui-fg-subtle", children: review.source });
698
+ }
699
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0 items-center rounded-full bg-white px-2 py-1 shadow-borders-base", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: review.icon, alt: review.source, style: { height, width: "auto" } }) });
700
+ }
701
+ function StoreIcon({ reference, size }) {
702
+ const box = { width: size, height: size };
703
+ return reference.icon ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: reference.icon, alt: "", width: size, height: size, style: box, className: "shrink-0 rounded-md bg-white object-contain shadow-borders-base" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: box, className: "txt-compact-xsmall-plus flex shrink-0 items-center justify-center rounded-md bg-ui-bg-base text-ui-fg-base shadow-borders-base", children: reference.name.slice(0, 1).toUpperCase() });
704
+ }
705
+ function meanRating(items) {
706
+ const reviews = items.map((r) => r.review).filter((r) => Boolean(r));
707
+ if (reviews.length === 0) return null;
708
+ const value = reviews.reduce((sum, r) => sum + r.rating / (r.scale || 5) * 5, 0) / reviews.length;
709
+ return { value: Math.round(value * 10) / 10, first: reviews[0] };
710
+ }
711
+ function ReferencesBadge({ items, labels }) {
712
+ if (items.length === 0) return null;
713
+ const mean = meanRating(items);
714
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Popover, { children: [
715
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
716
+ "button",
717
+ {
718
+ type: "button",
719
+ className: "inline-flex h-9 max-w-full items-center gap-x-3 rounded-full border border-ui-border-base bg-ui-bg-component py-1 pl-1.5 pr-3 outline-none transition-fg hover:bg-ui-bg-component-hover focus-visible:shadow-borders-focus",
720
+ children: [
721
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center", children: items.slice(0, 3).map((r, i) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.clx("rounded-md", i > 0 && "-ml-2"), children: /* @__PURE__ */ jsxRuntime.jsx(StoreIcon, { reference: r, size: 24 }) }, r.url)) }),
722
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small-plus truncate text-ui-fg-base", children: labels.count }),
723
+ mean ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-x-2 border-l border-ui-border-base pl-3", children: [
724
+ /* @__PURE__ */ jsxRuntime.jsx(Stars, { rating: mean.value, size: 13 }),
725
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-small-plus tabular-nums text-ui-fg-base", children: labels.rating(mean.value) }),
726
+ /* @__PURE__ */ jsxRuntime.jsx(SourceMark, { review: mean.first, height: 10 })
727
+ ] }) : null,
728
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ChevronDownMini, { className: "-ml-1 shrink-0 text-ui-fg-muted" })
729
+ ]
730
+ }
731
+ ) }),
732
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Popover.Content, { align: "start", sideOffset: 8, className: "w-[min(460px,calc(100vw-32px))] p-0", children: [
733
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-0.5 border-b border-ui-border-base px-4 py-3", children: [
734
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: labels.title }),
735
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: labels.subtitle })
736
+ ] }),
737
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex max-h-[60vh] flex-col divide-y divide-ui-border-base overflow-y-auto", children: items.map((r) => {
738
+ var _a, _b;
739
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-col gap-y-2 px-4 py-3", children: [
740
+ /* @__PURE__ */ jsxRuntime.jsxs("a", { href: r.url, target: "_blank", rel: "noreferrer", className: "group flex items-center gap-x-3", children: [
741
+ /* @__PURE__ */ jsxRuntime.jsx(StoreIcon, { reference: r, size: 32 }),
742
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col", children: [
743
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "truncate text-ui-fg-base", children: r.name }),
744
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "truncate text-ui-fg-interactive group-hover:text-ui-fg-interactive-hover", children: host(r.url) })
745
+ ] }),
746
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "ml-auto shrink-0 text-ui-fg-muted group-hover:text-ui-fg-interactive" })
747
+ ] }),
748
+ r.review ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [
749
+ /* @__PURE__ */ jsxRuntime.jsx(Stars, { rating: r.review.rating, scale: r.review.scale }),
750
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus tabular-nums text-ui-fg-base", children: labels.rating(r.review.rating / (r.review.scale || 5) * 5) }),
751
+ /* @__PURE__ */ jsxRuntime.jsx(SourceMark, { review: r.review }),
752
+ r.review.url ? /* @__PURE__ */ jsxRuntime.jsxs("a", { href: r.review.url, target: "_blank", rel: "noreferrer", className: "txt-compact-xsmall-plus inline-flex items-center gap-x-0.5 text-ui-fg-interactive hover:text-ui-fg-interactive-hover", children: [
753
+ labels.review,
754
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "h-3.5 w-3.5" })
755
+ ] }) : null
756
+ ] }) : null,
757
+ ((_a = r.review) == null ? void 0 : _a.quote) ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "border-l-2 border-ui-border-strong pl-2 italic text-ui-fg-subtle", children: [
758
+ nb(r.review.quote),
759
+ r.review.author ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "not-italic text-ui-fg-muted", children: [
760
+ " ",
761
+ r.review.author
762
+ ] }) : null
763
+ ] }) : null,
764
+ r.description ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: nb(r.description) }) : null,
765
+ ((_b = r.metrics) == null ? void 0 : _b.length) || r.since ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
766
+ (r.metrics ?? []).map((m) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "green", children: [
767
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: m.value }),
768
+ " ",
769
+ m.label
770
+ ] }, m.label)),
771
+ r.since ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: labels.since(r.since) }) : null
772
+ ] }) : null,
773
+ (r.links ?? []).length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-x-3 gap-y-1", children: (r.links ?? []).map((l) => /* @__PURE__ */ jsxRuntime.jsxs("a", { href: l.url, target: "_blank", rel: "noreferrer", className: "txt-compact-xsmall-plus inline-flex items-center gap-x-0.5 text-ui-fg-interactive hover:text-ui-fg-interactive-hover", children: [
774
+ l.label,
775
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "h-3.5 w-3.5" })
776
+ ] }, l.url)) }) : null
777
+ ] }, r.url);
778
+ }) })
779
+ ] })
780
+ ] });
781
+ }
782
+ const KODA_DISCORD = "https://discord.gg/WzUUKYzu8";
783
+ const KODA_EMAIL = "hello@koda.plus";
784
+ const KODA_DEMO = "https://medusa.koda.plus";
785
+ const plain = (text) => text.split(NBSP).join(" ");
786
+ function communityLabels(t, integration) {
787
+ const k = (key, options) => t(`community.${key}`, options);
788
+ return {
789
+ addStore: {
790
+ button: k("addStore.button"),
791
+ title: k("addStore.title"),
792
+ text: k("addStore.text"),
793
+ name: k("addStore.name"),
794
+ url: k("addStore.url"),
795
+ note: k("addStore.note"),
796
+ send: k("addStore.send"),
797
+ copy: k("addStore.copy"),
798
+ copied: k("addStore.copied"),
799
+ hint: k("addStore.hint", { email: KODA_EMAIL }),
800
+ subject: k("addStore.subject", { integration }),
801
+ greeting: k("addStore.greeting", { integration })
802
+ },
803
+ prompt: {
804
+ button: k("prompt.button"),
805
+ title: k("prompt.title"),
806
+ text: k("prompt.text"),
807
+ copy: k("prompt.copy"),
808
+ copied: k("prompt.copied"),
809
+ failed: k("prompt.failed")
810
+ },
811
+ discord: { button: k("discord.button"), title: k("discord.title") }
812
+ };
813
+ }
814
+ function AddStoreButton({ labels }) {
815
+ const [name, setName] = react.useState("");
816
+ const [url, setUrl] = react.useState("");
817
+ const [note, setNote] = react.useState("");
818
+ const ready = name.trim() !== "" && url.trim() !== "";
819
+ const subject = plain(labels.subject);
820
+ const body = plain(
821
+ [labels.greeting, "", `${labels.name}: ${name.trim()}`, `${labels.url}: ${url.trim()}`, ...note.trim() ? ["", note.trim()] : []].join("\n")
822
+ );
823
+ const send = () => {
824
+ window.location.href = `mailto:${KODA_EMAIL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
825
+ };
826
+ const copy = async () => {
827
+ try {
828
+ await navigator.clipboard.writeText(`${KODA_EMAIL}
829
+ ${subject}
830
+
831
+ ${body}`);
832
+ ui.toast.success(labels.copied);
833
+ } catch {
834
+ ui.toast.error(KODA_EMAIL);
835
+ }
836
+ };
837
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Popover, { children: [
838
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
839
+ "button",
840
+ {
841
+ type: "button",
842
+ className: "txt-compact-small-plus inline-flex h-9 items-center gap-x-1.5 rounded-full px-3 text-ui-fg-subtle outline-none transition-fg hover:bg-ui-bg-base-hover hover:text-ui-fg-base focus-visible:shadow-borders-focus",
843
+ children: [
844
+ /* @__PURE__ */ jsxRuntime.jsx(icons.PlusMini, {}),
845
+ labels.button
846
+ ]
847
+ }
848
+ ) }),
849
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Popover.Content, { align: "start", sideOffset: 8, className: "w-[min(420px,calc(100vw-32px))] p-0", children: [
850
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1 border-b border-ui-border-base px-4 py-3", children: [
851
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: labels.title }),
852
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: labels.text })
853
+ ] }),
854
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-4 py-4", children: [
855
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1.5", children: [
856
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: "xsmall", weight: "plus", htmlFor: "koda-add-store-name", children: labels.name }),
857
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "koda-add-store-name", size: "small", value: name, onChange: (e) => setName(e.target.value) })
858
+ ] }),
859
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1.5", children: [
860
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: "xsmall", weight: "plus", htmlFor: "koda-add-store-url", children: labels.url }),
861
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "koda-add-store-url", size: "small", type: "url", placeholder: "https://", value: url, onChange: (e) => setUrl(e.target.value) })
862
+ ] }),
863
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1.5", children: [
864
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: "xsmall", weight: "plus", htmlFor: "koda-add-store-note", children: labels.note }),
865
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Textarea, { id: "koda-add-store-note", rows: 3, value: note, onChange: (e) => setNote(e.target.value) })
866
+ ] })
867
+ ] }),
868
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 border-t border-ui-border-base px-4 py-3", children: [
869
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: labels.hint }),
870
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-end gap-2", children: [
871
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "secondary", disabled: !ready, onClick: () => void copy(), children: [
872
+ /* @__PURE__ */ jsxRuntime.jsx(icons.SquareTwoStack, {}),
873
+ labels.copy
874
+ ] }),
875
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "primary", disabled: !ready, onClick: send, children: [
876
+ /* @__PURE__ */ jsxRuntime.jsx(icons.PaperPlane, {}),
877
+ labels.send
878
+ ] })
879
+ ] })
880
+ ] })
881
+ ] })
882
+ ] });
883
+ }
884
+ function envNames(code) {
885
+ const names = /* @__PURE__ */ new Set();
886
+ for (const m of code.matchAll(/process\.env\.([A-Z][A-Z0-9_]*)/g)) names.add(m[1]);
887
+ for (const m of code.matchAll(/^([A-Z][A-Z0-9_]*)=/gm)) names.add(m[1]);
888
+ return [...names];
889
+ }
890
+ function buildSetupPrompt(spec, lang) {
891
+ const code = (text) => "`" + text + "`";
892
+ const env = envNames(`${spec.config}
893
+ ${spec.demo}`).map(code).join(", ");
894
+ const needs = spec.needs.map((n) => `- ${plain(n)}`);
895
+ const demoPage = `${KODA_DEMO}${spec.route}`;
896
+ const service = spec.service;
897
+ const config2 = ["```ts", spec.config, "```"];
898
+ if (lang.toLowerCase().startsWith("pl")) {
899
+ return [
900
+ `# Dodaj integrację ${service} od Koda Plus do tego sklepu na Medusie`,
901
+ "",
902
+ `Pracujesz w moim projekcie Medusa v2. Dodaj integrację ${service} od Koda Plus (${spec.pkg}) tak, żeby działała i wyglądała tak samo jak w sklepie demo Koda Plus, ${demoPage} (Panel, Przewodnik wdrożenia i Ustawienia), a potem zostaw projekt gotowy do dalszej pracy nad nią.`,
903
+ "",
904
+ "## Co robi integracja",
905
+ plain(spec.summary),
906
+ "",
907
+ "## Czego potrzeba po mojej stronie",
908
+ ...needs,
909
+ "",
910
+ "## Kroki",
911
+ "1. Zanim cokolwiek zmienisz, sprawdź projekt: Medusa 2.12 albo nowsza (tego wymagają strony i tłumaczenia panelu we wtyczce), menedżer pakietów projektu i medusa-config.ts. Jeśli coś blokuje instalację, zatrzymaj się i napisz mi, co.",
912
+ `2. Zainstaluj wtyczkę menedżerem pakietów projektu, na przykład ${code(`npm install ${spec.pkg}`)}. Jeśli rejestr jej nie znajduje, zatrzymaj się i daj mi znać: dostęp daje Koda Plus (${KODA_EMAIL}, Discord ${KODA_DISCORD}).`,
913
+ "3. Zarejestruj wtyczkę w medusa-config.ts, nie ruszając tego, co już tam jest. Użyj konfiguracji z Przewodnika wdrożenia:",
914
+ "",
915
+ ...config2,
916
+ "",
917
+ `4. Dodaj do opcji przełącznik trybu demo, tak jak w sklepie demo: ${code(spec.demo)}. Dopóki kluczy nie ma w .env, moduł pracuje na przykładowych danych i nie łączy się z ${service}.`,
918
+ `5. Wpisz zmienne do .env i do środowiska hostingu, nigdy do kodu ani do gita: ${env}. Jeśli nie podałem Ci wartości, zostaw ją pustą i wypisz mi ją na końcu.`,
919
+ `6. Uruchom migracje bazy (${code("npx medusa db:migrate")}), potem sklep, i otwórz ${spec.route} w panelu admina. Sprawdź, czy Panel, Przewodnik wdrożenia i Ustawienia otwierają się bez błędów w konsoli przeglądarki i w logu serwera.`,
920
+ `7. Nie włączaj żadnego zapisu, czyli niczego, co zmienia dane w ${service} albo w sklepie. Każdy zapis uzbraja człowiek w Ustawieniach, po próbie na sucho; opcje tylko na to pozwalają.`,
921
+ `8. Dopisz sekcję o tej integracji do notatek projektu dla agentów AI (CLAUDE.md albo AGENTS.md, zależnie od tego, czego używa projekt; jeśli nie ma żadnego, załóż CLAUDE.md): pakiet i jego wersja, gdzie są jego opcje w medusa-config.ts, zmienne środowiskowe, przełącznik demo, to, że zapisy uzbraja człowiek w Ustawieniach, że wdrożenie krok po kroku jest w panelu w Przewodniku wdrożenia, że pomoc jest na Discordzie (${KODA_DISCORD}) i że wtyczka zmienia się aktualizacją pakietu, nigdy edycją node_modules.`,
922
+ "",
923
+ "Na koniec napisz mi, co zmieniłeś i co zostało po mojej stronie: konta i klucze, pierwsze połączenie w Ustawieniach i uzbrojenie zapisów."
924
+ ].join("\n");
925
+ }
926
+ return [
927
+ `# Add the ${service} integration by Koda Plus to this Medusa store`,
928
+ "",
929
+ `You are working in my Medusa v2 project. Add the ${service} integration by Koda Plus (${spec.pkg}) so that it works and looks the same as in the Koda Plus demo store, ${demoPage} (Panel, Setup guide and Settings), then leave the project ready for further work on it.`,
930
+ "",
931
+ "## What the integration does",
932
+ plain(spec.summary),
933
+ "",
934
+ "## What it takes on my side",
935
+ ...needs,
936
+ "",
937
+ "## Steps",
938
+ "1. Before you change anything, check the project: Medusa 2.12 or newer (the plugin's admin pages and translations need it), the project's package manager and medusa-config.ts. If something blocks the install, stop and tell me what.",
939
+ `2. Install the plugin with the project's package manager, for example ${code(`npm install ${spec.pkg}`)}. If the registry cannot find it, stop and tell me: Koda Plus gives access (${KODA_EMAIL}, Discord ${KODA_DISCORD}).`,
940
+ "3. Register the plugin in medusa-config.ts without touching what is already there. Use the setup from the Setup guide:",
941
+ "",
942
+ ...config2,
943
+ "",
944
+ `4. Add the demo switch to the options, as the demo store has it: ${code(spec.demo)}. Until the keys are in .env, the module works on sample data and never calls ${service}.`,
945
+ `5. Put the variables in .env and in the hosting's environment, never in code or in git: ${env}. If I did not give you a value, leave it empty and list it for me at the end.`,
946
+ `6. Run the database migrations (${code("npx medusa db:migrate")}), then the store, and open ${spec.route} in the admin. Check that the Panel, the Setup guide and Settings open with no errors in the browser console or the server log.`,
947
+ `7. Do not turn on any writer, that is anything that changes data in ${service} or in the store. A person arms each writer in Settings, after a dry run; the options only allow it.`,
948
+ `8. Add a section about this integration to the project's notes for AI agents (CLAUDE.md or AGENTS.md, whichever the project uses; if neither exists, create CLAUDE.md): the package and its version, where its options sit in medusa-config.ts, the environment variables, the demo switch, that a person arms writers in Settings, that the step by step setup is in the admin under Setup guide, that help is on Discord (${KODA_DISCORD}), and that the plugin changes through package updates, never through edits in node_modules.`,
949
+ "",
950
+ "When you are done, tell me what you changed and what is left for me: accounts and keys, the first connection in Settings and arming the writers."
951
+ ].join("\n");
952
+ }
953
+ function PromptButton({ spec, lang, labels }) {
954
+ const prompt = react.useMemo(() => buildSetupPrompt(spec, lang), [spec, lang]);
955
+ const [state, setState] = react.useState("idle");
956
+ const copy = async () => {
957
+ try {
958
+ await navigator.clipboard.writeText(prompt);
959
+ setState("copied");
960
+ } catch {
961
+ setState("failed");
962
+ }
963
+ };
964
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Popover, { children: [
965
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "secondary", onClick: () => void copy(), children: [
966
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Sparkles, {}),
967
+ labels.button
968
+ ] }) }),
969
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Popover.Content, { align: "end", sideOffset: 8, className: "w-[min(560px,calc(100vw-32px))] p-0", children: [
970
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-x-2.5 border-b border-ui-border-base px-4 py-3", children: [
971
+ state === "copied" ? /* @__PURE__ */ jsxRuntime.jsx(icons.CheckCircleSolid, { className: "mt-px shrink-0 text-ui-tag-green-text" }) : null,
972
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-y-1", children: [
973
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: state === "copied" ? labels.copied : state === "failed" ? labels.failed : labels.title }),
974
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: labels.text })
975
+ ] })
976
+ ] }),
977
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "max-h-[45vh] overflow-auto whitespace-pre-wrap break-words bg-ui-bg-subtle px-4 py-3 font-mono text-[11px] leading-[18px] text-ui-fg-subtle", children: prompt }),
978
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-x-3 border-t border-ui-border-base px-4 py-3", children: [
979
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-mono txt-compact-xsmall text-ui-fg-muted", children: spec.pkg }),
980
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "secondary", onClick: () => void copy(), children: [
981
+ /* @__PURE__ */ jsxRuntime.jsx(icons.SquareTwoStack, {}),
982
+ labels.copy
983
+ ] })
984
+ ] })
985
+ ] })
986
+ ] });
987
+ }
988
+ function DiscordMark({ width = 16 }) {
989
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width, height: width * 48 / 64, viewBox: "0 0 64 48", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M40.575 0C39.9562 1.09866 39.4006 2.2352 38.8954 3.397C34.0967 2.67719 29.2096 2.67719 24.3982 3.397C23.9057 2.2352 23.3374 1.09866 22.7186 0C18.2104 0.770324 13.8157 2.12155 9.64839 4.02841C1.38951 16.2652 -0.845688 28.1863 0.265599 39.9432C5.10222 43.517 10.5197 46.2447 16.2909 47.9874C17.5916 46.2447 18.7407 44.3883 19.7257 42.4562C17.8568 41.7616 16.0509 40.8903 14.3208 39.88C14.7755 39.5517 15.2175 39.2107 15.6468 38.8824C25.7873 43.6559 37.5316 43.6559 47.6847 38.8824C48.1141 39.236 48.5561 39.577 49.0107 39.88C47.2806 40.9029 45.4748 41.7616 43.5931 42.4688C44.5781 44.4009 45.7273 46.2573 47.028 48C52.7991 46.2573 58.2167 43.5422 63.0533 39.9684C64.3666 26.3299 60.8055 14.5099 53.6452 4.04104C49.4905 2.13418 45.0959 0.782952 40.5876 0.0252565L40.575 0ZM21.1401 32.7072C18.0209 32.7072 15.4321 29.8785 15.4321 26.3804C15.4321 22.8824 17.9199 20.041 21.1275 20.041C24.3351 20.041 26.886 22.895 26.8354 26.3804C26.7849 29.8658 24.3224 32.7072 21.1401 32.7072ZM42.1788 32.7072C39.047 32.7072 36.4834 29.8785 36.4834 26.3804C36.4834 22.8824 38.9712 20.041 42.1788 20.041C45.3864 20.041 47.9246 22.895 47.8741 26.3804C47.8236 29.8658 45.3611 32.7072 42.1788 32.7072Z" }) });
990
+ }
991
+ function DiscordButton({ label, title }) {
992
+ return /* @__PURE__ */ jsxRuntime.jsxs(
993
+ "a",
994
+ {
995
+ href: KODA_DISCORD,
996
+ target: "_blank",
997
+ rel: "noreferrer",
998
+ title,
999
+ className: "txt-compact-small-plus inline-flex items-center gap-x-2 rounded-md bg-[#5865F2] px-2.5 py-1 text-white outline-none transition-fg hover:bg-[#4752C4] focus-visible:shadow-borders-focus",
1000
+ children: [
1001
+ /* @__PURE__ */ jsxRuntime.jsx(DiscordMark, {}),
1002
+ label
1003
+ ]
1004
+ }
1005
+ );
1006
+ }
1007
+ function HelpButtons({ spec, lang, labels }) {
1008
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 lg:justify-end", children: [
1009
+ /* @__PURE__ */ jsxRuntime.jsx(PromptButton, { spec, lang, labels: labels.prompt }),
1010
+ /* @__PURE__ */ jsxRuntime.jsx(DiscordButton, { label: labels.discord.button, title: labels.discord.title })
1011
+ ] });
1012
+ }
1013
+ function SettingsView({
1014
+ title,
1015
+ subtitle,
1016
+ tabs,
1017
+ value,
1018
+ onChange,
1019
+ children
1020
+ }) {
1021
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1022
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1023
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1 px-6 py-4", children: [
1024
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }),
1025
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "max-w-3xl text-ui-fg-subtle", children: subtitle })
1026
+ ] }),
1027
+ /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tablist", "aria-label": title, className: "flex gap-x-1 overflow-x-auto px-4 py-2", children: tabs.map((tab) => /* @__PURE__ */ jsxRuntime.jsxs(
1028
+ "button",
1029
+ {
1030
+ type: "button",
1031
+ role: "tab",
1032
+ "aria-selected": value === tab.id,
1033
+ onClick: () => onChange(tab.id),
1034
+ className: ui.clx(
1035
+ "txt-compact-small-plus inline-flex shrink-0 items-center gap-x-1.5 rounded-md px-3 py-1.5 outline-none transition-fg focus-visible:shadow-borders-focus",
1036
+ value === tab.id ? "bg-ui-bg-component text-ui-fg-base shadow-borders-base" : "text-ui-fg-subtle hover:bg-ui-bg-component-hover hover:text-ui-fg-base"
1037
+ ),
1038
+ children: [
1039
+ tab.label,
1040
+ tab.badge !== void 0 && tab.badge !== null && tab.badge !== 0 && tab.badge !== "" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: tab.tone ?? "grey", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: tab.badge }) }) : null
1041
+ ]
1042
+ },
1043
+ tab.id
1044
+ )) })
1045
+ ] }),
1046
+ children
1047
+ ] });
1048
+ }
1049
+ function References({ items, title, subtitle, openLabel, sinceLabel, reviewLabel, ratingLabel }) {
1050
+ if (items.length === 0) return null;
1051
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1052
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1 px-6 py-4", children: [
1053
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }),
1054
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: subtitle })
1055
+ ] }),
1056
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 px-6 py-4 md:grid-cols-2 xl:grid-cols-3", children: items.map((r) => {
1057
+ var _a;
1058
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 rounded-lg border border-ui-border-base bg-ui-bg-component px-4 py-3", children: [
1059
+ /* @__PURE__ */ jsxRuntime.jsxs("a", { href: r.url, target: "_blank", rel: "noreferrer", className: "group flex items-center gap-x-3", children: [
1060
+ r.icon ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: r.icon, alt: "", width: 36, height: 36, className: "h-9 w-9 shrink-0 rounded-md bg-white object-contain shadow-borders-base" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-medium-plus flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-ui-bg-base text-ui-fg-base shadow-borders-base", children: r.name.slice(0, 1).toUpperCase() }),
1061
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col", children: [
1062
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "truncate text-ui-fg-base", children: r.name }),
1063
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "truncate text-ui-fg-interactive group-hover:text-ui-fg-interactive-hover", children: host(r.url) })
1064
+ ] }),
1065
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "ml-auto shrink-0 text-ui-fg-muted group-hover:text-ui-fg-interactive" })
1066
+ ] }),
1067
+ r.review ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [
1068
+ /* @__PURE__ */ jsxRuntime.jsx(Stars, { rating: r.review.rating, scale: r.review.scale }),
1069
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus tabular-nums text-ui-fg-base", children: (ratingLabel ?? ((v) => v.toFixed(1)))(r.review.rating / (r.review.scale || 5) * 5) }),
1070
+ /* @__PURE__ */ jsxRuntime.jsx(SourceMark, { review: r.review }),
1071
+ r.review.url && reviewLabel ? /* @__PURE__ */ jsxRuntime.jsxs("a", { href: r.review.url, target: "_blank", rel: "noreferrer", className: "txt-compact-xsmall-plus inline-flex items-center gap-x-0.5 text-ui-fg-interactive hover:text-ui-fg-interactive-hover", children: [
1072
+ reviewLabel,
1073
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "h-3.5 w-3.5" })
1074
+ ] }) : null
1075
+ ] }) : null,
1076
+ r.description ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: nb(r.description) }) : null,
1077
+ ((_a = r.metrics) == null ? void 0 : _a.length) || r.since ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1078
+ (r.metrics ?? []).map((m) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "green", children: [
1079
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: m.value }),
1080
+ " ",
1081
+ m.label
1082
+ ] }, m.label)),
1083
+ r.since ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: sinceLabel(r.since) }) : null
1084
+ ] }) : null,
1085
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-auto flex flex-col gap-y-1 border-t border-ui-border-base pt-2", children: [
1086
+ (r.links ?? []).map((l) => /* @__PURE__ */ jsxRuntime.jsxs("a", { href: l.url, target: "_blank", rel: "noreferrer", className: "txt-compact-small inline-flex w-fit items-center gap-x-1 text-ui-fg-interactive hover:text-ui-fg-interactive-hover", children: [
1087
+ l.label,
1088
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "shrink-0" })
1089
+ ] }, l.url)),
1090
+ /* @__PURE__ */ jsxRuntime.jsxs("a", { href: r.url, target: "_blank", rel: "noreferrer", className: "txt-compact-small-plus inline-flex w-fit items-center gap-x-1 text-ui-fg-interactive hover:text-ui-fg-interactive-hover", children: [
1091
+ openLabel,
1092
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "shrink-0" })
1093
+ ] })
1094
+ ] })
1095
+ ] }, r.url);
1096
+ }) })
1097
+ ] });
1098
+ }
1099
+ function GuideIntro({ title, text, time, needs, needsLabel, timeLabel }) {
1100
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1101
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 py-4", children: [
1102
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }),
1103
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "max-w-3xl text-ui-fg-subtle", children: text })
1104
+ ] }),
1105
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 px-6 py-4 md:grid-cols-[200px_1fr]", children: [
1106
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1107
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: timeLabel }),
1108
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: time })
1109
+ ] }),
1110
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1111
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: needsLabel }),
1112
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: needs.map((n) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "small", color: "grey", children: n }, n)) })
1113
+ ] })
1114
+ ] })
1115
+ ] });
1116
+ }
1117
+ const STATE_TONE = { done: "green", todo: "orange", optional: "grey", later: "blue" };
1118
+ function CodeBlock({ code, copyLabel, copiedLabel }) {
1119
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-subtle", children: [
1120
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "txt-compact-small overflow-x-auto px-4 py-3 pr-12 font-mono text-ui-fg-base", children: code }),
1121
+ /* @__PURE__ */ jsxRuntime.jsx(
1122
+ "button",
1123
+ {
1124
+ type: "button",
1125
+ "aria-label": copyLabel,
1126
+ title: copyLabel,
1127
+ className: "absolute right-2 top-2 rounded-md p-1.5 text-ui-fg-muted transition-fg hover:bg-ui-bg-component-hover hover:text-ui-fg-base",
1128
+ onClick: () => {
1129
+ var _a;
1130
+ void ((_a = navigator.clipboard) == null ? void 0 : _a.writeText(code).then(() => ui.toast.success(copiedLabel)));
1131
+ },
1132
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.SquareTwoStack, {})
1133
+ }
1134
+ )
1135
+ ] });
1136
+ }
1137
+ function GuideSteps({
1138
+ title,
1139
+ subtitle,
1140
+ steps,
1141
+ stateLabels,
1142
+ checkLabel,
1143
+ copyLabel,
1144
+ copiedLabel
1145
+ }) {
1146
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1147
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1 px-6 py-4", children: [
1148
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }),
1149
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: subtitle }) : null
1150
+ ] }),
1151
+ /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "flex flex-col px-6 py-5", children: steps.map((s, i) => {
1152
+ const last = i === steps.length - 1;
1153
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "relative flex gap-x-4 pb-6 last:pb-0", children: [
1154
+ !last ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "absolute left-[13px] top-8 bottom-0 w-px bg-ui-border-base" }) : null,
1155
+ /* @__PURE__ */ jsxRuntime.jsx(
1156
+ "span",
1157
+ {
1158
+ className: ui.clx(
1159
+ "txt-compact-small-plus relative z-[1] flex h-7 w-7 shrink-0 items-center justify-center rounded-full border",
1160
+ s.state === "done" ? "border-ui-tag-green-border bg-ui-tag-green-bg text-ui-tag-green-text" : "border-ui-border-base bg-ui-bg-base text-ui-fg-subtle"
1161
+ ),
1162
+ children: s.state === "done" ? /* @__PURE__ */ jsxRuntime.jsx(icons.CheckCircleSolid, { className: "text-ui-tag-green-icon" }) : i + 1
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-2 pt-0.5", children: [
1166
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1167
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "base", weight: "plus", className: "text-ui-fg-base", children: s.title }),
1168
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: STATE_TONE[s.state], children: stateLabels[s.state] })
1169
+ ] }),
1170
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "txt-small flex max-w-3xl flex-col gap-y-2 text-ui-fg-subtle", children: s.body }),
1171
+ s.code ? /* @__PURE__ */ jsxRuntime.jsx(CodeBlock, { code: s.code, copyLabel, copiedLabel }) : null,
1172
+ s.link ? /* @__PURE__ */ jsxRuntime.jsxs("a", { href: s.link.href, target: "_blank", rel: "noreferrer", className: "txt-compact-small-plus inline-flex w-fit items-center gap-x-1 text-ui-fg-interactive hover:text-ui-fg-interactive-hover", children: [
1173
+ s.link.label,
1174
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, {})
1175
+ ] }) : null,
1176
+ s.check ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex max-w-3xl gap-x-2 rounded-lg border border-ui-border-base bg-ui-bg-subtle px-3 py-2", children: [
1177
+ /* @__PURE__ */ jsxRuntime.jsx(icons.CheckCircleSolid, { className: "mt-0.5 shrink-0 text-ui-tag-green-icon" }),
1178
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
1179
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-ui-fg-base", children: checkLabel }),
1180
+ " ",
1181
+ s.check
1182
+ ] })
1183
+ ] }) : null
1184
+ ] })
1185
+ ] }, s.id);
1186
+ }) })
1187
+ ] });
1188
+ }
1189
+ function GuideDiagram({ title, subtitle, nodes, links }) {
1190
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1191
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1 px-6 py-4", children: [
1192
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }),
1193
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: subtitle }) : null
1194
+ ] }),
1195
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-stretch gap-2 px-6 py-5 lg:flex-row lg:items-center", children: nodes.map((n, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "contents", children: [
1196
+ /* @__PURE__ */ jsxRuntime.jsxs(
1197
+ "div",
1198
+ {
1199
+ className: ui.clx(
1200
+ "flex min-w-0 flex-1 flex-col gap-y-1 rounded-lg border px-4 py-3",
1201
+ n.accent ? "border-ui-border-interactive bg-ui-bg-highlight" : "border-ui-border-base bg-ui-bg-component"
1202
+ ),
1203
+ children: [
1204
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: n.title }),
1205
+ n.caption ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: n.caption }) : null
1206
+ ]
1207
+ }
1208
+ ),
1209
+ i < nodes.length - 1 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 flex-col items-center justify-center px-1 text-center lg:w-28", children: [
1210
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: links[i] ?? "" }),
1211
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-hidden": true, className: "txt-compact-medium text-ui-fg-muted", children: [
1212
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden lg:inline", children: "→" }),
1213
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lg:hidden", children: "↓" })
1214
+ ] })
1215
+ ] }) : null
1216
+ ] }, n.title)) })
1217
+ ] });
1218
+ }
1219
+ function GuideChecklist({ title, subtitle, items }) {
1220
+ const done = items.filter((i) => i.done).length;
1221
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1222
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4 px-6 py-4", children: [
1223
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1224
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }),
1225
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: subtitle }) : null
1226
+ ] }),
1227
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "small", color: done === items.length ? "green" : "orange", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "tabular-nums", children: [
1228
+ done,
1229
+ "/",
1230
+ items.length
1231
+ ] }) })
1232
+ ] }),
1233
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-y-2 px-6 py-4", children: items.map((i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex gap-x-2", children: [
1234
+ /* @__PURE__ */ jsxRuntime.jsx(
1235
+ "span",
1236
+ {
1237
+ className: ui.clx(
1238
+ "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border",
1239
+ i.done ? "border-ui-tag-green-border bg-ui-tag-green-bg" : "border-ui-border-base bg-ui-bg-base"
1240
+ ),
1241
+ children: i.done ? /* @__PURE__ */ jsxRuntime.jsx(icons.CheckCircleSolid, { className: "h-4 w-4 text-ui-tag-green-icon" }) : null
1242
+ }
1243
+ ),
1244
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex flex-col", children: [
1245
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: i.done ? "text-ui-fg-base" : "text-ui-fg-subtle", children: i.label }),
1246
+ i.hint ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: i.hint }) : null
1247
+ ] })
1248
+ ] }, i.label)) })
1249
+ ] });
1250
+ }
1251
+ function GuideFaq({ title, items }) {
1252
+ const [open, setOpen] = react.useState(null);
1253
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1254
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }) }),
1255
+ items.map((it, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6", children: [
1256
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => setOpen(open === i ? null : i), className: "flex w-full items-center justify-between gap-4 py-3 text-left", "aria-expanded": open === i, children: [
1257
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: it.q }),
1258
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ChevronDownMini, { className: ui.clx("shrink-0 text-ui-fg-muted transition-transform", open === i && "rotate-180") })
1259
+ ] }),
1260
+ open === i ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "txt-small flex max-w-3xl flex-col gap-y-2 pb-4 text-ui-fg-subtle", children: it.a }) : null
1261
+ ] }, it.q))
1262
+ ] });
1263
+ }
1264
+ const CLOCK_SKEW_WARN_SECONDS = 60;
1265
+ const CLOCK_SKEW_LIMIT_SECONDS = 300;
1266
+ function skewLevel(skewMs) {
1267
+ if (skewMs === null) return "unknown";
1268
+ const s = Math.abs(skewMs) / 1e3;
1269
+ if (s < CLOCK_SKEW_WARN_SECONDS) return "ok";
1270
+ return s < CLOCK_SKEW_LIMIT_SECONDS ? "warn" : "error";
1271
+ }
1272
+ const GUIDE_STEPS = [
1273
+ "subiekt",
1274
+ "sdk",
1275
+ "service",
1276
+ "tunnel",
1277
+ "access",
1278
+ "medusa",
1279
+ "connection",
1280
+ "catalog",
1281
+ "testOrder",
1282
+ "documents",
1283
+ "prices",
1284
+ "live"
1285
+ ];
1286
+ const OPTIONAL_CHECKS = /* @__PURE__ */ new Set(["webhook"]);
1287
+ function guideChecks(s) {
1288
+ var _a, _b;
1289
+ const h = s.connection.health;
1290
+ const stock = s.lastRuns.stock;
1291
+ return {
1292
+ reachable: s.connection.reachable,
1293
+ signatures: s.diagnostics.signature === "ok",
1294
+ clock: skewLevel(s.diagnostics.clockSkewMs) === "ok",
1295
+ contract: Boolean(h) && !s.diagnostics.legacy,
1296
+ subiekt: Boolean((_a = h == null ? void 0 : h.subiekt) == null ? void 0 : _a.connected) && ((_b = h == null ? void 0 : h.subiekt) == null ? void 0 : _b.licence) !== "refused",
1297
+ stock: Boolean(stock && (stock.status === "success" || stock.status === "partial")),
1298
+ dryRun: !s.options.stockDryRun,
1299
+ zk: s.counts.zk > 0,
1300
+ wz: s.counts.wz > 0,
1301
+ // Nothing allowed means nothing to decide.
1302
+ writers: s.writers.filter((w) => w.allowed).every((w) => w.changedAt !== null),
1303
+ attention: s.counts.failed === 0,
1304
+ webhook: Boolean(s.diagnostics.webhook.lastAt)
1305
+ };
1306
+ }
1307
+ function readyForLive(checks) {
1308
+ return Object.keys(checks).every((k) => OPTIONAL_CHECKS.has(k) || checks[k]);
1309
+ }
1310
+ function guideStepStates(s) {
1311
+ var _a, _b, _c;
1312
+ const checks = guideChecks(s);
1313
+ const done = (ok) => ok ? "done" : "todo";
1314
+ const connected = Boolean((_b = (_a = s.connection.health) == null ? void 0 : _a.subiekt) == null ? void 0 : _b.connected);
1315
+ const writer = (key) => s.writers.find((w) => w.key === key);
1316
+ const products = s.lastRuns.products;
1317
+ const productsSupported = s.diagnostics.capabilities.includes("products");
1318
+ const productsRead = !productsSupported || !s.options.productSyncEnabled || Boolean(products && products.status !== "error");
1319
+ const prices = writer("prices");
1320
+ const creator = writer("products");
1321
+ return {
1322
+ subiekt: done(connected),
1323
+ // Sfera logs in only to a database of its own version: a login proves the SDK matches.
1324
+ sdk: done(connected),
1325
+ service: done(s.connection.reachable),
1326
+ tunnel: done(s.connection.reachable),
1327
+ access: s.options.cfAccess ? "done" : "optional",
1328
+ medusa: done(s.mode === "demo" || s.configured),
1329
+ connection: done(checks.reachable && connected && checks.signatures && checks.clock),
1330
+ catalog: done(checks.stock && productsRead),
1331
+ testOrder: done(checks.zk && checks.wz),
1332
+ documents: s.options.salesDocument === "none" ? "optional" : done(Boolean((_c = writer("documents")) == null ? void 0 : _c.active) && s.counts.fs + s.counts.pa > 0),
1333
+ prices: !(prices == null ? void 0 : prices.allowed) && !(creator == null ? void 0 : creator.allowed) ? "optional" : done(Boolean((prices == null ? void 0 : prices.active) || (creator == null ? void 0 : creator.active))),
1334
+ live: readyForLive(checks) ? "done" : "later"
1335
+ };
1336
+ }
1337
+ const LINKS = {
1338
+ insert: "https://www.insert.com.pl/programy_dla_firm/sprzedaz/subiekt_nexo_pro/opis.html",
1339
+ tunnel: "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel/",
1340
+ access: "https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/",
1341
+ dotnet: "https://dotnet.microsoft.com/download/dotnet/8.0"
1342
+ };
1343
+ const SERVICE_CODE = [
1344
+ '$b = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); -join ($b | ForEach-Object { $_.ToString("x2") })',
1345
+ '.\\deploy\\install-service.ps1 -ServiceUser "SERWER\\integracja"',
1346
+ "C:\\KodaSubiektBridge\\Koda.SubiektBridge.exe --check"
1347
+ ].join("\n");
1348
+ const MEDUSA_CODE = `{
1349
+ resolve: "@koda-plus/medusa-plugin-subiekt-nexo",
1350
+ options: {
1351
+ bridgeUrl: process.env.SUBIEKT_BRIDGE_URL,
1352
+ secret: process.env.SUBIEKT_SECRET,
1353
+ cfAccessClientId: process.env.SUBIEKT_CF_ACCESS_CLIENT_ID,
1354
+ cfAccessClientSecret: process.env.SUBIEKT_CF_ACCESS_CLIENT_SECRET,
1355
+ stockLocationId: process.env.SUBIEKT_STOCK_LOCATION_ID,
1356
+ stockDryRun: true,
1357
+ salesDocument: "none",
1358
+ salesDocumentAfter: "wz",
1359
+ nipSources: ["metadata.nip", "billing_address.company"],
1360
+ priceWriter: false,
1361
+ createMissingProducts: false,
1362
+ createContractors: false,
1363
+ },
1364
+ },
1365
+
1366
+ npx medusa db:migrate`;
1367
+ function usePromptSpec() {
1368
+ const { t } = reactI18next.useTranslation("subiekt");
1369
+ return react.useMemo(() => {
1370
+ const needs = t("guide.intro.needs", { returnObjects: true });
1371
+ return {
1372
+ service: "Subiekt nexo",
1373
+ pkg: "@koda-plus/medusa-plugin-subiekt-nexo",
1374
+ route: "/app/subiekt",
1375
+ summary: t("subtitle"),
1376
+ needs: Array.isArray(needs) ? needs : [],
1377
+ config: `// medusa-config.ts, in plugins: [ ... ]
1378
+ ${MEDUSA_CODE}`,
1379
+ demo: 'demo: process.env.SUBIEKT_DEMO === "true" || !process.env.SUBIEKT_BRIDGE_URL,'
1380
+ };
1381
+ }, [t]);
1382
+ }
1383
+ const CLOCK_CODE = "w32tm /resync";
1384
+ const WRITERS_CODE = `priceWriter: true,
1385
+ maxPriceChangesPerRun: 200,
1386
+ createMissingProducts: true,
1387
+ maxProductsPerRun: 20,`;
1388
+ const DOCUMENTS_CODE = `salesDocument: "auto",
1389
+ salesDocumentAfter: "wz",
1390
+ createContractors: true,`;
1391
+ function GuideView({ status, lang }) {
1392
+ const { t } = reactI18next.useTranslation("subiekt");
1393
+ function list(key) {
1394
+ const value = t(key, { returnObjects: true });
1395
+ return Array.isArray(value) ? value : [];
1396
+ }
1397
+ const paragraphs = (key) => list(key).map((p, i) => /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx(RichText, { text: p }) }, i));
1398
+ const checks = guideChecks(status);
1399
+ const states = guideStepStates(status);
1400
+ const extras = {
1401
+ sdk: { link: { label: t("guide.links.sdk"), href: LINKS.insert } },
1402
+ service: { code: SERVICE_CODE, link: { label: t("guide.links.dotnet"), href: LINKS.dotnet } },
1403
+ tunnel: { code: `curl https://subiekt-bridge.${t("guide.domain")}/healthz`, link: { label: t("guide.links.tunnel"), href: LINKS.tunnel } },
1404
+ access: { link: { label: t("guide.links.access"), href: LINKS.access } },
1405
+ medusa: { code: MEDUSA_CODE },
1406
+ connection: { code: CLOCK_CODE },
1407
+ documents: { code: DOCUMENTS_CODE },
1408
+ prices: { code: WRITERS_CODE }
1409
+ };
1410
+ const steps = GUIDE_STEPS.map((id) => ({
1411
+ id,
1412
+ title: t(`guide.steps.${id}.title`),
1413
+ state: states[id],
1414
+ body: paragraphs(`guide.steps.${id}.body`),
1415
+ check: t(`guide.steps.${id}.check`),
1416
+ ...extras[id]
1417
+ }));
1418
+ const s = status;
1419
+ const nodes = list("guide.diagram.nodes").map((n, i) => ({ ...n, accent: i === 2 }));
1420
+ const faq = list("guide.faq.items").map((item) => ({
1421
+ q: item.q,
1422
+ a: (item.a ?? []).map((p, i) => /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx(RichText, { text: p }) }, i))
1423
+ }));
1424
+ const references = referencesFor(s.references, lang);
1425
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
1426
+ /* @__PURE__ */ jsxRuntime.jsx(
1427
+ GuideIntro,
1428
+ {
1429
+ title: t("guide.intro.title"),
1430
+ text: t("guide.intro.text"),
1431
+ time: t("guide.intro.time"),
1432
+ timeLabel: t("guide.intro.timeLabel"),
1433
+ needs: list("guide.intro.needs"),
1434
+ needsLabel: t("guide.intro.needsLabel")
1435
+ }
1436
+ ),
1437
+ s.mode === "demo" ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "info", label: t("demo.label"), children: t("guide.demoNote") }) : null,
1438
+ /* @__PURE__ */ jsxRuntime.jsx(GuideDiagram, { title: t("guide.diagram.title"), subtitle: t("guide.diagram.subtitle"), nodes, links: list("guide.diagram.links") }),
1439
+ /* @__PURE__ */ jsxRuntime.jsx(
1440
+ GuideSteps,
1441
+ {
1442
+ title: t("guide.steps.title"),
1443
+ subtitle: t("guide.steps.subtitle"),
1444
+ steps,
1445
+ stateLabels: {
1446
+ done: t("guide.steps.states.done"),
1447
+ todo: t("guide.steps.states.todo"),
1448
+ optional: t("guide.steps.states.optional"),
1449
+ later: t("guide.steps.states.later")
1450
+ },
1451
+ checkLabel: t("guide.steps.checkLabel"),
1452
+ copyLabel: t("guide.steps.copy"),
1453
+ copiedLabel: t("guide.steps.copied")
1454
+ }
1455
+ ),
1456
+ /* @__PURE__ */ jsxRuntime.jsx(
1457
+ GuideChecklist,
1458
+ {
1459
+ title: t("guide.checklist.title"),
1460
+ subtitle: t("guide.checklist.subtitle"),
1461
+ items: Object.keys(checks).map((key) => ({
1462
+ label: t(`guide.checklist.items.${key}.label`),
1463
+ hint: t(`guide.checklist.items.${key}.hint`),
1464
+ done: checks[key]
1465
+ }))
1466
+ }
1467
+ ),
1468
+ /* @__PURE__ */ jsxRuntime.jsx(GuideFaq, { title: t("guide.faq.title"), items: faq }),
1469
+ /* @__PURE__ */ jsxRuntime.jsx(
1470
+ References,
1471
+ {
1472
+ items: references,
1473
+ title: t("references.title"),
1474
+ subtitle: t("references.subtitle"),
1475
+ openLabel: t("references.open"),
1476
+ sinceLabel: (since) => t("references.since", { date: monthYear(since, lang) }),
1477
+ reviewLabel: t("references.review"),
1478
+ ratingLabel: (value) => fmtRating(value, lang)
1479
+ }
1480
+ )
1481
+ ] });
1482
+ }
1483
+ const PAGE_SIZE$1 = 15;
1484
+ function SectionHeader({ title, subtitle, children }) {
1485
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 px-6 py-4 md:flex-row md:items-start md:justify-between", children: [
1486
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1487
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }),
1488
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "max-w-2xl text-ui-fg-subtle", children: subtitle }) : null
1489
+ ] }),
1490
+ children ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children }) : null
1491
+ ] });
1492
+ }
1493
+ const LEVEL_TEXT = {
1494
+ ok: "text-ui-fg-base",
1495
+ warn: "text-ui-tag-orange-text",
1496
+ error: "text-ui-tag-red-text",
1497
+ unknown: "text-ui-fg-muted"
1498
+ };
1499
+ function signedMs(ms) {
1500
+ const sign = ms > 0 ? "+" : ms < 0 ? "-" : "";
1501
+ return `${sign}${fmtMs(Math.abs(ms))}`;
1502
+ }
1503
+ function BridgeSection({ status, lang }) {
1504
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1505
+ const { t } = reactI18next.useTranslation("subiekt");
1506
+ const d = status.diagnostics;
1507
+ const h = status.connection.health;
1508
+ const none = t("connection.none");
1509
+ const skew = skewLevel(d.clockSkewMs);
1510
+ const licence = ((_a = h == null ? void 0 : h.subiekt) == null ? void 0 : _a.licence) === "ok" || ((_b = h == null ? void 0 : h.subiekt) == null ? void 0 : _b.licence) === "refused" ? h.subiekt.licence : "unknown";
1511
+ const signatureTone = d.signature === "ok" ? LEVEL_TEXT.ok : d.signature === "unknown" ? LEVEL_TEXT.unknown : LEVEL_TEXT.error;
1512
+ const rejectedLater = Boolean(d.webhook.rejectedAt && (!d.webhook.lastAt || d.webhook.rejectedAt > d.webhook.lastAt));
1513
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1514
+ /* @__PURE__ */ jsxRuntime.jsx(SectionHeader, { title: t("bridge.title"), subtitle: t("bridge.subtitle") }),
1515
+ d.legacy ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "warning", label: t("bridge.title"), children: t("bridge.legacy") }) }) : null,
1516
+ status.mode === "live" && ((_c = h == null ? void 0 : h.bridge) == null ? void 0 : _c.mode) === "fake" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "warning", label: t("bridge.title"), children: t("bridge.fakeMode") }) }) : null,
1517
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-2", children: [
1518
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.versions"), children: t("bridge.versionsValue", { bridge: ((_d = h == null ? void 0 : h.bridge) == null ? void 0 : _d.version) ?? "?", contract: ((_e = h == null ? void 0 : h.bridge) == null ? void 0 : _e.contract) ?? "?", plugin: status.pluginVersion }) }),
1519
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.sdk"), children: h ? t("bridge.sdkValue", { sdk: ((_f = h.bridge) == null ? void 0 : _f.sdk_version) ?? none, db: ((_g = h.subiekt) == null ? void 0 : _g.database_version) ?? none }) : none }),
1520
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.licence"), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: licence === "refused" ? LEVEL_TEXT.error : licence === "ok" ? LEVEL_TEXT.ok : LEVEL_TEXT.unknown, children: t(`bridge.licenceStates.${licence}`) }) }),
1521
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.started"), children: fmtDateTime((_h = h == null ? void 0 : h.bridge) == null ? void 0 : _h.started_at, lang) || none }),
1522
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.latency"), children: fmtMs(d.latencyMs) }),
1523
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.skew"), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: LEVEL_TEXT[skew], children: t(`bridge.skewStates.${skew}`, { value: d.clockSkewMs === null ? "" : signedMs(d.clockSkewMs) }) }) }),
1524
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.signature"), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: signatureTone, children: t(`bridge.signatureStates.${d.signature}`, { defaultValue: d.signature }) }) }),
1525
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.lastEvent"), children: (h == null ? void 0 : h.events) ? t("bridge.lastEventValue", { id: h.events.last_id, time: fmtDateTime(h.events.last_at, lang) || none }) : none }),
1526
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.queues"), children: (h == null ? void 0 : h.queues) ? t("bridge.queuesValue", { sfera: h.queues.sfera_pending, webhook: h.queues.webhook_pending }) : none }),
1527
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("bridge.webhook"), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
1528
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: d.webhook.lastAt ? t("bridge.webhookValue", { time: fmtDateTime(d.webhook.lastAt, lang) }) : t("bridge.webhookNever") }),
1529
+ rejectedLater ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: LEVEL_TEXT.warn, children: t("bridge.webhookRejected", {
1530
+ time: fmtDateTime(d.webhook.rejectedAt, lang),
1531
+ reason: t(`bridge.webhookReasons.${d.webhook.rejectReason ?? "missing"}`, { defaultValue: d.webhook.rejectReason ?? "" })
1532
+ }) }) : null
1533
+ ] }) }),
1534
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("connection.checkedAt"), children: fmtDateTime(d.checkedAt, lang) || t("connection.never") })
1535
+ ] }),
1536
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
1537
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1538
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: t("bridge.capabilities") }),
1539
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: d.capabilities.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: none }) : d.capabilities.map((c) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "green", children: t(`bridge.capabilityNames.${c}`, { defaultValue: c }) }, c)) })
1540
+ ] }),
1541
+ d.missing.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1542
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: t("bridge.cannot") }),
1543
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-y-1", children: d.missing.map((m) => {
1544
+ const hint = m.reason === "bridge_config" ? t(`bridge.capabilityHints.${m.capability}`, { defaultValue: "" }) : "";
1545
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "txt-compact-small flex flex-wrap gap-x-1", children: [
1546
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-ui-fg-base", children: [
1547
+ t(`bridge.capabilityNames.${m.capability}`, { defaultValue: m.capability }),
1548
+ ":"
1549
+ ] }),
1550
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-subtle", children: t(`bridge.reasons.${m.reason}`, { defaultValue: m.reason }) }),
1551
+ hint ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-ui-fg-muted", children: [
1552
+ "(",
1553
+ hint,
1554
+ ")"
1555
+ ] }) : null
1556
+ ] }, m.capability);
1557
+ }) })
1558
+ ] }) : null
1559
+ ] })
1560
+ ] });
1561
+ }
1562
+ const WRITER_ORDER = ["documents", "contractors", "prices", "products"];
1563
+ function WriterRow({ writer, status, lang }) {
1564
+ const { t } = reactI18next.useTranslation("subiekt");
1565
+ const prompt = ui.usePrompt();
1566
+ const toggle = useSubiektWriter();
1567
+ const name = t(`writers.names.${writer.key}`);
1568
+ const demo = status.mode === "demo";
1569
+ const o = status.options;
1570
+ const description = t(`writers.descriptions.${writer.key}`, {
1571
+ cap: writer.key === "prices" ? o.maxPriceChangesPerRun : o.maxProductsPerRun,
1572
+ mode: t(`writers.documentModes.${o.salesDocument}`)
1573
+ });
1574
+ const onChange = async (armed) => {
1575
+ const ok = await prompt({
1576
+ title: armed ? t("writers.armTitle", { name }) : t("writers.disarmTitle", { name }),
1577
+ description: armed ? demo ? t("writers.armTextDemo") : t("writers.armText") : t("writers.disarmText"),
1578
+ confirmText: armed ? t("writers.armConfirm") : t("writers.disarmConfirm"),
1579
+ cancelText: t("writers.cancel")
1580
+ });
1581
+ if (!ok) return;
1582
+ try {
1583
+ await toggle.mutateAsync({ writer: writer.key, armed });
1584
+ ui.toast.success(armed ? t("writers.armedToast") : t("writers.disarmedToast"));
1585
+ } catch (err) {
1586
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
1587
+ }
1588
+ };
1589
+ const canArm = writer.allowed && !writer.unsupported;
1590
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 px-6 py-4 md:flex-row md:items-start md:justify-between", children: [
1591
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [
1592
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1593
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: name }),
1594
+ writer.active ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "green", children: t("writers.active") }) : writer.armed ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "orange", children: t("writers.blocked") }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: t("writers.disarmed") }),
1595
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-x-1", children: [
1596
+ writer.allowed ? null : /* @__PURE__ */ jsxRuntime.jsx(icons.LockClosedSolid, {}),
1597
+ t("writers.option", { option: writer.option }),
1598
+ " ",
1599
+ writer.allowed ? t("writers.allowed") : t("writers.forbidden")
1600
+ ] }) })
1601
+ ] }),
1602
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "max-w-2xl text-ui-fg-subtle", children: description }),
1603
+ writer.unsupported ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-tag-orange-text", children: t("writers.unsupported") }) : null,
1604
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: writer.changedAt ? t(writer.armed ? "writers.armedBy" : "writers.disarmedBy", { who: writer.changedBy ?? "?", time: fmtDateTime(writer.changedAt, lang) }) : t("writers.never") })
1605
+ ] }),
1606
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0 items-center gap-x-2 pt-0.5", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Switch, { checked: writer.armed, disabled: toggle.isPending || !writer.armed && !canArm, onCheckedChange: (v) => void onChange(Boolean(v)), "aria-label": name }) })
1607
+ ] });
1608
+ }
1609
+ function WritersSection({ status, lang }) {
1610
+ const { t } = reactI18next.useTranslation("subiekt");
1611
+ const writers = WRITER_ORDER.map((k) => status.writers.find((w) => w.key === k)).filter((w) => Boolean(w));
1612
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1613
+ /* @__PURE__ */ jsxRuntime.jsx(SectionHeader, { title: t("writers.title"), subtitle: t("writers.subtitle") }),
1614
+ status.mode === "demo" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: t("writers.demoNote") }) }) : null,
1615
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: writers.map((w) => /* @__PURE__ */ jsxRuntime.jsx(WriterRow, { writer: w, status, lang }, w.key)) })
1616
+ ] });
1617
+ }
1618
+ const STATUS_FILTERS = ["planned", "applied", "simulated", "over_cap", "stale", "failed", "quarantined", "skipped"];
1619
+ function ChangeCell({ change, lang }) {
1620
+ const { t } = reactI18next.useTranslation("subiekt");
1621
+ if (change.kind === "create") return /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("products.newProduct", { price: fmtMoney(change.to, change.currency, lang) }) });
1622
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "whitespace-nowrap tabular-nums", children: [
1623
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-muted", children: fmtMoney(change.from, change.currency, lang) }),
1624
+ " → ",
1625
+ fmtMoney(change.to, change.currency, lang)
1626
+ ] });
1627
+ }
1628
+ function SubiektProductCell({ change: c }) {
1629
+ const codes = [c.kind === "create" ? c.symbol : null, c.ean ? `EAN ${c.ean}` : null].filter(Boolean).join(", ");
1630
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col", children: [
1631
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "truncate text-ui-fg-base", children: c.kind === "create" ? c.title ?? c.symbol : c.symbol }),
1632
+ codes ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "truncate font-mono text-ui-fg-muted", children: codes }) : null
1633
+ ] });
1634
+ }
1635
+ function StoreProductCell({ change: c }) {
1636
+ const { t } = reactI18next.useTranslation("subiekt");
1637
+ if (c.productId) {
1638
+ const code = c.matchedBy === "ean" ? c.ean : c.sku;
1639
+ const matched = c.matchedBy && code ? t("products.matchedKey", { by: t(`products.matchedBy.${c.matchedBy}`, { defaultValue: c.matchedBy }), code }) : void 0;
1640
+ return /* @__PURE__ */ jsxRuntime.jsx(
1641
+ StoreLink,
1642
+ {
1643
+ to: `/products/${c.productId}`,
1644
+ title: c.title ?? c.sku ?? c.symbol,
1645
+ mono: c.sku ?? (c.ean ? `EAN ${c.ean}` : null),
1646
+ monoTitle: matched,
1647
+ action: t("store.openProduct")
1648
+ }
1649
+ );
1650
+ }
1651
+ if (c.kind === "create" && c.status === "applied") {
1652
+ return /* @__PURE__ */ jsxRuntime.jsx(StoreLink, { to: `/products?q=${encodeURIComponent(c.symbol)}`, title: c.title ?? c.symbol, mono: c.symbol, monoTitle: t("store.draft"), action: t("store.findProduct") });
1653
+ }
1654
+ if (c.kind === "create") return /* @__PURE__ */ jsxRuntime.jsx(StoreUnlinked, { label: t("store.notInStore"), hint: t("store.notInStoreHint"), mono: c.symbol });
1655
+ return /* @__PURE__ */ jsxRuntime.jsx(StoreUnlinked, { label: t("store.noProduct"), mono: c.sku ?? c.symbol });
1656
+ }
1657
+ function ProductsSection({ status, lang, poll, onAction }) {
1658
+ var _a, _b, _c, _d, _e, _f;
1659
+ const { t } = reactI18next.useTranslation("subiekt");
1660
+ const [kind, setKind] = react.useState("all");
1661
+ const [filter, setFilter] = react.useState("all");
1662
+ const [search, setSearch] = react.useState("");
1663
+ const [offset, setOffset] = react.useState(0);
1664
+ const q = search.trim();
1665
+ const running = status.running.includes("products");
1666
+ const products = useSubiektProducts(kind, filter, q, offset, PAGE_SIZE$1, poll || running);
1667
+ const sync = useSubiektSync();
1668
+ const release = useSubiektRelease();
1669
+ react.useEffect(() => setOffset(0), [kind, filter, q]);
1670
+ const data = products.data;
1671
+ const run = (data == null ? void 0 : data.run) ?? status.lastRuns.products ?? null;
1672
+ const stats = (run == null ? void 0 : run.stats) ?? null;
1673
+ const o = status.options;
1674
+ const supported = status.mode === "demo" || status.diagnostics.capabilities.includes("products");
1675
+ const ready = status.mode === "demo" || status.configured;
1676
+ const rows = (data == null ? void 0 : data.changes) ?? [];
1677
+ const summary = data == null ? void 0 : data.summary;
1678
+ const level = o.priceLevel || (stats == null ? void 0 : stats.level);
1679
+ const settings = t("products.settings", {
1680
+ level: level ? t("products.settingsLevel", { symbol: level }) : t("products.firstLevel"),
1681
+ type: t(`products.types.${o.priceType}`),
1682
+ target: o.priceTarget === "price_list" ? t("products.settingsTargets.price_list", { id: o.priceListId ?? "?" }) : t("products.settingsTargets.variant"),
1683
+ currency: o.priceCurrency.toUpperCase()
1684
+ });
1685
+ const onRead = async () => {
1686
+ try {
1687
+ const r = await sync.mutateAsync("products");
1688
+ if (r.alreadyRunning) ui.toast.info(t("toast.already"));
1689
+ else ui.toast.success(t("toast.started"));
1690
+ onAction();
1691
+ } catch (err) {
1692
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
1693
+ }
1694
+ };
1695
+ const onRelease = async (id) => {
1696
+ try {
1697
+ await release.mutateAsync(id);
1698
+ ui.toast.success(t("toast.released"));
1699
+ } catch (err) {
1700
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
1701
+ }
1702
+ };
1703
+ const counters = stats ? [
1704
+ ["subiektProducts", stats.subiektProducts, false],
1705
+ ["matched", stats.matched, false],
1706
+ ["priceChanges", stats.priceChanges, false],
1707
+ ["unchanged", stats.unchanged, false],
1708
+ ["toCreate", stats.toCreate, false],
1709
+ ["conflicts", stats.conflicts, true],
1710
+ ["unmatchedVariants", stats.unmatchedVariants, true],
1711
+ ["noPrice", stats.noPrice, true]
1712
+ ] : [];
1713
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1714
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionHeader, { title: t("products.title"), subtitle: t("products.subtitle"), children: [
1715
+ (run == null ? void 0 : run.dryRun) ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "purple", children: t("stock.dryRun") }) : null,
1716
+ run ? /* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: run.status }) : null,
1717
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "secondary", disabled: !ready || !supported || !o.productSyncEnabled || running, isLoading: sync.isPending, onClick: onRead, children: [
1718
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowPath, {}),
1719
+ running ? t("actions.running") : t("actions.products")
1720
+ ] })
1721
+ ] }),
1722
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1 px-6 py-3", children: [
1723
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: settings }),
1724
+ run ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
1725
+ runSummary(run, t),
1726
+ " ",
1727
+ fmtDateTime(run.startedAt, lang),
1728
+ ", ",
1729
+ fmtDuration(run.durationMs)
1730
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: t("products.never") })
1731
+ ] }),
1732
+ !o.productSyncEnabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "info", label: t("products.title"), children: t("products.disabled") }) }) : !supported ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "warning", label: t("products.title"), children: t("products.unsupported") }) }) : null,
1733
+ (stats == null ? void 0 : stats.blocked) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "warning", label: t("products.title"), children: t(`products.blocked.${stats.blocked}`) }) }) : null,
1734
+ counters.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-x-4 gap-y-2 px-6 py-4 sm:grid-cols-4", children: counters.map(([key, value, warn]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
1735
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: t(`products.counters.${key}`) }),
1736
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: ui.clx("tabular-nums", warn && value ? "text-ui-tag-orange-text" : void 0), children: fmtNumber(value ?? 0, lang) })
1737
+ ] }, key)) }) : null,
1738
+ (stats == null ? void 0 : stats.samples) && (((_a = stats.samples.conflicts) == null ? void 0 : _a.length) || ((_b = stats.samples.unmatchedVariants) == null ? void 0 : _b.length) || ((_c = stats.samples.noPrice) == null ? void 0 : _c.length)) ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
1739
+ ((_d = stats.samples.conflicts) == null ? void 0 : _d.length) ? /* @__PURE__ */ jsxRuntime.jsx(SampleList, { label: t("products.samples.conflicts"), items: stats.samples.conflicts }) : null,
1740
+ ((_e = stats.samples.unmatchedVariants) == null ? void 0 : _e.length) ? /* @__PURE__ */ jsxRuntime.jsx(SampleList, { label: t("products.samples.unmatchedVariants"), items: stats.samples.unmatchedVariants }) : null,
1741
+ ((_f = stats.samples.noPrice) == null ? void 0 : _f.length) ? /* @__PURE__ */ jsxRuntime.jsx(SampleList, { label: t("products.samples.noPrice"), items: stats.samples.noPrice }) : null
1742
+ ] }) : null,
1743
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 px-6 py-3", children: [
1744
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
1745
+ /* @__PURE__ */ jsxRuntime.jsx(
1746
+ FilterPills,
1747
+ {
1748
+ value: kind,
1749
+ onChange: setKind,
1750
+ options: [
1751
+ { value: "all", label: t("products.statuses.all") },
1752
+ { value: "price", label: t("products.kinds.price") },
1753
+ { value: "create", label: t("products.kinds.create") }
1754
+ ]
1755
+ }
1756
+ ),
1757
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full md:w-56", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { size: "small", type: "search", placeholder: t("products.search"), value: search, onChange: (e) => setSearch(e.target.value) }) })
1758
+ ] }),
1759
+ /* @__PURE__ */ jsxRuntime.jsx(
1760
+ FilterPills,
1761
+ {
1762
+ value: filter,
1763
+ onChange: setFilter,
1764
+ options: [
1765
+ { value: "all", label: t("products.statuses.all"), count: summary == null ? void 0 : summary.total },
1766
+ ...STATUS_FILTERS.filter((s) => ((summary == null ? void 0 : summary[s]) ?? 0) > 0 || s === filter).map((s) => ({ value: s, label: t(`products.statuses.${s}`), count: summary == null ? void 0 : summary[s] }))
1767
+ ]
1768
+ }
1769
+ )
1770
+ ] }),
1771
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1772
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1773
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("products.columns.product") }),
1774
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("products.columns.change") }),
1775
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: /* @__PURE__ */ jsxRuntime.jsx(StoreColumn, { label: t("products.columns.store"), hint: t("products.matching") }) }),
1776
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("products.columns.status") }),
1777
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("products.columns.details") })
1778
+ ] }) }),
1779
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Row, { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 5, className: "px-6 py-6 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: products.isLoading ? "" : t("products.empty") }) }) }) : rows.map((c) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { className: "[&_td]:py-2.5", children: [
1780
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "max-w-[240px]", children: /* @__PURE__ */ jsxRuntime.jsx(SubiektProductCell, { change: c }) }),
1781
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ChangeCell, { change: c, lang }) }),
1782
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "max-w-[300px]", children: /* @__PURE__ */ jsxRuntime.jsx(StoreProductCell, { change: c }) }),
1783
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(CatalogStatusBadge, { status: c.status }) }),
1784
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { className: "max-w-md", children: [
1785
+ c.lastError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: c.status === "failed" || c.status === "quarantined" ? "text-ui-tag-red-text" : "text-ui-fg-subtle", children: c.lastError }) : null,
1786
+ c.appliedAt ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: fmtDateTime(c.appliedAt, lang) }) : null
1787
+ ] })
1788
+ ] }, c.id)) })
1789
+ ] }) }),
1790
+ /* @__PURE__ */ jsxRuntime.jsx(Pager, { offset, limit: PAGE_SIZE$1, count: (data == null ? void 0 : data.count) ?? 0, onChange: setOffset }),
1791
+ data && data.quarantined.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 py-4", children: [
1792
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: t("products.quarantine") }),
1793
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: t("products.quarantineHint") }),
1794
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col divide-y", children: data.quarantined.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-start justify-between gap-x-4 py-2", children: [
1795
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col", children: [
1796
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "font-mono", children: [
1797
+ t(`products.kinds.${item.kind}`),
1798
+ ": ",
1799
+ item.key
1800
+ ] }),
1801
+ item.lastError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-tag-red-text", children: item.lastError }) : null
1802
+ ] }),
1803
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: release.isPending && release.variables === item.id, onClick: () => onRelease(item.id), children: t("actions.release") })
1804
+ ] }, item.id)) })
1805
+ ] }) : null
1806
+ ] });
1807
+ }
1808
+ const PAGE_SIZE = 15;
1809
+ const SETTINGS_TABS = ["connection", "bridge", "writers", "stock", "runs"];
1810
+ const SubiektPage = () => {
1811
+ const { t, i18n } = reactI18next.useTranslation("subiekt");
1812
+ const lang = i18n.language || "en";
1813
+ const client = reactQuery.useQueryClient();
1814
+ const nav = usePageNav(SETTINGS_TABS);
1815
+ const [pollUntil, setPollUntil] = react.useState(0);
1816
+ const status = useSubiektStatus(pollUntil);
1817
+ const s = status.data;
1818
+ const polling = ((s == null ? void 0 : s.running.length) ?? 0) > 0 || Date.now() < pollUntil;
1819
+ const runKey = JSON.stringify(Object.values((s == null ? void 0 : s.lastRuns) ?? {}).map((r) => (r == null ? void 0 : r.id) ?? ""));
1820
+ const seen = react.useRef(void 0);
1821
+ react.useEffect(() => {
1822
+ if (seen.current !== void 0 && seen.current !== runKey) {
1823
+ void client.invalidateQueries({ queryKey: subiektKeys.all, predicate: (q) => q.queryKey[1] !== "status" });
1824
+ }
1825
+ seen.current = runKey;
1826
+ }, [runKey, client]);
1827
+ const poll = () => setPollUntil(Date.now() + 3e4);
1828
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
1829
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1830
+ /* @__PURE__ */ jsxRuntime.jsx(Header, { status: s, loading: status.isLoading, lang, nav, onAction: poll }),
1831
+ status.isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "error", label: t("title"), children: errorMessage(status.error) }) }) : null,
1832
+ s ? /* @__PURE__ */ jsxRuntime.jsx(Warnings, { status: s }) : null,
1833
+ s && nav.view === "panel" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 divide-x divide-y md:grid-cols-4 xl:grid-cols-7 xl:divide-y-0", children: [
1834
+ /* @__PURE__ */ jsxRuntime.jsx(StatTile, { label: t("stats.sent24h"), value: fmtNumber(s.counts.succeeded24h, lang) }),
1835
+ /* @__PURE__ */ jsxRuntime.jsx(StatTile, { label: t("stats.waiting"), value: fmtNumber(s.counts.waiting, lang), tone: s.counts.waiting ? void 0 : "muted" }),
1836
+ /* @__PURE__ */ jsxRuntime.jsx(
1837
+ StatTile,
1838
+ {
1839
+ label: t("stats.queue"),
1840
+ value: fmtNumber(s.counts.pending + s.counts.running + s.counts.unknown, lang),
1841
+ tone: s.counts.pending + s.counts.running + s.counts.unknown ? void 0 : "muted"
1842
+ }
1843
+ ),
1844
+ /* @__PURE__ */ jsxRuntime.jsx(StatTile, { label: t("stats.attention"), value: fmtNumber(s.counts.failed, lang), tone: s.counts.failed ? "attention" : "muted" }),
1845
+ /* @__PURE__ */ jsxRuntime.jsx(StatTile, { label: t("stats.zk"), value: fmtNumber(s.counts.zk, lang) }),
1846
+ /* @__PURE__ */ jsxRuntime.jsx(StatTile, { label: t("stats.wz"), value: fmtNumber(s.counts.wz, lang) }),
1847
+ /* @__PURE__ */ jsxRuntime.jsx(StatTile, { label: t("stats.sales"), value: fmtNumber(s.counts.fs + s.counts.pa, lang), tone: s.counts.fs + s.counts.pa ? void 0 : "muted" })
1848
+ ] }) : null
1849
+ ] }),
1850
+ s && nav.view === "guide" ? /* @__PURE__ */ jsxRuntime.jsx(GuideView, { status: s, lang }) : null,
1851
+ nav.view === "panel" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1852
+ s ? /* @__PURE__ */ jsxRuntime.jsx(TasksSection, { status: s, lang, poll: polling, onAction: poll }) : null,
1853
+ /* @__PURE__ */ jsxRuntime.jsx(DocumentsSection, { lang, poll: polling }),
1854
+ s ? /* @__PURE__ */ jsxRuntime.jsx(ProductsSection, { status: s, lang, poll: polling, onAction: poll }) : null
1855
+ ] }) : null,
1856
+ nav.view === "settings" ? /* @__PURE__ */ jsxRuntime.jsxs(
1857
+ SettingsView,
1858
+ {
1859
+ title: t("settings.title"),
1860
+ subtitle: t("settings.subtitle"),
1861
+ value: nav.tab,
1862
+ onChange: (tab) => nav.go("settings", tab),
1863
+ tabs: [
1864
+ { id: "connection", label: t("settings.tab.connection") },
1865
+ { id: "bridge", label: t("settings.tab.bridge") },
1866
+ { id: "writers", label: t("settings.tab.writers"), badge: s == null ? void 0 : s.writers.filter((w) => w.active).length, tone: "orange" },
1867
+ { id: "stock", label: t("settings.tab.stock"), badge: (s == null ? void 0 : s.options.stockDryRun) ? t("stock.dryRun") : null, tone: "purple" },
1868
+ { id: "runs", label: t("settings.tab.runs") }
1869
+ ],
1870
+ children: [
1871
+ s && nav.tab === "connection" ? /* @__PURE__ */ jsxRuntime.jsx(ConnectionSection, { status: s, lang }) : null,
1872
+ s && nav.tab === "bridge" ? /* @__PURE__ */ jsxRuntime.jsx(BridgeSection, { status: s, lang }) : null,
1873
+ s && nav.tab === "writers" ? /* @__PURE__ */ jsxRuntime.jsx(WritersSection, { status: s, lang }) : null,
1874
+ s && nav.tab === "stock" ? /* @__PURE__ */ jsxRuntime.jsx(StockSection, { run: s.lastRuns.stock ?? null, lang }) : null,
1875
+ nav.tab === "runs" ? /* @__PURE__ */ jsxRuntime.jsx(RunsSection, { lang, poll: polling }) : null
1876
+ ]
1877
+ }
1878
+ ) : null
1879
+ ] });
1880
+ };
1881
+ function Header({
1882
+ status,
1883
+ loading,
1884
+ lang,
1885
+ nav,
1886
+ onAction
1887
+ }) {
1888
+ const { t } = reactI18next.useTranslation("subiekt");
1889
+ const sync = useSubiektSync();
1890
+ const check = useSubiektCheck();
1891
+ const ready = Boolean(status && (status.mode === "demo" || status.configured));
1892
+ const running = new Set((status == null ? void 0 : status.running) ?? []);
1893
+ const state = connectionState(status);
1894
+ const references = status ? referencesFor(status.references, lang) : [];
1895
+ const promptSpec = usePromptSpec();
1896
+ const community = communityLabels((key, options) => t(key, options), `${t("title")} ${t("by")}`);
1897
+ const start = async (what) => {
1898
+ try {
1899
+ const r = await sync.mutateAsync(what);
1900
+ if (r.alreadyRunning) ui.toast.info(t("toast.already"));
1901
+ else ui.toast.success(t("toast.started"));
1902
+ onAction();
1903
+ } catch (err) {
1904
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
1905
+ }
1906
+ };
1907
+ const onCheck = async () => {
1908
+ var _a, _b;
1909
+ try {
1910
+ const r = await check.mutateAsync();
1911
+ if ((_a = r.result) == null ? void 0 : _a.ok) ui.toast.success(t("toast.checkedOk"));
1912
+ else ui.toast.error(t("toast.checkedFail", { error: ((_b = r.result) == null ? void 0 : _b.error) ?? "?" }));
1913
+ } catch (err) {
1914
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
1915
+ }
1916
+ };
1917
+ const panel = nav.view !== "guide";
1918
+ const checkAction = { key: "check", label: t("actions.check"), loading: check.isPending, disabled: !ready, onClick: () => void onCheck() };
1919
+ return /* @__PURE__ */ jsxRuntime.jsx(
1920
+ IntegrationHeader,
1921
+ {
1922
+ icon: /* @__PURE__ */ jsxRuntime.jsx(SubiektIcon, { width: 28, height: 28 }),
1923
+ title: t("title"),
1924
+ by: t("by"),
1925
+ badges: !loading ? /* @__PURE__ */ jsxRuntime.jsx(ModeBadge, { color: state.tone, label: t(`mode.${state.key}`), title: t("demo.label"), children: (status == null ? void 0 : status.mode) === "demo" ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("demo.text") }) : null }) : null,
1926
+ description: t("subtitle"),
1927
+ social: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1928
+ /* @__PURE__ */ jsxRuntime.jsx(
1929
+ ReferencesBadge,
1930
+ {
1931
+ items: references,
1932
+ labels: {
1933
+ count: references.length === 1 ? t("references.badgeOne") : t("references.badgeMany", { count: references.length }),
1934
+ title: t("references.title"),
1935
+ subtitle: t("references.subtitle"),
1936
+ open: t("references.open"),
1937
+ review: t("references.review"),
1938
+ since: (since) => t("references.since", { date: monthYear(since, lang) }),
1939
+ rating: (value) => fmtRating(value, lang)
1940
+ }
1941
+ }
1942
+ ),
1943
+ /* @__PURE__ */ jsxRuntime.jsx(AddStoreButton, { labels: community.addStore })
1944
+ ] }),
1945
+ help: /* @__PURE__ */ jsxRuntime.jsx(HelpButtons, { spec: promptSpec, lang, labels: community }),
1946
+ view: nav.view,
1947
+ onView: (v) => nav.go(v),
1948
+ labels: { panel: t("view.panel"), guide: t("view.guide"), settings: t("settings.title"), more: t("actions.moreActions") },
1949
+ primary: panel ? {
1950
+ key: "stock",
1951
+ label: running.has("stock") ? t("actions.running") : t("actions.stock"),
1952
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowPath, {}),
1953
+ disabled: !ready || running.has("stock"),
1954
+ onClick: () => void start("stock")
1955
+ } : null,
1956
+ actions: panel ? [
1957
+ {
1958
+ key: "events",
1959
+ label: running.has("events") ? t("actions.running") : t("actions.events"),
1960
+ disabled: !ready || running.has("events"),
1961
+ onClick: () => void start("events")
1962
+ },
1963
+ checkAction
1964
+ ] : [checkAction]
1965
+ }
1966
+ );
1967
+ }
1968
+ function Warnings({ status }) {
1969
+ const { t } = reactI18next.useTranslation("subiekt");
1970
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1971
+ status.mode === "live" && status.missing.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "warning", label: t("missing.label"), children: t("missing.text", { missing: status.missing.join(", ") }) }) }) : null,
1972
+ status.optionWarnings.map((w) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "warning", label: t("optionWarnings.label"), children: t(`optionWarnings.${w}`, { defaultValue: w }) }) }, w))
1973
+ ] });
1974
+ }
1975
+ function ConnectionSection({ status, lang }) {
1976
+ var _a, _b, _c, _d, _e, _f, _g;
1977
+ const { t } = reactI18next.useTranslation("subiekt");
1978
+ const c = status.connection;
1979
+ const h = c.health;
1980
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1981
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
1982
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: t("connection.title") }),
1983
+ /* @__PURE__ */ jsxRuntime.jsx(ConnectionBadge, { status })
1984
+ ] }),
1985
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-2", children: [
1986
+ /* @__PURE__ */ jsxRuntime.jsxs(Field, { label: t("connection.bridge"), children: [
1987
+ h ? /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1988
+ (_a = h.bridge) == null ? void 0 : _a.name,
1989
+ " ",
1990
+ (_b = h.bridge) == null ? void 0 : _b.version,
1991
+ " ",
1992
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-ui-fg-muted", children: [
1993
+ "(",
1994
+ t("connection.contract", { version: (_c = h.bridge) == null ? void 0 : _c.contract }),
1995
+ ")"
1996
+ ] })
1997
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-muted", children: status.bridgeHost ?? t("connection.none") }),
1998
+ status.bridgeHost && h ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-ui-fg-muted", children: status.bridgeHost }) : null
1999
+ ] }),
2000
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("connection.subiekt"), children: (h == null ? void 0 : h.subiekt) ? [h.subiekt.product, h.subiekt.version].filter(Boolean).join(" ") || t("connection.none") : t("connection.none") }),
2001
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("connection.database"), children: ((_d = h == null ? void 0 : h.subiekt) == null ? void 0 : _d.database) ?? t("connection.none") }),
2002
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("connection.company"), children: ((_e = h == null ? void 0 : h.subiekt) == null ? void 0 : _e.company) ?? t("connection.none") }),
2003
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("connection.warehouse"), children: ((_f = h == null ? void 0 : h.subiekt) == null ? void 0 : _f.warehouse) ?? t("connection.none") }),
2004
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("connection.checkedAt"), children: fmtDateTime(c.checkedAt, lang) || t("connection.never") }),
2005
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("connection.eventsReadAt"), children: fmtDateTime(c.eventsReadAt, lang) || t("connection.never") })
2006
+ ] }),
2007
+ c.lastError && (!c.reachable || h && !((_g = h.subiekt) == null ? void 0 : _g.connected)) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "error", label: t("connection.lastError"), children: [
2008
+ c.lastError,
2009
+ c.consecutiveFailures > 1 ? ` (${t("connection.failures", { count: c.consecutiveFailures })})` : ""
2010
+ ] }) }) : null,
2011
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: t("connection.schedule") }) })
2012
+ ] });
2013
+ }
2014
+ function StockSection({ run, lang }) {
2015
+ const { t } = reactI18next.useTranslation("subiekt");
2016
+ const stats = (run == null ? void 0 : run.stats) ?? null;
2017
+ const counters = stats ? [
2018
+ ["subiektProducts", stats.subiektProducts, false],
2019
+ ["matchedItems", stats.matchedItems, false],
2020
+ ["toUpdate", stats.toUpdate, false],
2021
+ ["toCreate", stats.toCreate, false],
2022
+ ["unchanged", stats.unchanged, false],
2023
+ ["unmatchedVariants", stats.unmatchedVariants, true],
2024
+ ["unmatchedSubiekt", stats.unmatchedSubiekt, false],
2025
+ ["conflicts", stats.conflicts, true]
2026
+ ] : [];
2027
+ const samples = stats == null ? void 0 : stats.samples;
2028
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
2029
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-x-2 px-6 py-4", children: [
2030
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2031
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: t("stock.title") }),
2032
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: t("stock.subtitle") })
2033
+ ] }),
2034
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-x-2 whitespace-nowrap", children: [
2035
+ (run == null ? void 0 : run.dryRun) ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "purple", children: t("stock.dryRun") }) : null,
2036
+ run ? /* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: run.status }) : null
2037
+ ] })
2038
+ ] }),
2039
+ !run ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: t("stock.never") }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2040
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3", children: [
2041
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: runSummary(run, t) }),
2042
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
2043
+ fmtDateTime(run.startedAt, lang),
2044
+ ", ",
2045
+ fmtDuration(run.durationMs)
2046
+ ] })
2047
+ ] }),
2048
+ counters.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-x-4 gap-y-2 px-6 py-4 sm:grid-cols-4", children: counters.map(([key, value, warn]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
2049
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: t(`stock.counters.${key}`) }),
2050
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: warn && value ? "text-ui-tag-orange-text tabular-nums" : "tabular-nums", children: fmtNumber(value ?? 0, lang) })
2051
+ ] }, key)) }) : null,
2052
+ samples ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 px-6 py-4", children: [
2053
+ samples.changes && samples.changes.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2054
+ SampleList,
2055
+ {
2056
+ label: t("stock.samples.changes"),
2057
+ items: samples.changes.map((c) => `${c.sku ?? "?"}: ${c.from === null ? "-" : fmtNumber(c.from, lang)} → ${fmtNumber(c.to, lang)}`)
2058
+ }
2059
+ ) : null,
2060
+ samples.unmatchedVariants && samples.unmatchedVariants.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(SampleList, { label: t("stock.samples.unmatchedVariants"), items: samples.unmatchedVariants }) : null,
2061
+ samples.unmatchedSubiekt && samples.unmatchedSubiekt.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(SampleList, { label: t("stock.samples.unmatchedSubiekt"), items: samples.unmatchedSubiekt }) : null,
2062
+ samples.conflicts && samples.conflicts.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(SampleList, { label: t("stock.samples.conflicts"), items: samples.conflicts }) : null
2063
+ ] }) : null
2064
+ ] })
2065
+ ] });
2066
+ }
2067
+ function TaskDetails({ task, lang }) {
2068
+ const { t } = reactI18next.useTranslation("subiekt");
2069
+ const buyer = task.buyer;
2070
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-0.5", children: [
2071
+ task.lastError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: task.status === "failed" ? "text-ui-tag-red-text" : "text-ui-fg-subtle", children: [
2072
+ task.lastErrorCode ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono", children: [
2073
+ "[",
2074
+ task.lastErrorCode,
2075
+ "] "
2076
+ ] }) : null,
2077
+ task.lastError
2078
+ ] }) : null,
2079
+ task.status === "unknown" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-tag-orange-text", children: t("tasks.unknownHint") }) : null,
2080
+ task.manualAction ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-tag-orange-text", children: t("tasks.manual") }) : null,
2081
+ buyer ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: buyer.source === "retail" ? t("tasks.buyer.retail") : t(`tasks.buyer.${buyer.source}`, { name: buyer.name ?? buyer.symbol ?? "?", nip: buyer.nip ?? "?" }) }) : null,
2082
+ task.warnings.map((w) => /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-tag-orange-text", children: w }, w)),
2083
+ (task.status === "pending" || task.status === "unknown") && task.nextAttemptAt && task.attempts > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: t("tasks.nextAttempt", { time: fmtDateTime(task.nextAttemptAt, lang) }) }) : null
2084
+ ] });
2085
+ }
2086
+ function TasksSection({ status, lang, poll, onAction }) {
2087
+ var _a, _b;
2088
+ const { t } = reactI18next.useTranslation("subiekt");
2089
+ const [filter, setFilter] = react.useState(status.counts.failed > 0 ? "attention" : "all");
2090
+ const [search, setSearch] = react.useState("");
2091
+ const [offset, setOffset] = react.useState(0);
2092
+ const q = search.trim();
2093
+ const open = status.counts.waiting + status.counts.pending + status.counts.running + status.counts.unknown;
2094
+ const tasks = useSubiektTasks(filter, q, offset, PAGE_SIZE, poll || status.counts.pending + status.counts.running + status.counts.unknown > 0);
2095
+ const retry = useSubiektRetry();
2096
+ const rows = ((_a = tasks.data) == null ? void 0 : _a.tasks) ?? [];
2097
+ react.useEffect(() => setOffset(0), [filter, q]);
2098
+ const onRetry = async (task) => {
2099
+ try {
2100
+ await retry.mutateAsync(task.id);
2101
+ ui.toast.success(t("toast.retried"));
2102
+ onAction();
2103
+ } catch (err) {
2104
+ ui.toast.error(t("toast.error", { error: errorMessage(err) }));
2105
+ }
2106
+ };
2107
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
2108
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 px-6 py-4 md:flex-row md:items-center md:justify-between", children: [
2109
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2110
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: t("tasks.title") }),
2111
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "max-w-3xl text-ui-fg-subtle", title: t("tasks.technical"), children: t("tasks.subtitle") })
2112
+ ] }),
2113
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full md:w-56", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { size: "small", type: "search", placeholder: t("tasks.search"), value: search, onChange: (e) => setSearch(e.target.value) }) })
2114
+ ] }),
2115
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
2116
+ FilterPills,
2117
+ {
2118
+ value: filter,
2119
+ onChange: setFilter,
2120
+ options: [
2121
+ { value: "attention", label: t("tasks.filters.attention"), count: status.counts.failed },
2122
+ { value: "open", label: t("tasks.filters.open"), count: open },
2123
+ { value: "done", label: t("tasks.filters.done") },
2124
+ { value: "all", label: t("tasks.filters.all") }
2125
+ ]
2126
+ }
2127
+ ) }),
2128
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
2129
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
2130
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: /* @__PURE__ */ jsxRuntime.jsx(StoreColumn, { label: t("tasks.columns.order"), hint: t("store.orderMatching") }) }),
2131
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("tasks.columns.operation") }),
2132
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("tasks.columns.document") }),
2133
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("tasks.columns.status") }),
2134
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: t("tasks.columns.attempts") }),
2135
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("tasks.columns.details") }),
2136
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
2137
+ ] }) }),
2138
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Row, { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 7, className: "px-6 py-6 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: tasks.isLoading ? "" : t("tasks.empty") }) }) }) : rows.map((task) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { className: "[&_td]:py-2.5", children: [
2139
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "max-w-[260px]", children: /* @__PURE__ */ jsxRuntime.jsx(StoreOrderCell, { orderId: task.orderId, displayId: task.displayId }) }),
2140
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "whitespace-nowrap", children: task.kind === "order.document" && (task.documentKind === "fs" || task.documentKind === "pa") ? t(`tasks.documentKinds.${task.documentKind}`) : t(`tasks.kinds.${task.kind}`) }),
2141
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "whitespace-nowrap font-mono text-xs", children: task.documentNumber ?? "" }),
2142
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(TaskStatusBadge, { status: task.status }) }),
2143
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right tabular-nums", children: task.attempts }),
2144
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "max-w-md", children: /* @__PURE__ */ jsxRuntime.jsx(TaskDetails, { task, lang }) }),
2145
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: task.status === "failed" || task.status === "unknown" || task.status === "pending" && task.attempts > 0 || task.status === "waiting" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", isLoading: retry.isPending && retry.variables === task.id, onClick: () => onRetry(task), children: task.status === "waiting" ? t("actions.send") : t("actions.retry") }) : null })
2146
+ ] }, task.id)) })
2147
+ ] }) }),
2148
+ /* @__PURE__ */ jsxRuntime.jsx(Pager, { offset, limit: PAGE_SIZE, count: ((_b = tasks.data) == null ? void 0 : _b.count) ?? 0, onChange: setOffset })
2149
+ ] });
2150
+ }
2151
+ function DocumentsSection({ lang, poll }) {
2152
+ var _a, _b;
2153
+ const { t } = reactI18next.useTranslation("subiekt");
2154
+ const [kind, setKind] = react.useState("all");
2155
+ const [offset, setOffset] = react.useState(0);
2156
+ const docs = useSubiektDocuments(kind, "", offset, PAGE_SIZE, poll);
2157
+ const rows = ((_a = docs.data) == null ? void 0 : _a.documents) ?? [];
2158
+ react.useEffect(() => setOffset(0), [kind]);
2159
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
2160
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 px-6 py-4 md:flex-row md:items-center md:justify-between", children: [
2161
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2162
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: t("documents.title") }),
2163
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "max-w-3xl text-ui-fg-subtle", children: t("documents.subtitle") })
2164
+ ] }),
2165
+ /* @__PURE__ */ jsxRuntime.jsx(
2166
+ FilterPills,
2167
+ {
2168
+ value: kind,
2169
+ onChange: setKind,
2170
+ options: ["all", "ZK", "WZ", "FS", "PA"].map((k) => ({ value: k, label: t(`documents.filters.${k}`) }))
2171
+ }
2172
+ )
2173
+ ] }),
2174
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
2175
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
2176
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("documents.columns.number") }),
2177
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: /* @__PURE__ */ jsxRuntime.jsx(StoreColumn, { label: t("documents.columns.order"), hint: t("store.orderMatching") }) }),
2178
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("documents.columns.issued") }),
2179
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("documents.columns.source") }),
2180
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("documents.columns.ksef") }),
2181
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("documents.columns.status") })
2182
+ ] }) }),
2183
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Row, { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 6, className: "px-6 py-6 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: docs.isLoading ? "" : t("documents.empty") }) }) }) : rows.map((d) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { className: "[&_td]:py-2.5", children: [
2184
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { className: "whitespace-nowrap font-mono text-xs", children: [
2185
+ d.number,
2186
+ d.related.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-ui-fg-muted", children: [
2187
+ " ← ",
2188
+ d.related.map((r) => r.number).join(", ")
2189
+ ] }) : null
2190
+ ] }),
2191
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "max-w-[260px]", children: /* @__PURE__ */ jsxRuntime.jsx(StoreOrderCell, { orderId: d.orderId, displayId: d.displayId }) }),
2192
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "whitespace-nowrap", children: fmtDateTime(d.issuedAt, lang) }),
2193
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: t(`documents.sources.${d.source}`, { defaultValue: d.source }) }),
2194
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "whitespace-nowrap font-mono text-xs", children: d.ksefNumber ? d.ksefNumber : d.kind === "FS" && d.status !== "canceled" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-sans text-ui-fg-muted", children: t("documents.ksefWaiting") }) : "" }),
2195
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(DocumentStatusBadge, { status: d.status }) })
2196
+ ] }, d.id)) })
2197
+ ] }) }),
2198
+ /* @__PURE__ */ jsxRuntime.jsx(Pager, { offset, limit: PAGE_SIZE, count: ((_b = docs.data) == null ? void 0 : _b.count) ?? 0, onChange: setOffset })
2199
+ ] });
2200
+ }
2201
+ function RunsSection({ lang, poll }) {
2202
+ var _a;
2203
+ const { t } = reactI18next.useTranslation("subiekt");
2204
+ const runs = useSubiektRuns(poll);
2205
+ const rows = ((_a = runs.data) == null ? void 0 : _a.runs) ?? [];
2206
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
2207
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: t("runs.title") }) }),
2208
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
2209
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
2210
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("runs.columns.kind") }),
2211
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("runs.columns.trigger") }),
2212
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("runs.columns.status") }),
2213
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("runs.columns.message") }),
2214
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: t("runs.columns.started") }),
2215
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: t("runs.columns.duration") })
2216
+ ] }) }),
2217
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Row, { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 6, className: "px-6 py-6 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: runs.isLoading ? "" : t("runs.empty") }) }) }) : rows.map((r) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { className: "[&_td]:py-2.5", children: [
2218
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "whitespace-nowrap", children: t(`runs.kinds.${r.kind}`) }),
2219
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "whitespace-nowrap text-ui-fg-subtle", children: t(`runs.triggers.${r.trigger}`, { defaultValue: r.trigger }) }),
2220
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: r.status }) }),
2221
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "max-w-lg", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: runSummary(r, t) }) }),
2222
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "whitespace-nowrap", children: fmtDateTime(r.startedAt, lang) }),
2223
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right tabular-nums", children: fmtDuration(r.durationMs) })
2224
+ ] }, r.id)) })
2225
+ ] }) })
2226
+ ] });
2227
+ }
2228
+ const config = adminSdk.defineRouteConfig({
2229
+ label: "Subiekt nexo",
2230
+ icon: SubiektIcon
2231
+ });
2232
+ const en = {
2233
+ title: "Subiekt nexo",
2234
+ by: "by Koda Plus",
2235
+ subtitle: "Orders become ZK documents in Subiekt nexo PRO, the WZ issued in the warehouse comes back to the order, sales documents carry their KSeF number, and stock and prices flow from Subiekt to Medusa.",
2236
+ view: { panel: "Panel", guide: "Setup guide" },
2237
+ settings: {
2238
+ title: "Settings",
2239
+ subtitle: "The technical side of the integration: the connection to the bridge and its diagnostics, the writers, the stock sync and the history of background jobs.",
2240
+ tab: {
2241
+ connection: "Connection",
2242
+ bridge: "Bridge",
2243
+ writers: "Writers",
2244
+ stock: "Stock",
2245
+ runs: "History"
2246
+ }
2247
+ },
2248
+ mode: {
2249
+ demo: "Demo bridge",
2250
+ connected: "Connected",
2251
+ subiektDown: "Subiekt not answering",
2252
+ unreachable: "Bridge unreachable",
2253
+ notConfigured: "Not configured",
2254
+ unknown: "Not checked yet"
2255
+ },
2256
+ actions: {
2257
+ moreActions: "More actions",
2258
+ check: "Check connection",
2259
+ checking: "Checking",
2260
+ stock: "Sync stock",
2261
+ events: "Read documents",
2262
+ tasks: "Send queue now",
2263
+ products: "Read products",
2264
+ running: "Running",
2265
+ retry: "Send again",
2266
+ send: "Send to Subiekt",
2267
+ openPanel: "Subiekt panel",
2268
+ release: "Release"
2269
+ },
2270
+ demo: {
2271
+ label: "Demo mode",
2272
+ text: 'A simulated bridge inside Medusa issues ZK, WZ, FS and PA numbers: new orders get a ZK within seconds, the warehouse "issues" a WZ about three minutes later, the invoice or receipt follows, and an invoice gets its KSeF number two minutes after that. Stock and prices are computed from your catalog; writers armed here only simulate, your catalog stays untouched. To connect a real Subiekt, open the setup guide.'
2273
+ },
2274
+ missing: {
2275
+ label: "Configuration",
2276
+ text: "Missing plugin options: {{missing}}. bridgeUrl is the public address of the bridge (for example a Cloudflare Tunnel), secret is the same random string in Medusa and in the bridge (openssl rand -hex 32)."
2277
+ },
2278
+ optionWarnings: {
2279
+ label: "Option to fix",
2280
+ priceListId: "priceTarget is price_list but priceListId is not a price list id (plist_...). The price writer stays off until it is."
2281
+ },
2282
+ stats: {
2283
+ sent24h: "Sent in 24 h",
2284
+ waiting: "Waiting for payment",
2285
+ queue: "In the queue",
2286
+ attention: "Need attention",
2287
+ zk: "ZK documents",
2288
+ wz: "WZ documents",
2289
+ sales: "FS and PA"
2290
+ },
2291
+ store: {
2292
+ openOrder: "Open order",
2293
+ openProduct: "Open product",
2294
+ findProduct: "Find product",
2295
+ orderMatching: "Every document Subiekt issues for an order keeps the order's id in its notes, as [medusa:order_...]: that is how the document finds its order.",
2296
+ noOrder: "No order in the store",
2297
+ noOrderHint: "Subiekt sent this document without an order tag in its notes, so it belongs to no order here. A WZ typed by hand, instead of issued from the ZK, has no tag.",
2298
+ noProduct: "No product in the store",
2299
+ notInStore: "Not in the store yet",
2300
+ notInStoreHint: "Only in Subiekt so far. When the writer for new products is on, it is created in the store as a draft.",
2301
+ draft: "Created in the store as a draft, with this SKU."
2302
+ },
2303
+ connection: {
2304
+ title: "Connection",
2305
+ bridge: "Bridge",
2306
+ contract: "contract {{version}}",
2307
+ subiekt: "Subiekt",
2308
+ database: "Database",
2309
+ company: "Company",
2310
+ warehouse: "ZK warehouse",
2311
+ checkedAt: "Last check",
2312
+ lastError: "Last error",
2313
+ failures: "{{count}} failed attempt(s) in a row",
2314
+ never: "never",
2315
+ none: "none",
2316
+ eventsReadAt: "Documents read",
2317
+ schedule: "Queue every minute, documents every 2 minutes, stock every 10 minutes, products every hour."
2318
+ },
2319
+ bridge: {
2320
+ title: "Bridge",
2321
+ subtitle: "What the last check of the bridge said. Click Check connection for a fresh one.",
2322
+ versions: "Versions",
2323
+ versionsValue: "bridge {{bridge}}, contract {{contract}}, plugin {{plugin}}",
2324
+ sdk: "nexo SDK and database",
2325
+ sdkValue: "SDK {{sdk}}, database {{db}}",
2326
+ licence: "nexo licence",
2327
+ licenceStates: { ok: "accepted the last login", refused: "refused work (trial over or expired)", unknown: "not known yet" },
2328
+ started: "Running since",
2329
+ fakeMode: "The bridge runs in fake mode (Bridge:Mode fake): nothing reaches Subiekt. Set Mode to sfera on the bridge machine.",
2330
+ latency: "Round trip",
2331
+ skew: "Clock skew",
2332
+ skewStates: {
2333
+ ok: "{{value}}, fine",
2334
+ warn: "{{value}}, fix the clock: signatures fail beyond 5 minutes",
2335
+ error: "{{value}}, every signature fails: set the time on both machines",
2336
+ unknown: "not measured yet"
2337
+ },
2338
+ signature: "Signatures",
2339
+ signatureStates: {
2340
+ ok: "accepted by the bridge",
2341
+ invalid_signature: "rejected: the secret differs between Medusa and the bridge",
2342
+ stale_timestamp: "rejected: the clocks differ by more than 5 minutes",
2343
+ forbidden: "refused by Cloudflare Access: check the service token",
2344
+ unreachable: "no answer from the bridge",
2345
+ unknown: "not checked yet"
2346
+ },
2347
+ lastEvent: "Last event",
2348
+ lastEventValue: "#{{id}}, {{time}}",
2349
+ queues: "Waiting in the bridge",
2350
+ queuesValue: "{{sfera}} Sfera operation(s), {{webhook}} webhook(s)",
2351
+ webhook: "Webhook from the bridge",
2352
+ webhookValue: "last accepted {{time}}",
2353
+ webhookNever: "none yet (optional: Medusa reads the feed every 2 minutes)",
2354
+ webhookRejected: "last rejected {{time}}: {{reason}}",
2355
+ webhookReasons: {
2356
+ stale: "the clocks differ by more than 5 minutes",
2357
+ mismatch: "the secret differs from the bridge",
2358
+ missing: "the request had no signature",
2359
+ malformed: "the signature header was malformed",
2360
+ no_secret: "no secret is configured"
2361
+ },
2362
+ capabilities: "The bridge does",
2363
+ cannot: "Not available yet",
2364
+ legacy: "This bridge speaks contract 1.0: products, contractors, sales documents and KSeF numbers need bridge 0.2.0 (contract 1.1). The rest works as before.",
2365
+ reasons: {
2366
+ no_health: "check the connection first",
2367
+ update_bridge: "update the bridge to 0.2.0 or newer",
2368
+ bridge_config: "switched off in the bridge configuration"
2369
+ },
2370
+ capabilityNames: {
2371
+ orders: "Orders (ZK)",
2372
+ fulfillments: "WZ from Medusa",
2373
+ stock: "Stock",
2374
+ events: "Event feed",
2375
+ products: "Products and prices",
2376
+ contractors: "Contractors by NIP",
2377
+ "contractors.create": "New contractors",
2378
+ "documents.fs": "Invoices (FS)",
2379
+ "documents.pa": "Receipts (PA)",
2380
+ "documents.ksef": "KSeF numbers",
2381
+ webhook: "Webhook to Medusa"
2382
+ },
2383
+ capabilityHints: {
2384
+ contractors: 'Bridge:Subiekt:BuyerMode "customer"',
2385
+ "contractors.create": 'Bridge:Subiekt:BuyerMode "customer" and Bridge:Subiekt:CreateContractors true',
2386
+ "documents.fs": "Bridge:Documents:Fs true",
2387
+ "documents.pa": "Bridge:Documents:Pa true",
2388
+ "documents.ksef": "Bridge:Documents:Fs true",
2389
+ webhook: "Bridge:Medusa:WebhookUrl",
2390
+ products: "",
2391
+ orders: "",
2392
+ fulfillments: "",
2393
+ stock: "",
2394
+ events: ""
2395
+ }
2396
+ },
2397
+ writers: {
2398
+ title: "Writers",
2399
+ subtitle: "Every write added in 0.2.0 needs its option in medusa-config.ts and a person arming it here. An option set to false wins.",
2400
+ demoNote: "Demo: armed writers only simulate. Nothing in this store changes.",
2401
+ names: {
2402
+ prices: "Prices from Subiekt",
2403
+ products: "New products from Subiekt",
2404
+ documents: "Sales documents (FS, PA)",
2405
+ contractors: "New contractors in Subiekt"
2406
+ },
2407
+ descriptions: {
2408
+ prices: "Applies the price plan to Medusa, at most {{cap}} changes a run.",
2409
+ products: "Creates draft products for Subiekt products missing in Medusa, at most {{cap}} a run.",
2410
+ documents: "Issues the invoice or receipt of each order in Subiekt ({{mode}}).",
2411
+ contractors: "Lets the bridge create a contractor when no one in Subiekt has the buyer's NIP."
2412
+ },
2413
+ option: "Option {{option}}",
2414
+ allowed: "allows it",
2415
+ forbidden: "does not allow it",
2416
+ armed: "Armed",
2417
+ disarmed: "Off",
2418
+ active: "Writing",
2419
+ inactive: "Not writing",
2420
+ unsupported: "The bridge cannot do this yet.",
2421
+ changed: "{{who}}, {{time}}",
2422
+ armedBy: "Armed by {{who}}, {{time}}",
2423
+ disarmedBy: "Switched off by {{who}}, {{time}}",
2424
+ never: "Never armed",
2425
+ blocked: "Armed, but not writing",
2426
+ armedToast: "Armed.",
2427
+ disarmedToast: "Switched off.",
2428
+ armTitle: "Arm: {{name}}?",
2429
+ disarmTitle: "Switch off: {{name}}?",
2430
+ armText: "From now on this writer changes data for real, within its limits. You can switch it off here at any time, and the change is recorded with your name.",
2431
+ armTextDemo: "Demo: the writer only simulates. Nothing in this store or in any Subiekt changes.",
2432
+ disarmText: "The writer stops before its next item. Plans are still computed and shown.",
2433
+ armConfirm: "Arm",
2434
+ disarmConfirm: "Switch off",
2435
+ cancel: "Cancel",
2436
+ documentModes: {
2437
+ none: "none",
2438
+ fs: "always an FS",
2439
+ pa: "always a PA",
2440
+ auto: "an FS for a valid NIP, a PA otherwise"
2441
+ }
2442
+ },
2443
+ products: {
2444
+ title: "Products from Subiekt",
2445
+ subtitle: "Prices from Subiekt next to the store products they update, and Subiekt products the store does not sell yet. Everything is a plan first: nothing changes until someone switches its writer on in Settings.",
2446
+ matching: "Subiekt products are matched to store variants by EAN first, then by the SKU against the Subiekt symbol. A code used twice is a conflict and is never guessed.",
2447
+ matchedKey: "Matched by {{by}}: {{code}}",
2448
+ settings: "Prices from {{level}} ({{type}}) to {{target}}, in {{currency}}.",
2449
+ settingsLevel: "level {{symbol}}",
2450
+ settingsTargets: { variant: "variant prices", price_list: "price list {{id}}" },
2451
+ types: { gross: "gross", net: "net" },
2452
+ firstLevel: "the first price level",
2453
+ never: "No products read yet.",
2454
+ unsupported: "The bridge does not send products yet: update it to 0.2.0 (contract 1.1).",
2455
+ disabled: "Products from Subiekt are off (productSyncEnabled: false).",
2456
+ counters: {
2457
+ subiektProducts: "Products in Subiekt",
2458
+ matched: "Matched",
2459
+ priceChanges: "Price changes",
2460
+ unchanged: "Already right",
2461
+ toCreate: "To create",
2462
+ conflicts: "Conflicts",
2463
+ unmatchedVariants: "Variants without a match",
2464
+ noPrice: "Without a price"
2465
+ },
2466
+ kinds: { price: "Prices", create: "New products" },
2467
+ statuses: {
2468
+ all: "All",
2469
+ planned: "Planned",
2470
+ applied: "Applied",
2471
+ simulated: "Simulated",
2472
+ over_cap: "Next run",
2473
+ failed: "Failed",
2474
+ quarantined: "Quarantined",
2475
+ stale: "Changed meanwhile",
2476
+ skipped: "Skipped"
2477
+ },
2478
+ columns: { product: "Product in Subiekt", change: "Change", store: "Product in the store", matched: "Matched by", status: "Status", details: "Details" },
2479
+ matchedBy: { ean: "EAN", sku: "SKU" },
2480
+ newProduct: "new, {{price}}",
2481
+ empty: "Nothing in this plan.",
2482
+ search: "Symbol, SKU or EAN",
2483
+ quarantine: "Quarantined after repeated failures",
2484
+ quarantineHint: "The writer skips these until you release them. Fix the cause first (the last error says what failed).",
2485
+ samples: { conflicts: "Conflicts, never guessed", unmatchedVariants: "Variants without a match (SKU)", noPrice: "Without a price in this level" },
2486
+ blocked: {
2487
+ currency_mismatch: "The price level is in another currency than priceCurrency: nothing was planned.",
2488
+ no_level: "The configured priceLevel does not exist in the bridge: nothing was planned."
2489
+ }
2490
+ },
2491
+ tasks: {
2492
+ title: "Queue",
2493
+ subtitle: "Orders on their way to Subiekt: every ZK, WZ, invoice or receipt the store asked for, and how it went. A send that failed waits here for you, with a Send again button.",
2494
+ technical: "Every call Medusa owes the bridge, with retries. A failed task waits for a person.",
2495
+ filters: { attention: "Need attention", open: "Open", done: "Done", all: "All" },
2496
+ search: "Order number",
2497
+ empty: "Nothing here.",
2498
+ columns: { order: "Order in the store", operation: "Operation", status: "Status", attempts: "Attempts", document: "Document in Subiekt", details: "Details", actions: "" },
2499
+ kinds: { "order.create": "Create ZK", "order.cancel": "Cancel ZK", "order.fulfill": "Issue WZ", "order.document": "Issue sales document" },
2500
+ documentKinds: { fs: "Issue FS", pa: "Issue PA" },
2501
+ statuses: {
2502
+ waiting: "Waiting for payment",
2503
+ pending: "Queued",
2504
+ running: "Sending",
2505
+ unknown: "Answer unclear",
2506
+ succeeded: "Done",
2507
+ failed: "Needs attention",
2508
+ canceled: "Canceled"
2509
+ },
2510
+ unknownHint: "The bridge may have issued it. The next attempt asks the bridge first and never issues a second one.",
2511
+ nextAttempt: "Next attempt {{time}}",
2512
+ manual: "Finish by hand in Subiekt",
2513
+ buyer: { existing: "Buyer {{name}} (NIP {{nip}}), found in Subiekt", created: "Buyer {{name}} (NIP {{nip}}), created in Subiekt", retail: "Retail buyer" },
2514
+ page: "{{from}}-{{to}} of {{count}}",
2515
+ prev: "Previous",
2516
+ next: "Next"
2517
+ },
2518
+ documents: {
2519
+ title: "Documents",
2520
+ subtitle: "What Subiekt issued for your orders: ZK, WZ, invoices and receipts, each next to its order in the store.",
2521
+ filters: { all: "All", ZK: "ZK", WZ: "WZ", FS: "FS", PA: "PA" },
2522
+ empty: "No documents yet.",
2523
+ columns: { number: "Number", order: "Order in the store", issued: "Issued", source: "Source", status: "Status", ksef: "KSeF number" },
2524
+ sources: { bridge: "Medusa", subiekt: "Warehouse", demo: "Demo" },
2525
+ statuses: { open: "Open", canceled: "Canceled", completed: "Completed" },
2526
+ ksefWaiting: "waiting for KSeF"
2527
+ },
2528
+ stock: {
2529
+ title: "Stock",
2530
+ subtitle: "Subiekt to Medusa, by EAN first, then SKU against the product symbol.",
2531
+ never: "No stock sync yet.",
2532
+ dryRun: "Dry run",
2533
+ counters: {
2534
+ subiektProducts: "Products in Subiekt",
2535
+ matchedItems: "Matched",
2536
+ toUpdate: "To update",
2537
+ toCreate: "To create",
2538
+ unchanged: "Already right",
2539
+ unmatchedSubiekt: "Only in Subiekt",
2540
+ unmatchedVariants: "Variants without a match",
2541
+ conflicts: "Conflicts"
2542
+ },
2543
+ samples: {
2544
+ changes: "Changes",
2545
+ unmatchedVariants: "Variants without a match (SKU)",
2546
+ unmatchedSubiekt: "Only in Subiekt (symbol)",
2547
+ conflicts: "Conflicts, left untouched"
2548
+ }
2549
+ },
2550
+ runs: {
2551
+ title: "History",
2552
+ empty: "No runs yet.",
2553
+ columns: { kind: "Job", trigger: "Trigger", status: "Result", message: "Summary", started: "Started", duration: "Time" },
2554
+ kinds: { stock: "Stock", events: "Documents", tasks: "Queue", health: "Connection", products: "Products" },
2555
+ triggers: { schedule: "Schedule", manual: "Manual", webhook: "Webhook", event: "Event", auto: "Automatic" },
2556
+ statuses: { success: "OK", partial: "Partial", error: "Error", skipped: "Skipped" },
2557
+ summary: {
2558
+ stockDry: "Dry run: {{changes}} level(s) would change, {{unchanged}} already right.",
2559
+ stock: "{{changes}} level(s) updated, {{unchanged}} already right.",
2560
+ tasks: "{{succeeded}} sent.",
2561
+ tasksIssues: "{{succeeded}} sent, {{retry}} to retry, {{failed}} need attention.",
2562
+ events: "{{applied}} new document(s) from Subiekt.",
2563
+ eventsNone: "Nothing new.",
2564
+ health: "Bridge and Subiekt answer.",
2565
+ productsPlan: "Plan: {{prices}} price change(s), {{creates}} new product(s). No writer armed.",
2566
+ productsApplied: "{{prices}} price change(s), {{creates}} new product(s) planned, {{applied}} applied, {{failed}} failed."
2567
+ }
2568
+ },
2569
+ widget: {
2570
+ title: "Subiekt nexo",
2571
+ demo: "Demo",
2572
+ more: "Panel",
2573
+ none: "This order has not been sent to Subiekt.",
2574
+ waiting: "The ZK will be created when the payment is captured.",
2575
+ send: "Send to Subiekt now",
2576
+ sent: "Queued. The ZK appears here in a few seconds.",
2577
+ ksef: "KSeF {{number}}",
2578
+ ksefWaiting: "KSeF number not assigned yet",
2579
+ issueFs: "Issue invoice (FS)",
2580
+ issuePa: "Issue receipt (PA)",
2581
+ issued: "Queued. The document appears here in a few seconds.",
2582
+ autoOff: "Automatic sales documents are off: arm the writer on the Subiekt page.",
2583
+ autoOn: "The sales document is issued automatically after the {{after}}."
2584
+ },
2585
+ toast: {
2586
+ started: "Started. Results appear here in a moment.",
2587
+ already: "Already running.",
2588
+ checkedOk: "Bridge and Subiekt answer.",
2589
+ checkedFail: "No connection: {{error}}",
2590
+ retried: "Queued again.",
2591
+ released: "Released. The next run tries it again.",
2592
+ error: "Something went wrong: {{error}}"
2593
+ },
2594
+ references: {
2595
+ title: "Running in stores built by Koda Plus",
2596
+ subtitle: "Live stores implemented by Koda Plus that run on this integration every day.",
2597
+ open: "Open the store",
2598
+ since: "Since {{date}}",
2599
+ badgeOne: "Running in 1 store",
2600
+ badgeMany: "Running in {{count}} stores",
2601
+ review: "Read the review"
2602
+ },
2603
+ community: {
2604
+ addStore: {
2605
+ button: "Add your store",
2606
+ title: "Does your store run on this integration?",
2607
+ text: "Tell us about it. Once we check it, the store joins this list with a link and its rating.",
2608
+ name: "Store name",
2609
+ url: "Store address",
2610
+ note: "A few words about the setup (optional)",
2611
+ send: "Send the request",
2612
+ copy: "Copy the text",
2613
+ copied: "Request copied, paste it into a mail to hello@koda.plus",
2614
+ hint: "Your mail app opens with the message to {{email}} ready to send.",
2615
+ subject: "A store for {{integration}}",
2616
+ greeting: "Hello, my store runs on {{integration}}. Please add it to the list of stores."
2617
+ },
2618
+ prompt: {
2619
+ button: "Copy prompt",
2620
+ title: "Add this integration to your store",
2621
+ text: "Paste it into Claude Code, Cursor or another AI agent opened in your Medusa project. The agent installs the plugin, sets it up like here and leaves notes for further work.",
2622
+ copy: "Copy again",
2623
+ copied: "Prompt copied",
2624
+ failed: "Could not copy. Select the text and copy it by hand."
2625
+ },
2626
+ discord: {
2627
+ button: "Help on Discord",
2628
+ title: "Join the Koda Plus server on Discord"
2629
+ }
2630
+ },
2631
+ guide: {
2632
+ intro: {
2633
+ title: "Connect Medusa to Subiekt nexo PRO",
2634
+ text: "Subiekt has no web API: its programming interface, Sfera, is a Windows library next to the Subiekt database. So a small program, the bridge, runs as a Windows service on a machine that sees the Subiekt SQL Server, and a Cloudflare Tunnel publishes it over HTTPS without opening a port. Medusa signs every request, the bridge creates the ZK of each order, Medusa reads back the WZ, the invoice or receipt with its KSeF number, stock and prices. Nothing is written until you allow it.",
2635
+ timeLabel: "Time",
2636
+ time: "2 to 4 hours of setup, then a few days of test orders and plans before you arm the writers.",
2637
+ needsLabel: "You need",
2638
+ needs: [
2639
+ "Subiekt nexo PRO (Sfera is included)",
2640
+ "Windows machine on all the time, next to the database",
2641
+ "nexo SDK of your Subiekt version",
2642
+ ".NET 8 SDK",
2643
+ "A Subiekt operator for the bridge",
2644
+ "A domain in Cloudflare",
2645
+ "The bridge from Koda Plus",
2646
+ "Access to medusa-config.ts and its environment"
2647
+ ]
2648
+ },
2649
+ diagram: {
2650
+ title: "How the parts talk",
2651
+ subtitle: "Only Medusa starts requests to the bridge. The bridge may nudge Medusa when new documents wait; the documents themselves always come through Medusa's own signed request.",
2652
+ nodes: [
2653
+ { title: "Medusa", caption: "this plugin: queue, plans, writers, admin. Hosted anywhere, for example Railway." },
2654
+ { title: "Cloudflare Tunnel", caption: "publishes the bridge at https://subiekt-bridge.your-shop.pl without an open port. A reverse proxy with HTTPS works too." },
2655
+ { title: "Bridge (Windows service)", caption: "KodaSubiektBridge on 127.0.0.1:5280, one Sfera session, its own event feed." },
2656
+ { title: "Sfera (nexo SDK)", caption: "InsERT's .NET interface, the same version as the database." },
2657
+ { title: "Subiekt nexo PRO", caption: "documents, contractors, stock and prices in its SQL Server database." }
2658
+ ],
2659
+ links: ["HTTPS, every request signed", "cloudflared", "inside the bridge", "SQL Server, Windows login"]
2660
+ },
2661
+ domain: "your-shop.pl",
2662
+ demoNote: "Demo mode: the states and ticks below come from the simulated bridge. Connected to a real Subiekt, they come from yours.",
2663
+ links: {
2664
+ sdk: "Subiekt nexo PRO at InsERT (nexo SDK download)",
2665
+ dotnet: ".NET 8 download",
2666
+ tunnel: "Cloudflare: create a tunnel",
2667
+ access: "Cloudflare: service tokens"
2668
+ },
2669
+ steps: {
2670
+ title: "From zero to production",
2671
+ subtitle: "Each step turns green by itself when Medusa sees it working.",
2672
+ checkLabel: "Check:",
2673
+ copy: "Copy",
2674
+ copied: "Copied.",
2675
+ states: { done: "Done", todo: "To do", optional: "Optional", later: "At go-live" },
2676
+ subiekt: {
2677
+ title: "Prepare Subiekt nexo PRO",
2678
+ body: [
2679
+ 'The bridge works through Sfera, which InsERT calls "Sfera dla Subiekta nexo" and includes in Subiekt nexo PRO. Sfera has no licence of its own to buy or activate: an active Subiekt nexo PRO licence is enough. Plain Subiekt nexo has no Sfera and needs the PRO version (from InsERT or its partner); the separate Sfera PRO+ add-on (event sphere, spherical menu) is not needed.',
2680
+ "In Subiekt create an operator only for the bridge, for example `Integracja`, with rights to customer orders, external issues (WZ), sales documents, products and contractors. Write down its password.",
2681
+ "Decide the warehouse for new ZK (its symbol, for example `MAG`) and the retail buyer: the contractor every ZK without a company goes to. Note its NIP. For prices, note the symbol of the price level to publish and make sure its base price list is approved.",
2682
+ "Document series: ZK and WZ get the numbering of that warehouse (for example `ZK 128/MAG/2026`), and every document uses the default definition of its type in Subiekt, so FS and PA keep the numbering your accountant set. VAT comes from the product cards in Subiekt: the bridge sets the gross price the customer paid on each line and Subiekt computes net and VAT with the product's rate, so keep those rates in line with the store's tax settings.",
2683
+ "Note the exact version of Subiekt (its About window shows it): the nexo SDK must have the same one."
2684
+ ],
2685
+ check: "Subiekt shows the version, the operator can log in, the retail buyer exists."
2686
+ },
2687
+ sdk: {
2688
+ title: "Install the nexo SDK and .NET 8 on the Windows machine",
2689
+ body: [
2690
+ "Pick a Windows machine that runs all the time and reaches the Subiekt SQL Server, usually the server Subiekt itself runs on. A laptop that sleeps loses orders for as long as it sleeps (Medusa retries, but nothing moves).",
2691
+ "Install the .NET 8 SDK and the nexo SDK of your Subiekt version (InsERT publishes it on the Subiekt nexo PRO page). It installs to `C:\\InsERT\\nexoSDK\\Bin\\nexoSDK_<version>`. A different version than the database is the most common failure: Sfera refuses to connect and says the database is in another version."
2692
+ ],
2693
+ check: "The folder C:\\InsERT\\nexoSDK\\Bin has a nexoSDK_<version> folder with the same version as Subiekt."
2694
+ },
2695
+ service: {
2696
+ title: "Install the bridge as a Windows service",
2697
+ body: [
2698
+ "Copy the bridge from Koda Plus to the machine, for example `C:\\Koda\\subiekt-nexo-bridge`. Open PowerShell as administrator in that folder and run `deploy\\install-service.ps1` with `-ServiceUser`, the Windows account the service will run as (not LocalSystem: SQL Server accepts the Windows login of that account). The first run publishes the bridge to `C:\\KodaSubiektBridge`, creates `appsettings.Local.json` there from a template and stops: fill in every value that starts with `WSTAW-`.",
2699
+ "The keys, all under `Bridge`: `Mode` sfera; `Secret` the shared secret (the first line below generates one); `Subiekt:Server` the SQL Server instance, for example `SERWER\\INSERTNEXO`; `Subiekt:Database` the company database; `Subiekt:Operator` and `Subiekt:OperatorPassword`; `Subiekt:Warehouse` for new ZK; `Subiekt:StockWarehouses` whose stock goes to Medusa; `Subiekt:BuyerNip` the retail buyer; `Subiekt:BuyerMode` fixed, or customer to use the buyer's NIP; `Subiekt:CreateContractors`; `Products:PriceLevels`; `Documents:Fs` and `Documents:Pa`; `Medusa:WebhookUrl` (optional).",
2700
+ "Run the same command again. It publishes the bridge against the newest nexo SDK installed, runs `--check` (the configuration, then a read-only Sfera login with the warehouse, the retail buyer and the price levels), asks for the password of the service account, creates the KodaSubiektBridge service with a restart on failure, starts it and calls /healthz.",
2701
+ "Upgrades: `deploy\\upgrade-service.ps1` (backup, publish, check, start; with -Rollback it restores the backup by itself when the check fails). Removal: `deploy\\uninstall-service.ps1`, which keeps the configuration, the event feed and the logs."
2702
+ ],
2703
+ check: "--check ends with Ready, the script with /healthz = ok, and http://127.0.0.1:5280/ on that machine shows the status page."
2704
+ },
2705
+ tunnel: {
2706
+ title: "Publish the bridge through Cloudflare Tunnel",
2707
+ body: [
2708
+ "In the Cloudflare dashboard open Networking, Tunnels, and choose Create a tunnel. Name it (for example `subiekt-bridge`), choose Windows, and run the install command it shows in an administrator terminal on the bridge machine. cloudflared then runs as a Windows service.",
2709
+ "On the tunnel open Routes, Add route, Published application. Subdomain `subiekt-bridge`, your domain, Service URL `http://127.0.0.1:5280`. Leave the path empty: Cloudflare passes it on unchanged, and the signatures cover it. The domain must already be a website in your Cloudflare account.",
2710
+ "The bridge listens on 127.0.0.1 only, so the Windows firewall needs no inbound rule. The machine needs outbound HTTPS for cloudflared and SQL Server access to the Subiekt database."
2711
+ ],
2712
+ check: "https://subiekt-bridge.your-shop.pl/healthz answers ok, while any /v1 address answers invalid_signature without a signature."
2713
+ },
2714
+ access: {
2715
+ title: "Lock the address with Cloudflare Access",
2716
+ body: [
2717
+ "Signatures already protect every /v1 call. Access adds a second lock before a request reaches the machine. In Cloudflare Zero Trust create a service token (Access controls, Service credentials, Service Tokens) and copy the Client ID and Client Secret: the secret is shown only once.",
2718
+ "Add a self-hosted Access application for subiekt-bridge.your-shop.pl with a policy whose action is Service Auth and which includes that token. Then set cfAccessClientId and cfAccessClientSecret in Medusa. A token expires after the duration you chose: renew it before then."
2719
+ ],
2720
+ check: "Check connection still says Connected, while a browser without the token is stopped by Cloudflare Access."
2721
+ },
2722
+ medusa: {
2723
+ title: "Configure the plugin in Medusa",
2724
+ body: [
2725
+ "Add the plugin to medusa-config.ts with the bridge address and the same secret, then run `npx medusa db:migrate` and restart Medusa. Start with a dry run of stock and every writer off: the first days only plan.",
2726
+ "With several stock locations set stockLocationId to the one that mirrors the Subiekt warehouse. salesDocument decides the invoice or receipt (none, fs, pa, auto) and salesDocumentAfter when (wz or zk). nipSources says where your checkout keeps the buyer's NIP."
2727
+ ],
2728
+ check: "This page shows the bridge host instead of Not configured."
2729
+ },
2730
+ connection: {
2731
+ title: "Check the connection, signatures and clocks",
2732
+ body: [
2733
+ "Click Check connection. In Settings (the cog), the Bridge tab shows the versions, the nexo SDK and database, the licence, what the bridge can do, the round trip and the clock skew.",
2734
+ "Signatures fail when the secret differs or when the clocks differ by more than 5 minutes. Keep Windows time synchronized (Settings, Time and language, Sync now, or `w32tm /resync` as administrator)."
2735
+ ],
2736
+ check: "Connected, signatures accepted, clock skew under a minute."
2737
+ },
2738
+ catalog: {
2739
+ title: "Read stock and plan prices",
2740
+ body: [
2741
+ "Click Sync stock. With stockDryRun the run only records the plan: what would change, variants without a match, products only in Subiekt, conflicts. Fix codes where they differ (EAN in Subiekt under Miary, Kod kreskowy, or the SKU against the product symbol) until the list is short and expected.",
2742
+ "Click Read products. The plan lists price changes from and to, products to create and conflicts. Nothing changes yet."
2743
+ ],
2744
+ check: "A stock run without errors, and a product plan you have read."
2745
+ },
2746
+ testOrder: {
2747
+ title: "Test order: ZK, WZ, invoice",
2748
+ body: [
2749
+ "Place a test order paid on delivery. Within seconds Subiekt has its ZK and this order shows the number. Issue the WZ from that ZK in Subiekt (the WZ must come from the ZK: Subiekt copies the order tag with it); within a few minutes the WZ shows on the order.",
2750
+ "With salesDocument set and the documents writer armed, the invoice or receipt follows. Cancel a second test order before its WZ to see the cancel: Sfera cannot set the status Unieważnione, so the ZK gets a visible mark and a person finishes it in Subiekt."
2751
+ ],
2752
+ check: "The test order shows its ZK and WZ."
2753
+ },
2754
+ documents: {
2755
+ title: "Invoices, receipts and contractors",
2756
+ body: [
2757
+ "salesDocument auto issues an FS when the order carries a valid NIP and a PA otherwise. The bridge realizes the WZ when there is one (the goods left already) and the ZK otherwise, once per order. KSeF numbers arrive when Subiekt sent the e-invoice; they appear on the order. Receipts are fiscalized in Subiekt, never by the bridge.",
2758
+ "For company buyers set the bridge to BuyerMode customer: the ZK goes to the contractor with that NIP. To create missing contractors, set CreateContractors in the bridge, createContractors in Medusa and arm the contractors writer. An invalid NIP (checksum) sends the order to the retail buyer with a warning in the queue."
2759
+ ],
2760
+ check: "Settings, Writers tab: documents armed, and the test order shows its FS or PA."
2761
+ },
2762
+ prices: {
2763
+ title: "Prices and new products",
2764
+ body: [
2765
+ "Set priceWriter or createMissingProducts in medusa-config.ts, read the plan, then arm the writer here. Each run applies at most maxPriceChangesPerRun prices and maxProductsPerRun products, reads every item again right before it writes, skips what changed in Medusa meanwhile and quarantines an item that failed three runs in a row. New products are drafts: add images and a sales channel, then publish."
2766
+ ],
2767
+ check: "Settings, Writers tab: the price writer armed by a person, the plan rows Applied."
2768
+ },
2769
+ live: {
2770
+ title: "Go live",
2771
+ body: [
2772
+ "Switch stockDryRun off once the stock plan is right. Arm every writer you want deliberately, one at a time. Set Medusa:WebhookUrl in the bridge to https://your-medusa/hooks/subiekt so WZ arrive within seconds. Subscribe to subiekt.task_failed to alert your team, and keep the queue at zero Need attention."
2773
+ ],
2774
+ check: "Every item of the go-live checklist below is ticked."
2775
+ }
2776
+ },
2777
+ checklist: {
2778
+ title: "Go-live checklist",
2779
+ subtitle: "Ticks come from the live status of this store.",
2780
+ items: {
2781
+ reachable: { label: "The bridge answers through the tunnel", hint: "Settings, Bridge tab: Connected." },
2782
+ signatures: { label: "Signatures accepted", hint: "Same secret in Medusa and the bridge." },
2783
+ clock: { label: "Clock skew under a minute", hint: "Signatures fail beyond 5 minutes." },
2784
+ contract: { label: "The bridge speaks contract 1.1", hint: "Bridge 0.2.0 or newer: products, documents, contractors." },
2785
+ subiekt: { label: "Subiekt logs in and the licence accepts it", hint: "Settings, Bridge tab: nexo licence." },
2786
+ stock: { label: "First stock read without errors", hint: "Settings, Stock tab." },
2787
+ dryRun: { label: "Stock dry run switched off after review", hint: "stockDryRun: false." },
2788
+ zk: { label: "First ZK created", hint: "A test order." },
2789
+ wz: { label: "A WZ came back from the warehouse", hint: "Issued from the ZK in Subiekt." },
2790
+ writers: { label: "Every allowed writer decided by a person", hint: "Settings, Writers tab: armed or switched off on purpose." },
2791
+ attention: { label: "Nothing needs attention in the queue", hint: "Queue: Need attention is empty." },
2792
+ webhook: { label: "Webhook from the bridge accepted (optional)", hint: "Bridge:Medusa:WebhookUrl." }
2793
+ }
2794
+ },
2795
+ faq: {
2796
+ title: "When something goes wrong",
2797
+ items: [
2798
+ {
2799
+ q: "Subiekt not answering: the database is in another version than the SDK",
2800
+ a: [
2801
+ `Sfera connects only to a database of exactly its own version. After a Subiekt update the database moves on and the bridge built against the old SDK cannot log in. Settings, Connection tab, then says "The nexo database has a different version than the SDK the bridge was built with", with the SDK version and Sfera's own message; the bridge answers subiekt_unavailable and Medusa keeps retrying, no order is lost.`,
2802
+ "Install the nexo SDK of the new version, then run deploy\\upgrade-service.ps1: it rebuilds the bridge against the newest SDK installed. To pin one, pass -NexoSdkBin with its Bin folder."
2803
+ ]
2804
+ },
2805
+ {
2806
+ q: "The KodaSubiektBridge service does not start",
2807
+ a: [
2808
+ "Look in Event Viewer (Windows Logs, Application) and in C:\\KodaSubiektBridge\\logs. The usual causes: the password of the service account changed (Services, KodaSubiektBridge, Log On: type the new one), appsettings.Local.json is not valid JSON (--check reads the same files and says where), or another program took port 5280 (set Urls in appsettings.Local.json and the Service URL of the tunnel).",
2809
+ "After a Subiekt update the service usually starts but Subiekt does not answer: that is the version change from the previous answer."
2810
+ ]
2811
+ },
2812
+ {
2813
+ q: "The nexo licence refuses work",
2814
+ a: [
2815
+ 'A used up trial or an expired licence stops Sfera with "Limit czasu pracy". Activate a valid Subiekt nexo PRO licence on that machine. Make sure the licence has a workstation for the bridge machine; your InsERT partner can confirm how it counts the Sfera login.'
2816
+ ]
2817
+ },
2818
+ {
2819
+ q: "Every call fails after days of uptime",
2820
+ a: [
2821
+ "Sfera starts a WPF dispatcher on every thread that uses it, and a Windows service has a small desktop heap. Bridges that called Sfera from random threads ran out of it after about six days. This bridge runs every Sfera call on one dedicated thread, so it does not; if you see it, check that the service runs this bridge, then restart it."
2822
+ ]
2823
+ },
2824
+ {
2825
+ q: "Subiekt is busy for over 5 minutes",
2826
+ a: [
2827
+ "Sfera is not safe for parallel sessions, so the bridge runs one operation at a time. A very long operation (a huge stock read, Subiekt doing maintenance) makes the others wait; the answer busy is retried by Medusa later."
2828
+ ]
2829
+ },
2830
+ {
2831
+ q: "invalid_signature or stale_timestamp",
2832
+ a: [
2833
+ "invalid_signature: the secret differs between Medusa and the bridge. stale_timestamp: the clocks differ by more than 5 minutes. The Bridge tab in Settings shows the skew. To change the secret without downtime, put the old one in previousSecret (Medusa) and PreviousSecret (bridge) for the switch."
2834
+ ]
2835
+ },
2836
+ {
2837
+ q: "A WZ issued in Subiekt does not appear in Medusa",
2838
+ a: [
2839
+ "Issue the WZ from the ZK: Subiekt copies the ZK notes, with the order tag [medusa:order_...], to the WZ, and that tag is how the bridge finds the order. A WZ typed by hand has no tag. The bridge looks for new WZ every 2 minutes and Medusa reads the feed every 2 minutes; on its very first run the watcher starts after the newest WZ and announces none of the older ones."
2840
+ ]
2841
+ },
2842
+ {
2843
+ q: "Can an order get two ZK or two invoices?",
2844
+ a: [
2845
+ "No. The bridge writes the order tag into every document and looks for it before it creates one, under the order lock and again under the Sfera lock. Medusa keeps one task per order and claims it atomically. An unclear answer (a timeout) becomes Answer unclear, and the next attempt asks the bridge first."
2846
+ ]
2847
+ },
2848
+ {
2849
+ q: "Finish by hand in Subiekt after a cancel",
2850
+ a: [
2851
+ "Sfera cannot set the ZK status Unieważnione. The bridge marks the ZK at the top of its notes and with a custom flag when you have one; a person sets the status. Once a WZ or a sales document exists, the cancel is refused (document_locked): handle it as a return or a correction in Subiekt."
2852
+ ]
2853
+ },
2854
+ {
2855
+ q: "Lines without a product in Subiekt (unmatched_lines)",
2856
+ a: [
2857
+ "The bridge matches every line by EAN first, then the SKU against the product symbol, and writes nothing when one line has no product. Fix the EAN in Subiekt (Miary, Kod kreskowy) or the SKU, then Send again. Services and gift cards without codes can be left off with omitLinesWithoutCode."
2858
+ ]
2859
+ },
2860
+ {
2861
+ q: "--check passes but the service cannot log in to SQL Server",
2862
+ a: [
2863
+ "--check runs as you, the service as its own Windows account. Give that account access to the nexo database, or run --check as that account."
2864
+ ]
2865
+ },
2866
+ {
2867
+ q: "Contractors are not created",
2868
+ a: [
2869
+ "Four switches must agree: Bridge:Subiekt:BuyerMode customer, Bridge:Subiekt:CreateContractors true, createContractors in medusa-config.ts, and the contractors writer armed here. An invalid NIP never creates one; the queue shows the warning."
2870
+ ]
2871
+ },
2872
+ {
2873
+ q: "A variant is missing from the price plan",
2874
+ a: [
2875
+ "Prices match by EAN, then by the SKU equal to the symbol, without stripping SKU suffixes, so a wholesale -WH variant keeps its price. A product without a price in the level, a level in another currency, or a duplicated EAN never produce a change."
2876
+ ]
2877
+ },
2878
+ {
2879
+ q: "The bridge machine was off over the weekend",
2880
+ a: [
2881
+ "Nothing is lost: every call is a task first and is retried for about two and a half days. Tasks still failing after that wait under Need attention with a Send again button."
2882
+ ]
2883
+ }
2884
+ ]
2885
+ }
2886
+ }
2887
+ };
2888
+ const pl$1 = {
2889
+ title: "Subiekt nexo",
2890
+ by: "by Koda Plus",
2891
+ subtitle: "Zamówienia trafiają do Subiekta nexo PRO jako dokumenty ZK, WZ wystawione w magazynie wraca do zamówienia, dokumenty sprzedaży dostają numer KSeF, a stany i ceny płyną z Subiekta do Medusy.",
2892
+ view: { panel: "Panel", guide: "Przewodnik wdrożenia" },
2893
+ settings: {
2894
+ title: "Ustawienia",
2895
+ subtitle: "Techniczna strona integracji: połączenie z mostem i jego diagnostyka, przełączniki zapisu, synchronizacja stanów i historia zadań w tle.",
2896
+ tab: {
2897
+ connection: "Połączenie",
2898
+ bridge: "Most",
2899
+ writers: "Zapisy",
2900
+ stock: "Stany",
2901
+ runs: "Historia"
2902
+ }
2903
+ },
2904
+ mode: {
2905
+ demo: "Most demo",
2906
+ connected: "Połączone",
2907
+ subiektDown: "Subiekt nie odpowiada",
2908
+ unreachable: "Most nieosiągalny",
2909
+ notConfigured: "Brak konfiguracji",
2910
+ unknown: "Jeszcze nie sprawdzono"
2911
+ },
2912
+ actions: {
2913
+ moreActions: "Więcej akcji",
2914
+ check: "Sprawdź połączenie",
2915
+ checking: "Sprawdzam",
2916
+ stock: "Synchronizuj stany",
2917
+ events: "Pobierz dokumenty",
2918
+ tasks: "Wyślij kolejkę teraz",
2919
+ products: "Pobierz produkty",
2920
+ running: "Trwa",
2921
+ retry: "Wyślij ponownie",
2922
+ send: "Wyślij do Subiekta",
2923
+ openPanel: "Panel Subiekta",
2924
+ release: "Zwolnij"
2925
+ },
2926
+ demo: {
2927
+ label: "Tryb demo",
2928
+ text: "Symulowany most wewnątrz Medusy nadaje numery ZK, WZ, FS i PA: nowe zamówienie dostaje ZK w kilka sekund, magazyn „wystawia” WZ około trzech minut później, potem powstaje faktura albo paragon, a faktura dostaje numer KSeF dwie minuty później. Stany i ceny liczone są z Twojego katalogu; włączone tu zapisy tylko symulują, katalog zostaje nietknięty. Jak podłączyć prawdziwego Subiekta, opisuje przewodnik wdrożenia."
2929
+ },
2930
+ missing: {
2931
+ label: "Konfiguracja",
2932
+ text: "Brakuje opcji wtyczki: {{missing}}. bridgeUrl to publiczny adres mostu (na przykład Cloudflare Tunnel), secret to ten sam losowy ciąg w Medusie i w moście (openssl rand -hex 32)."
2933
+ },
2934
+ optionWarnings: {
2935
+ label: "Opcja do poprawy",
2936
+ priceListId: "priceTarget ma wartość price_list, ale priceListId nie jest identyfikatorem cennika (plist_...). Do czasu poprawki zapis cen zostaje wyłączony."
2937
+ },
2938
+ stats: {
2939
+ sent24h: "Wysłane w 24 h",
2940
+ waiting: "Czekają na płatność",
2941
+ queue: "W kolejce",
2942
+ attention: "Wymagają uwagi",
2943
+ zk: "Dokumenty ZK",
2944
+ wz: "Dokumenty WZ",
2945
+ sales: "FS i PA"
2946
+ },
2947
+ store: {
2948
+ openOrder: "Otwórz zamówienie",
2949
+ openProduct: "Otwórz produkt",
2950
+ findProduct: "Znajdź produkt",
2951
+ orderMatching: "Każdy dokument, który Subiekt wystawia do zamówienia, trzyma w uwagach identyfikator tego zamówienia, jako [medusa:order_...]: po nim dokument znajduje swoje zamówienie.",
2952
+ noOrder: "Bez zamówienia w sklepie",
2953
+ noOrderHint: "Subiekt przysłał ten dokument bez znacznika zamówienia w uwagach, więc nie należy tu do żadnego zamówienia. WZ wpisane ręcznie, a nie wystawione z ZK, nie ma znacznika.",
2954
+ noProduct: "Brak produktu w sklepie",
2955
+ notInStore: "Jeszcze nie w sklepie",
2956
+ notInStoreHint: "Na razie tylko w Subiekcie. Gdy zapis nowych produktów jest włączony, produkt powstaje w sklepie jako szkic.",
2957
+ draft: "Założony w sklepie jako szkic, z tym SKU."
2958
+ },
2959
+ connection: {
2960
+ title: "Połączenie",
2961
+ bridge: "Most",
2962
+ contract: "kontrakt {{version}}",
2963
+ subiekt: "Subiekt",
2964
+ database: "Baza",
2965
+ company: "Firma",
2966
+ warehouse: "Magazyn ZK",
2967
+ checkedAt: "Ostatnie sprawdzenie",
2968
+ lastError: "Ostatni błąd",
2969
+ failures: "Nieudane próby z rzędu: {{count}}",
2970
+ never: "nigdy",
2971
+ none: "brak",
2972
+ eventsReadAt: "Dokumenty pobrane",
2973
+ schedule: "Kolejka co minutę, dokumenty co 2 minuty, stany co 10 minut, produkty co godzinę."
2974
+ },
2975
+ bridge: {
2976
+ title: "Most",
2977
+ subtitle: "Co powiedziało ostatnie sprawdzenie mostu. Świeży odczyt daje przycisk Sprawdź połączenie.",
2978
+ versions: "Wersje",
2979
+ versionsValue: "most {{bridge}}, kontrakt {{contract}}, wtyczka {{plugin}}",
2980
+ sdk: "nexo SDK i baza",
2981
+ sdkValue: "SDK {{sdk}}, baza {{db}}",
2982
+ licence: "Licencja nexo",
2983
+ licenceStates: { ok: "przyjęła ostatnie logowanie", refused: "odmawia pracy (koniec wersji próbnej albo wygasła)", unknown: "jeszcze nieznana" },
2984
+ started: "Działa od",
2985
+ fakeMode: "Most działa w trybie fake (Bridge:Mode fake): nic nie trafia do Subiekta. Ustaw Mode na sfera na komputerze z mostem.",
2986
+ latency: "Czas odpowiedzi",
2987
+ skew: "Różnica zegarów",
2988
+ skewStates: {
2989
+ ok: "{{value}}, w porządku",
2990
+ warn: "{{value}}, popraw zegar: powyżej 5 minut podpisy przestają działać",
2991
+ error: "{{value}}, każdy podpis jest odrzucany: ustaw czas na obu maszynach",
2992
+ unknown: "jeszcze nie zmierzono"
2993
+ },
2994
+ signature: "Podpisy",
2995
+ signatureStates: {
2996
+ ok: "przyjęte przez most",
2997
+ invalid_signature: "odrzucone: sekret w Medusie i w moście jest różny",
2998
+ stale_timestamp: "odrzucone: zegary różnią się o ponad 5 minut",
2999
+ forbidden: "zablokowane przez Cloudflare Access: sprawdź token usługi",
3000
+ unreachable: "most nie odpowiada",
3001
+ unknown: "jeszcze nie sprawdzono"
3002
+ },
3003
+ lastEvent: "Ostatnie zdarzenie",
3004
+ lastEventValue: "#{{id}}, {{time}}",
3005
+ queues: "Czeka w moście",
3006
+ queuesValue: "operacje Sfery: {{sfera}}, webhooki: {{webhook}}",
3007
+ webhook: "Webhook z mostu",
3008
+ webhookValue: "ostatni przyjęty {{time}}",
3009
+ webhookNever: "jeszcze żaden (opcjonalny: Medusa i tak czyta kanał co 2 minuty)",
3010
+ webhookRejected: "ostatni odrzucony {{time}}: {{reason}}",
3011
+ webhookReasons: {
3012
+ stale: "zegary różnią się o ponad 5 minut",
3013
+ mismatch: "sekret różni się od tego w moście",
3014
+ missing: "żądanie nie miało podpisu",
3015
+ malformed: "nagłówek podpisu był uszkodzony",
3016
+ no_secret: "nie ustawiono sekretu"
3017
+ },
3018
+ capabilities: "Most obsługuje",
3019
+ cannot: "Jeszcze niedostępne",
3020
+ legacy: "Ten most mówi kontraktem 1.0: produkty, kontrahenci, dokumenty sprzedaży i numery KSeF wymagają mostu 0.2.0 (kontrakt 1.1). Reszta działa jak dotąd.",
3021
+ reasons: {
3022
+ no_health: "najpierw sprawdź połączenie",
3023
+ update_bridge: "zaktualizuj most do 0.2.0 lub nowszego",
3024
+ bridge_config: "wyłączone w konfiguracji mostu"
3025
+ },
3026
+ capabilityNames: {
3027
+ orders: "Zamówienia (ZK)",
3028
+ fulfillments: "WZ z Medusy",
3029
+ stock: "Stany",
3030
+ events: "Kanał zdarzeń",
3031
+ products: "Produkty i ceny",
3032
+ contractors: "Kontrahenci po NIP",
3033
+ "contractors.create": "Nowi kontrahenci",
3034
+ "documents.fs": "Faktury (FS)",
3035
+ "documents.pa": "Paragony (PA)",
3036
+ "documents.ksef": "Numery KSeF",
3037
+ webhook: "Webhook do Medusy"
3038
+ },
3039
+ capabilityHints: {
3040
+ contractors: 'Bridge:Subiekt:BuyerMode "customer"',
3041
+ "contractors.create": 'Bridge:Subiekt:BuyerMode "customer" oraz Bridge:Subiekt:CreateContractors true',
3042
+ "documents.fs": "Bridge:Documents:Fs true",
3043
+ "documents.pa": "Bridge:Documents:Pa true",
3044
+ "documents.ksef": "Bridge:Documents:Fs true",
3045
+ webhook: "Bridge:Medusa:WebhookUrl",
3046
+ products: "",
3047
+ orders: "",
3048
+ fulfillments: "",
3049
+ stock: "",
3050
+ events: ""
3051
+ }
3052
+ },
3053
+ writers: {
3054
+ title: "Przełączniki zapisu",
3055
+ subtitle: "Każdy zapis dodany w 0.2.0 wymaga swojej opcji w medusa-config.ts i osoby, która włączy go tutaj. Opcja ustawiona na false wygrywa.",
3056
+ demoNote: "Demo: włączone zapisy tylko symulują. W tym sklepie nic się nie zmienia.",
3057
+ names: {
3058
+ prices: "Ceny z Subiekta",
3059
+ products: "Nowe produkty z Subiekta",
3060
+ documents: "Dokumenty sprzedaży (FS, PA)",
3061
+ contractors: "Nowi kontrahenci w Subiekcie"
3062
+ },
3063
+ descriptions: {
3064
+ prices: "Nanosi plan cen na Medusę, najwyżej {{cap}} zmian w jednym przebiegu.",
3065
+ products: "Zakłada szkice produktów dla towarów z Subiekta, których brakuje w Medusie, najwyżej {{cap}} w jednym przebiegu.",
3066
+ documents: "Wystawia w Subiekcie fakturę albo paragon do każdego zamówienia ({{mode}}).",
3067
+ contractors: "Pozwala mostowi założyć kontrahenta, gdy nikt w Subiekcie nie ma NIP nabywcy."
3068
+ },
3069
+ option: "Opcja {{option}}",
3070
+ allowed: "pozwala",
3071
+ forbidden: "nie pozwala",
3072
+ armed: "Włączony",
3073
+ disarmed: "Wyłączony",
3074
+ active: "Zapisuje",
3075
+ inactive: "Nie zapisuje",
3076
+ unsupported: "Most jeszcze tego nie potrafi.",
3077
+ changed: "{{who}}, {{time}}",
3078
+ armedBy: "Włączył: {{who}}, {{time}}",
3079
+ disarmedBy: "Wyłączył: {{who}}, {{time}}",
3080
+ never: "Nigdy nie włączony",
3081
+ blocked: "Włączony, ale nie zapisuje",
3082
+ armedToast: "Włączone.",
3083
+ disarmedToast: "Wyłączone.",
3084
+ armTitle: "Włączyć: {{name}}?",
3085
+ disarmTitle: "Wyłączyć: {{name}}?",
3086
+ armText: "Od teraz ten zapis naprawdę zmienia dane, w swoich limitach. Możesz go tu wyłączyć w każdej chwili, a zmiana zostaje zapisana z Twoim nazwiskiem.",
3087
+ armTextDemo: "Demo: zapis tylko symuluje. Nic nie zmienia się ani w tym sklepie, ani w żadnym Subiekcie.",
3088
+ disarmText: "Zapis zatrzymuje się przed kolejną pozycją. Plany nadal są liczone i pokazywane.",
3089
+ armConfirm: "Włącz",
3090
+ disarmConfirm: "Wyłącz",
3091
+ cancel: "Anuluj",
3092
+ documentModes: {
3093
+ none: "żaden",
3094
+ fs: "zawsze FS",
3095
+ pa: "zawsze PA",
3096
+ auto: "FS przy poprawnym NIP, w pozostałych przypadkach PA"
3097
+ }
3098
+ },
3099
+ products: {
3100
+ title: "Produkty z Subiekta",
3101
+ subtitle: "Ceny z Subiekta obok produktów w sklepie, które aktualizują, oraz towary z Subiekta, których sklep jeszcze nie sprzedaje. Najpierw wszystko jest planem: nic się nie zmienia, dopóki ktoś nie włączy zapisu w Ustawieniach.",
3102
+ matching: "Towary z Subiekta są dopasowywane do wariantów w sklepie najpierw po EAN, potem po SKU porównanym z symbolem w Subiekcie. Kod użyty dwa razy to konflikt i nigdy nie jest zgadywany.",
3103
+ matchedKey: "Dopasowane po {{by}}: {{code}}",
3104
+ settings: "Ceny z {{level}} ({{type}}) do: {{target}}, w walucie {{currency}}.",
3105
+ settingsLevel: "poziomu {{symbol}}",
3106
+ settingsTargets: { variant: "ceny wariantów", price_list: "cennik {{id}}" },
3107
+ types: { gross: "brutto", net: "netto" },
3108
+ firstLevel: "pierwszego poziomu cen",
3109
+ never: "Produkty jeszcze nie zostały pobrane.",
3110
+ unsupported: "Most jeszcze nie wysyła produktów: zaktualizuj go do 0.2.0 (kontrakt 1.1).",
3111
+ disabled: "Produkty z Subiekta są wyłączone (productSyncEnabled: false).",
3112
+ counters: {
3113
+ subiektProducts: "Towary w Subiekcie",
3114
+ matched: "Dopasowane",
3115
+ priceChanges: "Zmiany cen",
3116
+ unchanged: "Bez zmian",
3117
+ toCreate: "Do założenia",
3118
+ conflicts: "Konflikty",
3119
+ unmatchedVariants: "Warianty bez dopasowania",
3120
+ noPrice: "Bez ceny"
3121
+ },
3122
+ kinds: { price: "Ceny", create: "Nowe produkty" },
3123
+ statuses: {
3124
+ all: "Wszystkie",
3125
+ planned: "Zaplanowane",
3126
+ applied: "Naniesione",
3127
+ simulated: "Zasymulowane",
3128
+ over_cap: "Następny przebieg",
3129
+ failed: "Nieudane",
3130
+ quarantined: "W kwarantannie",
3131
+ stale: "Zmienione w międzyczasie",
3132
+ skipped: "Pominięte"
3133
+ },
3134
+ columns: { product: "Towar w Subiekcie", change: "Zmiana", store: "Produkt w sklepie", matched: "Dopasowanie", status: "Status", details: "Szczegóły" },
3135
+ matchedBy: { ean: "EAN", sku: "SKU" },
3136
+ newProduct: "nowy, {{price}}",
3137
+ empty: "Nic w tym planie.",
3138
+ search: "Symbol, SKU albo EAN",
3139
+ quarantine: "W kwarantannie po kolejnych błędach",
3140
+ quarantineHint: "Zapis pomija te pozycje, dopóki ich nie zwolnisz. Najpierw usuń przyczynę (ostatni błąd mówi, co poszło nie tak).",
3141
+ samples: { conflicts: "Konflikty, nigdy nie zgadujemy", unmatchedVariants: "Warianty bez dopasowania (SKU)", noPrice: "Bez ceny w tym poziomie" },
3142
+ blocked: {
3143
+ currency_mismatch: "Poziom cen jest w innej walucie niż priceCurrency: nic nie zaplanowano.",
3144
+ no_level: "Ustawionego poziomu cen (priceLevel) nie ma w moście: nic nie zaplanowano."
3145
+ }
3146
+ },
3147
+ tasks: {
3148
+ title: "Kolejka",
3149
+ subtitle: "Zamówienia w drodze do Subiekta: każde ZK, WZ, faktura albo paragon, o które poprosił sklep, i jak poszło. Wysyłka, która się nie udała, czeka tu na Ciebie z przyciskiem Wyślij ponownie.",
3150
+ technical: "Każde wywołanie, które Medusa ma do wykonania w moście, z ponowieniami. Nieudane zadanie czeka na człowieka.",
3151
+ filters: { attention: "Wymagają uwagi", open: "Otwarte", done: "Zakończone", all: "Wszystkie" },
3152
+ search: "Numer zamówienia",
3153
+ empty: "Nic tu nie ma.",
3154
+ columns: { order: "Zamówienie w sklepie", operation: "Operacja", status: "Status", attempts: "Próby", document: "Dokument w Subiekcie", details: "Szczegóły", actions: "" },
3155
+ kinds: { "order.create": "Utwórz ZK", "order.cancel": "Anuluj ZK", "order.fulfill": "Wystaw WZ", "order.document": "Wystaw dokument sprzedaży" },
3156
+ documentKinds: { fs: "Wystaw FS", pa: "Wystaw PA" },
3157
+ statuses: {
3158
+ waiting: "Czeka na płatność",
3159
+ pending: "W kolejce",
3160
+ running: "Wysyłanie",
3161
+ unknown: "Niejasna odpowiedź",
3162
+ succeeded: "Gotowe",
3163
+ failed: "Wymaga uwagi",
3164
+ canceled: "Anulowane"
3165
+ },
3166
+ unknownHint: "Most mógł go już wystawić. Kolejna próba najpierw pyta most i nigdy nie wystawia drugiego.",
3167
+ nextAttempt: "Następna próba {{time}}",
3168
+ manual: "Dokończ ręcznie w Subiekcie",
3169
+ buyer: { existing: "Nabywca {{name}} (NIP {{nip}}), znaleziony w Subiekcie", created: "Nabywca {{name}} (NIP {{nip}}), założony w Subiekcie", retail: "Nabywca detaliczny" },
3170
+ page: "{{from}}-{{to}} z {{count}}",
3171
+ prev: "Poprzednie",
3172
+ next: "Następne"
3173
+ },
3174
+ documents: {
3175
+ title: "Dokumenty",
3176
+ subtitle: "Co Subiekt wystawił do Twoich zamówień: ZK, WZ, faktury i paragony, każdy dokument obok swojego zamówienia w sklepie.",
3177
+ filters: { all: "Wszystkie", ZK: "ZK", WZ: "WZ", FS: "FS", PA: "PA" },
3178
+ empty: "Jeszcze nie ma dokumentów.",
3179
+ columns: { number: "Numer", order: "Zamówienie w sklepie", issued: "Wystawiono", source: "Źródło", status: "Status", ksef: "Numer KSeF" },
3180
+ sources: { bridge: "Medusa", subiekt: "Magazyn", demo: "Demo" },
3181
+ statuses: { open: "Otwarty", canceled: "Anulowany", completed: "Zrealizowany" },
3182
+ ksefWaiting: "czeka na KSeF"
3183
+ },
3184
+ stock: {
3185
+ title: "Stany",
3186
+ subtitle: "Z Subiekta do Medusy, najpierw po EAN, potem po SKU porównanym z symbolem towaru.",
3187
+ never: "Synchronizacja stanów jeszcze się nie odbyła.",
3188
+ dryRun: "Próba na sucho",
3189
+ counters: {
3190
+ subiektProducts: "Towary w Subiekcie",
3191
+ matchedItems: "Dopasowane",
3192
+ toUpdate: "Do aktualizacji",
3193
+ toCreate: "Do utworzenia",
3194
+ unchanged: "Bez zmian",
3195
+ unmatchedSubiekt: "Tylko w Subiekcie",
3196
+ unmatchedVariants: "Warianty bez dopasowania",
3197
+ conflicts: "Konflikty"
3198
+ },
3199
+ samples: {
3200
+ changes: "Zmiany",
3201
+ unmatchedVariants: "Warianty bez dopasowania (SKU)",
3202
+ unmatchedSubiekt: "Tylko w Subiekcie (symbol)",
3203
+ conflicts: "Konflikty, pozostawione bez zmian"
3204
+ }
3205
+ },
3206
+ runs: {
3207
+ title: "Historia",
3208
+ empty: "Jeszcze nic się nie wydarzyło.",
3209
+ columns: { kind: "Zadanie", trigger: "Wyzwalacz", status: "Wynik", message: "Podsumowanie", started: "Start", duration: "Czas" },
3210
+ kinds: { stock: "Stany", events: "Dokumenty", tasks: "Kolejka", health: "Połączenie", products: "Produkty" },
3211
+ triggers: { schedule: "Harmonogram", manual: "Ręcznie", webhook: "Webhook", event: "Zdarzenie", auto: "Automatycznie" },
3212
+ statuses: { success: "OK", partial: "Częściowo", error: "Błąd", skipped: "Pominięto" },
3213
+ summary: {
3214
+ stockDry: "Próba na sucho: do zmiany {{changes}}, bez zmian {{unchanged}}.",
3215
+ stock: "Zaktualizowane poziomy: {{changes}}, bez zmian: {{unchanged}}.",
3216
+ tasks: "Wysłane: {{succeeded}}.",
3217
+ tasksIssues: "Wysłane: {{succeeded}}, do ponowienia: {{retry}}, wymagają uwagi: {{failed}}.",
3218
+ events: "Nowe dokumenty z Subiekta: {{applied}}.",
3219
+ eventsNone: "Nic nowego.",
3220
+ health: "Most i Subiekt odpowiadają.",
3221
+ productsPlan: "Plan: zmiany cen {{prices}}, nowe produkty {{creates}}. Żaden zapis nie jest włączony.",
3222
+ productsApplied: "Zaplanowane zmiany cen: {{prices}}, nowe produkty: {{creates}}, naniesione: {{applied}}, nieudane: {{failed}}."
3223
+ }
3224
+ },
3225
+ widget: {
3226
+ title: "Subiekt nexo",
3227
+ demo: "Demo",
3228
+ more: "Panel",
3229
+ none: "To zamówienie nie zostało wysłane do Subiekta.",
3230
+ waiting: "ZK powstanie po zaksięgowaniu płatności.",
3231
+ send: "Wyślij do Subiekta teraz",
3232
+ sent: "W kolejce. ZK pojawi się tu za kilka sekund.",
3233
+ ksef: "KSeF {{number}}",
3234
+ ksefWaiting: "Numer KSeF jeszcze nie nadany",
3235
+ issueFs: "Wystaw fakturę (FS)",
3236
+ issuePa: "Wystaw paragon (PA)",
3237
+ issued: "W kolejce. Dokument pojawi się tu za kilka sekund.",
3238
+ autoOff: "Automatyczne dokumenty sprzedaży są wyłączone: włącz zapis na stronie Subiekta.",
3239
+ autoOn: "Dokument sprzedaży powstanie automatycznie po {{after}}."
3240
+ },
3241
+ toast: {
3242
+ started: "Uruchomione. Wynik pojawi się tu za chwilę.",
3243
+ already: "Już trwa.",
3244
+ checkedOk: "Most i Subiekt odpowiadają.",
3245
+ checkedFail: "Brak połączenia: {{error}}",
3246
+ retried: "Ponownie w kolejce.",
3247
+ released: "Zwolnione. Następny przebieg spróbuje ponownie.",
3248
+ error: "Coś poszło nie tak: {{error}}"
3249
+ },
3250
+ references: {
3251
+ title: "Działa w sklepach wdrożonych przez Koda Plus",
3252
+ subtitle: "Prawdziwe sklepy z wdrożeniem Koda Plus, które pracują na tej integracji na co dzień.",
3253
+ open: "Otwórz sklep",
3254
+ since: "Od {{date}}",
3255
+ badgeOne: "Działa w 1 sklepie",
3256
+ badgeMany: "Działa w {{count}} sklepach",
3257
+ review: "Zobacz opinię"
3258
+ },
3259
+ community: {
3260
+ addStore: {
3261
+ button: "Dodaj swój sklep",
3262
+ title: "Twój sklep działa na tej integracji?",
3263
+ text: "Daj nam znać. Po sprawdzeniu dodamy go do tej listy z linkiem i oceną.",
3264
+ name: "Nazwa sklepu",
3265
+ url: "Adres sklepu",
3266
+ note: "Kilka słów o wdrożeniu (opcjonalnie)",
3267
+ send: "Wyślij zgłoszenie",
3268
+ copy: "Kopiuj treść",
3269
+ copied: "Treść skopiowana, wklej ją w mail do hello@koda.plus",
3270
+ hint: "Otworzy się Twoja poczta z gotową wiadomością do {{email}}.",
3271
+ subject: "Zgłoszenie sklepu: {{integration}}",
3272
+ greeting: "Dzień dobry, mój sklep działa na integracji {{integration}}. Proszę o dodanie go do listy sklepów."
3273
+ },
3274
+ prompt: {
3275
+ button: "Kopiuj prompt",
3276
+ title: "Dodaj tę integrację do swojego sklepu",
3277
+ text: "Wklej go w Claude Code, Cursorze albo innym agencie AI otwartym w projekcie Twojej Medusy. Agent zainstaluje wtyczkę, ustawi ją tak jak tutaj i zostawi notatki do dalszej pracy.",
3278
+ copy: "Kopiuj jeszcze raz",
3279
+ copied: "Prompt skopiowany",
3280
+ failed: "Nie udało się skopiować. Zaznacz tekst i skopiuj go ręcznie."
3281
+ },
3282
+ discord: {
3283
+ button: "Pomoc na Discordzie",
3284
+ title: "Dołącz do serwera Koda Plus na Discordzie"
3285
+ }
3286
+ },
3287
+ guide: {
3288
+ intro: {
3289
+ title: "Połącz Medusę z Subiektem nexo PRO",
3290
+ text: "Subiekt nie ma API w sieci: jego interfejs programistyczny, Sfera, to biblioteka Windows działająca obok bazy Subiekta. Dlatego mały program, most, pracuje jako usługa Windows na komputerze, który widzi serwer SQL Subiekta, a Cloudflare Tunnel publikuje go przez HTTPS bez otwierania portu. Medusa podpisuje każde żądanie, most zakłada ZK każdego zamówienia, a Medusa odczytuje z powrotem WZ, fakturę albo paragon z numerem KSeF, stany i ceny. Nic nie jest zapisywane, dopóki na to nie pozwolisz.",
3291
+ timeLabel: "Czas",
3292
+ time: "2 do 4 godzin konfiguracji, potem kilka dni zamówień testowych i planów, zanim włączysz zapisy.",
3293
+ needsLabel: "Potrzebujesz",
3294
+ needs: [
3295
+ "Subiekta nexo PRO (Sfera w zestawie)",
3296
+ "Komputera z Windows włączonego cały czas, przy bazie",
3297
+ "nexo SDK w wersji Twojego Subiekta",
3298
+ ".NET 8 SDK",
3299
+ "Operatora Subiekta dla mostu",
3300
+ "Domeny w Cloudflare",
3301
+ "Mostu od Koda Plus",
3302
+ "Dostępu do medusa-config.ts i jego zmiennych środowiskowych"
3303
+ ]
3304
+ },
3305
+ diagram: {
3306
+ title: "Jak rozmawiają ze sobą części",
3307
+ subtitle: "Żądania do mostu wysyła tylko Medusa. Most może szturchnąć Medusę, gdy czekają nowe dokumenty; same dokumenty zawsze przychodzą w podpisanym żądaniu Medusy.",
3308
+ nodes: [
3309
+ { title: "Medusa", caption: "ta wtyczka: kolejka, plany, zapisy, panel. Hostowana gdziekolwiek, na przykład na Railway." },
3310
+ { title: "Cloudflare Tunnel", caption: "publikuje most pod https://subiekt-bridge.twoj-sklep.pl bez otwartego portu. Odwrotne proxy z HTTPS też się nada." },
3311
+ { title: "Most (usługa Windows)", caption: "KodaSubiektBridge na 127.0.0.1:5280, jedna sesja Sfery, własny kanał zdarzeń." },
3312
+ { title: "Sfera (nexo SDK)", caption: "interfejs .NET od InsERT, w tej samej wersji co baza." },
3313
+ { title: "Subiekt nexo PRO", caption: "dokumenty, kontrahenci, stany i ceny w bazie SQL Server." }
3314
+ ],
3315
+ links: ["HTTPS, każde żądanie podpisane", "cloudflared", "wewnątrz mostu", "SQL Server, logowanie Windows"]
3316
+ },
3317
+ domain: "twoj-sklep.pl",
3318
+ demoNote: "Tryb demo: stany kroków i znaczniki poniżej pochodzą z symulowanego mostu. Po podłączeniu prawdziwego Subiekta pochodzą z Twojego.",
3319
+ links: {
3320
+ sdk: "Subiekt nexo PRO w InsERT (pobranie nexo SDK)",
3321
+ dotnet: "Pobierz .NET 8",
3322
+ tunnel: "Cloudflare: tworzenie tunelu",
3323
+ access: "Cloudflare: tokeny usług"
3324
+ },
3325
+ steps: {
3326
+ title: "Od zera do produkcji",
3327
+ subtitle: "Każdy krok sam zmienia się na zielony, gdy Medusa widzi, że działa.",
3328
+ checkLabel: "Sprawdzenie:",
3329
+ copy: "Kopiuj",
3330
+ copied: "Skopiowano.",
3331
+ states: { done: "Gotowe", todo: "Do zrobienia", optional: "Opcjonalne", later: "Przy starcie" },
3332
+ subiekt: {
3333
+ title: "Przygotuj Subiekta nexo PRO",
3334
+ body: [
3335
+ "Most działa przez Sferę, którą InsERT nazywa „Sfera dla Subiekta nexo” i dołącza do Subiekta nexo PRO. Sfera nie ma własnej licencji do kupienia ani aktywacji: wystarczy aktywna licencja Subiekta nexo PRO. Zwykły Subiekt nexo nie ma Sfery i potrzebuje wersji PRO (od InsERT albo partnera); osobny dodatek Sfera PRO+ (sfera zdarzeniowa, menu sferyczne) nie jest potrzebny.",
3336
+ "Załóż w Subiekcie operatora tylko dla mostu, na przykład `Integracja`, z uprawnieniami do zamówień od klientów, wydań zewnętrznych (WZ), dokumentów sprzedaży, towarów i kontrahentów. Zapisz jego hasło.",
3337
+ "Wybierz magazyn dla nowych ZK (jego symbol, na przykład `MAG`) i nabywcę detalicznego: kontrahenta, do którego trafia każde ZK bez firmy. Zanotuj jego NIP. Dla cen zanotuj symbol poziomu cen do publikacji i upewnij się, że jego cennik bazowy jest zatwierdzony.",
3338
+ "Serie dokumentów: ZK i WZ dostają numerację tego magazynu (na przykład `ZK 128/MAG/2026`), a każdy dokument używa domyślnej definicji swojego typu w Subiekcie, więc FS i PA zachowują numerację ustawioną przez księgowość. VAT pochodzi z kartotek towarów w Subiekcie: most ustawia na każdej pozycji cenę brutto zapłaconą przez klienta, a Subiekt wylicza netto i VAT według stawki towaru, więc trzymaj te stawki zgodne z ustawieniami podatków sklepu.",
3339
+ "Zanotuj dokładną wersję Subiekta (pokazuje ją okno informacji o programie): nexo SDK musi mieć tę samą."
3340
+ ],
3341
+ check: "Subiekt pokazuje wersję, operator się loguje, nabywca detaliczny istnieje."
3342
+ },
3343
+ sdk: {
3344
+ title: "Zainstaluj nexo SDK i .NET 8 na komputerze z Windows",
3345
+ body: [
3346
+ "Wybierz komputer z Windows, który pracuje cały czas i sięga do serwera SQL Subiekta, zwykle ten sam serwer, na którym działa Subiekt. Laptop, który usypia, wstrzymuje zamówienia na cały czas uśpienia (Medusa ponawia, ale nic nie płynie).",
3347
+ "Zainstaluj .NET 8 SDK (dotnet.microsoft.com/download/dotnet/8.0) i nexo SDK w wersji Twojego Subiekta (InsERT udostępnia je na stronie Subiekta nexo PRO). Instaluje się w `C:\\InsERT\\nexoSDK\\Bin\\nexoSDK_<wersja>`. Inna wersja niż baza to najczęstsza awaria: Sfera odmawia połączenia i mówi, że baza jest w innej wersji."
3348
+ ],
3349
+ check: "Folder C:\\InsERT\\nexoSDK\\Bin zawiera folder nexoSDK_<wersja> w tej samej wersji co Subiekt."
3350
+ },
3351
+ service: {
3352
+ title: "Zainstaluj most jako usługę Windows",
3353
+ body: [
3354
+ "Skopiuj most od Koda Plus na ten komputer, na przykład do `C:\\Koda\\subiekt-nexo-bridge`. Otwórz w tym folderze PowerShell jako administrator i uruchom `deploy\\install-service.ps1` z `-ServiceUser`, czyli kontem Windows, na którym ma działać usługa (nie LocalSystem: SQL Server przyjmuje logowanie Windows tego konta). Pierwsze uruchomienie publikuje most do `C:\\KodaSubiektBridge`, zakłada tam `appsettings.Local.json` ze wzoru i kończy pracę: uzupełnij każdą wartość zaczynającą się od `WSTAW-`.",
3355
+ "Klucze, wszystkie w sekcji `Bridge`: `Mode` sfera; `Secret` wspólny sekret (pierwsza linia poniżej go generuje); `Subiekt:Server` instancja SQL Server, na przykład `SERWER\\INSERTNEXO`; `Subiekt:Database` baza firmy; `Subiekt:Operator` i `Subiekt:OperatorPassword`; `Subiekt:Warehouse` dla nowych ZK; `Subiekt:StockWarehouses`, z których stany idą do Medusy; `Subiekt:BuyerNip` nabywca detaliczny; `Subiekt:BuyerMode` fixed albo customer, żeby użyć NIP nabywcy; `Subiekt:CreateContractors`; `Products:PriceLevels`; `Documents:Fs` i `Documents:Pa`; `Medusa:WebhookUrl` (opcjonalnie).",
3356
+ "Uruchom to samo polecenie jeszcze raz. Publikuje most na najnowszym zainstalowanym nexo SDK, uruchamia `--check` (konfiguracja, potem logowanie do Sfery tylko do odczytu, z magazynem, nabywcą detalicznym i poziomami cen), pyta o hasło konta usługi, zakłada usługę KodaSubiektBridge z restartem po awarii, uruchamia ją i odpytuje /healthz.",
3357
+ "Aktualizacje: `deploy\\upgrade-service.ps1` (kopia zapasowa, publikacja, sprawdzenie, start; z -Rollback sam przywraca kopię, gdy sprawdzenie zawiedzie). Usunięcie: `deploy\\uninstall-service.ps1`, który zostawia konfigurację, kanał zdarzeń i logi."
3358
+ ],
3359
+ check: "--check kończy się słowem Ready, skrypt komunikatem /healthz = ok, a http://127.0.0.1:5280/ na tym komputerze pokazuje stronę stanu."
3360
+ },
3361
+ tunnel: {
3362
+ title: "Opublikuj most przez Cloudflare Tunnel",
3363
+ body: [
3364
+ "W panelu Cloudflare otwórz Networking, Tunnels i wybierz Create a tunnel. Nadaj nazwę (na przykład `subiekt-bridge`), wybierz Windows i uruchom pokazane polecenie instalacji w terminalu administratora na komputerze z mostem. cloudflared działa potem jako usługa Windows.",
3365
+ "W tunelu otwórz Routes, Add route, Published application. Subdomain `subiekt-bridge`, Twoja domena, Service URL `http://127.0.0.1:5280`. Ścieżkę zostaw pustą: Cloudflare przekazuje ją bez zmian, a podpisy ją obejmują. Domena musi już być dodana jako witryna na Twoim koncie Cloudflare.",
3366
+ "Most nasłuchuje tylko na 127.0.0.1, więc zapora Windows nie potrzebuje reguły przychodzącej. Komputer potrzebuje wychodzącego HTTPS dla cloudflared i dostępu do bazy Subiekta w SQL Server."
3367
+ ],
3368
+ check: "https://subiekt-bridge.twoj-sklep.pl/healthz odpowiada ok, a każdy adres /v1 bez podpisu odpowiada invalid_signature."
3369
+ },
3370
+ access: {
3371
+ title: "Zamknij adres przez Cloudflare Access",
3372
+ body: [
3373
+ "Podpisy już chronią każde wywołanie /v1. Access dokłada drugi zamek, zanim żądanie dotrze do komputera. W Cloudflare Zero Trust utwórz token usługi (Access controls, Service credentials, Service Tokens) i skopiuj Client ID oraz Client Secret: sekret jest pokazywany tylko raz.",
3374
+ "Dodaj aplikację Access typu self-hosted dla subiekt-bridge.twoj-sklep.pl z polityką o akcji Service Auth, która obejmuje ten token. Potem ustaw w Medusie cfAccessClientId i cfAccessClientSecret. Token wygasa po wybranym przy tworzeniu czasie: odnów go wcześniej."
3375
+ ],
3376
+ check: "Sprawdź połączenie nadal pokazuje Połączone, a przeglądarkę bez tokenu zatrzymuje Cloudflare Access."
3377
+ },
3378
+ medusa: {
3379
+ title: "Skonfiguruj wtyczkę w Medusie",
3380
+ body: [
3381
+ "Dodaj wtyczkę do medusa-config.ts z adresem mostu i tym samym sekretem, potem uruchom `npx medusa db:migrate` i zrestartuj Medusę. Zacznij od próby na sucho dla stanów i wszystkich zapisów wyłączonych: pierwsze dni tylko planują.",
3382
+ "Przy kilku lokalizacjach magazynowych ustaw stockLocationId na tę, która odpowiada magazynowi Subiekta. salesDocument decyduje o fakturze albo paragonie (none, fs, pa, auto), a salesDocumentAfter o tym, kiedy (wz albo zk). nipSources mówi, gdzie Twój checkout trzyma NIP nabywcy."
3383
+ ],
3384
+ check: "Ta strona pokazuje adres mostu zamiast Brak konfiguracji."
3385
+ },
3386
+ connection: {
3387
+ title: "Sprawdź połączenie, podpisy i zegary",
3388
+ body: [
3389
+ "Kliknij Sprawdź połączenie. W Ustawieniach (zębatka) zakładka Most pokazuje wersje, nexo SDK i bazę, licencję, co most obsługuje, czas odpowiedzi i różnicę zegarów.",
3390
+ "Podpisy zawodzą, gdy sekret jest różny albo gdy zegary różnią się o ponad 5 minut. Trzymaj czas Windows zsynchronizowany (Ustawienia, Czas i język, Synchronizuj teraz, albo `w32tm /resync` jako administrator)."
3391
+ ],
3392
+ check: "Połączone, podpisy przyjęte, różnica zegarów poniżej minuty."
3393
+ },
3394
+ catalog: {
3395
+ title: "Odczytaj stany i zaplanuj ceny",
3396
+ body: [
3397
+ "Kliknij Synchronizuj stany. Przy stockDryRun przebieg tylko zapisuje plan: co by się zmieniło, warianty bez dopasowania, towary tylko w Subiekcie, konflikty. Popraw kody tam, gdzie się różnią (EAN w Subiekcie w Miary, Kod kreskowy, albo SKU względem symbolu towaru), aż lista będzie krótka i spodziewana.",
3398
+ "Kliknij Pobierz produkty. Plan pokazuje zmiany cen z i na, produkty do założenia i konflikty. Na razie nic się nie zmienia."
3399
+ ],
3400
+ check: "Przebieg stanów bez błędów i przeczytany plan produktów."
3401
+ },
3402
+ testOrder: {
3403
+ title: "Zamówienie testowe: ZK, WZ, faktura",
3404
+ body: [
3405
+ "Złóż zamówienie testowe płatne przy odbiorze. W kilka sekund Subiekt ma jego ZK, a to zamówienie pokazuje numer. Wystaw w Subiekcie WZ z tego ZK (WZ musi powstać z ZK: Subiekt przenosi wtedy znacznik zamówienia); w ciągu kilku minut WZ pojawi się przy zamówieniu.",
3406
+ "Przy ustawionym salesDocument i włączonym zapisie dokumentów potem powstaje faktura albo paragon. Anuluj drugie zamówienie testowe przed WZ, żeby zobaczyć anulowanie: Sfera nie ustawi statusu Unieważnione, więc ZK dostaje widoczny znacznik, a resztę robi człowiek w Subiekcie."
3407
+ ],
3408
+ check: "Zamówienie testowe pokazuje swoje ZK i WZ."
3409
+ },
3410
+ documents: {
3411
+ title: "Faktury, paragony i kontrahenci",
3412
+ body: [
3413
+ "salesDocument auto wystawia FS, gdy zamówienie niesie poprawny NIP, a w pozostałych przypadkach PA. Most realizuje WZ, jeśli istnieje (towar już wyjechał), a w przeciwnym razie ZK, raz na zamówienie. Numery KSeF przychodzą, gdy Subiekt wyśle e-fakturę; pojawiają się przy zamówieniu. Paragony fiskalizuje Subiekt, nigdy most.",
3414
+ "Dla firm ustaw w moście BuyerMode customer: ZK trafia do kontrahenta z tym NIP. Żeby zakładać brakujących kontrahentów, ustaw CreateContractors w moście, createContractors w Medusie i włącz zapis kontrahentów. Niepoprawny NIP (suma kontrolna) kieruje zamówienie do nabywcy detalicznego z ostrzeżeniem w kolejce."
3415
+ ],
3416
+ check: "Ustawienia, zakładka Zapisy: dokumenty włączone, a zamówienie testowe pokazuje swoją FS albo PA."
3417
+ },
3418
+ prices: {
3419
+ title: "Ceny i nowe produkty",
3420
+ body: [
3421
+ "Ustaw priceWriter albo createMissingProducts w medusa-config.ts, przeczytaj plan, potem włącz zapis tutaj. Każdy przebieg nanosi najwyżej maxPriceChangesPerRun cen i maxProductsPerRun produktów, tuż przed zapisem czyta każdą pozycję ponownie, pomija to, co ktoś w międzyczasie zmienił w Medusie, i wysyła do kwarantanny pozycję, która zawiodła trzy przebiegi z rzędu. Nowe produkty powstają jako szkice: dodaj zdjęcia i kanał sprzedaży, potem opublikuj."
3422
+ ],
3423
+ check: "Ustawienia, zakładka Zapisy: zapis cen włączony przez człowieka, wiersze planu Naniesione."
3424
+ },
3425
+ live: {
3426
+ title: "Start produkcyjny",
3427
+ body: [
3428
+ "Wyłącz stockDryRun, gdy plan stanów jest poprawny. Włączaj każdy potrzebny zapis świadomie, jeden po drugim. Ustaw w moście Medusa:WebhookUrl na https://twoja-medusa/hooks/subiekt, żeby WZ przychodziły w kilka sekund. Zasubskrybuj subiekt.task_failed, żeby alarmować zespół, i trzymaj kolejkę na zerze w Wymagają uwagi."
3429
+ ],
3430
+ check: "Każda pozycja listy kontrolnej poniżej jest odhaczona."
3431
+ }
3432
+ },
3433
+ checklist: {
3434
+ title: "Lista kontrolna przed startem",
3435
+ subtitle: "Znaczniki pochodzą z bieżącego stanu tego sklepu.",
3436
+ items: {
3437
+ reachable: { label: "Most odpowiada przez tunel", hint: "Ustawienia, zakładka Most: Połączone." },
3438
+ signatures: { label: "Podpisy przyjęte", hint: "Ten sam sekret w Medusie i w moście." },
3439
+ clock: { label: "Różnica zegarów poniżej minuty", hint: "Powyżej 5 minut podpisy przestają działać." },
3440
+ contract: { label: "Most mówi kontraktem 1.1", hint: "Most 0.2.0 lub nowszy: produkty, dokumenty, kontrahenci." },
3441
+ subiekt: { label: "Subiekt się loguje, a licencja to przyjmuje", hint: "Ustawienia, zakładka Most: licencja nexo." },
3442
+ stock: { label: "Pierwszy odczyt stanów bez błędów", hint: "Ustawienia, zakładka Stany." },
3443
+ dryRun: { label: "Próba na sucho dla stanów wyłączona po przeglądzie", hint: "stockDryRun: false." },
3444
+ zk: { label: "Pierwsze ZK założone", hint: "Zamówienie testowe." },
3445
+ wz: { label: "WZ wróciło z magazynu", hint: "Wystawione z ZK w Subiekcie." },
3446
+ writers: { label: "Każdy dozwolony zapis rozstrzygnięty przez człowieka", hint: "Ustawienia, zakładka Zapisy: włączony albo świadomie wyłączony." },
3447
+ attention: { label: "Nic w kolejce nie wymaga uwagi", hint: "Kolejka: Wymagają uwagi jest puste." },
3448
+ webhook: { label: "Webhook z mostu przyjęty (opcjonalnie)", hint: "Bridge:Medusa:WebhookUrl." }
3449
+ }
3450
+ },
3451
+ faq: {
3452
+ title: "Gdy coś nie działa",
3453
+ items: [
3454
+ {
3455
+ q: "Subiekt nie odpowiada: baza jest w innej wersji niż SDK",
3456
+ a: [
3457
+ "Sfera łączy się tylko z bazą dokładnie w swojej wersji. Po aktualizacji Subiekta baza idzie naprzód i most zbudowany na starym SDK nie może się zalogować. Zakładka Połączenie w Ustawieniach pokazuje wtedy „The nexo database has a different version than the SDK the bridge was built with” z wersją SDK i komunikatem samej Sfery; most odpowiada subiekt_unavailable, a Medusa ponawia, więc żadne zamówienie nie ginie.",
3458
+ "Zainstaluj nexo SDK w nowej wersji, potem uruchom deploy\\upgrade-service.ps1: przebuduje most na najnowszym zainstalowanym SDK. Żeby wskazać konkretne, podaj -NexoSdkBin z jego folderem Bin."
3459
+ ]
3460
+ },
3461
+ {
3462
+ q: "Usługa KodaSubiektBridge się nie uruchamia",
3463
+ a: [
3464
+ "Zajrzyj do Podglądu zdarzeń (Dzienniki systemu Windows, Aplikacja) i do C:\\KodaSubiektBridge\\logs. Zwykłe przyczyny: zmieniło się hasło konta usługi (Usługi, KodaSubiektBridge, Logowanie: wpisz nowe), appsettings.Local.json nie jest poprawnym JSON (--check czyta te same pliki i wskazuje miejsce) albo inny program zajął port 5280 (ustaw Urls w appsettings.Local.json i Service URL tunelu).",
3465
+ "Po aktualizacji Subiekta usługa zwykle startuje, ale Subiekt nie odpowiada: to zmiana wersji z poprzedniej odpowiedzi."
3466
+ ]
3467
+ },
3468
+ {
3469
+ q: "Licencja nexo odmawia pracy",
3470
+ a: [
3471
+ "Wykorzystana wersja próbna albo wygasła licencja zatrzymuje Sferę komunikatem „Limit czasu pracy”. Aktywuj ważną licencję Subiekta nexo PRO na tym komputerze. Upewnij się, że licencja ma stanowisko dla komputera z mostem; Twój partner InsERT potwierdzi, jak liczy logowanie przez Sferę."
3472
+ ]
3473
+ },
3474
+ {
3475
+ q: "Po kilku dniach pracy każde wywołanie zawodzi",
3476
+ a: [
3477
+ "Sfera uruchamia dyspozytor WPF na każdym wątku, który jej używa, a usługa Windows ma małą stertę pulpitu. Mosty, które wołały Sferę z przypadkowych wątków, wyczerpywały ją po około sześciu dniach. Ten most wykonuje każde wywołanie Sfery na jednym, stałym wątku, więc tego nie robi; jeśli to widzisz, sprawdź, czy usługa uruchamia ten most, i zrestartuj ją."
3478
+ ]
3479
+ },
3480
+ {
3481
+ q: "Subiekt jest zajęty dłużej niż 5 minut",
3482
+ a: [
3483
+ "Sfera nie jest bezpieczna dla równoległych sesji, więc most wykonuje jedną operację naraz. Bardzo długa operacja (ogromny odczyt stanów, prace serwisowe w Subiekcie) każe innym czekać; odpowiedź busy Medusa ponawia później."
3484
+ ]
3485
+ },
3486
+ {
3487
+ q: "invalid_signature albo stale_timestamp",
3488
+ a: [
3489
+ "invalid_signature: sekret w Medusie i w moście jest różny. stale_timestamp: zegary różnią się o ponad 5 minut. Zakładka Most w Ustawieniach pokazuje różnicę. Żeby zmienić sekret bez przerwy, na czas zmiany wpisz stary w previousSecret (Medusa) i PreviousSecret (most)."
3490
+ ]
3491
+ },
3492
+ {
3493
+ q: "WZ wystawione w Subiekcie nie pojawia się w Medusie",
3494
+ a: [
3495
+ "Wystawiaj WZ z ZK: Subiekt przenosi wtedy uwagi ZK ze znacznikiem zamówienia [medusa:order_...] na WZ, a po tym znaczniku most znajduje zamówienie. WZ wpisane ręcznie znacznika nie ma. Most szuka nowych WZ co 2 minuty, a Medusa czyta kanał co 2 minuty; przy pierwszym uruchomieniu obserwator zaczyna od najnowszego WZ i nie ogłasza starszych."
3496
+ ]
3497
+ },
3498
+ {
3499
+ q: "Czy zamówienie może dostać dwa ZK albo dwie faktury?",
3500
+ a: [
3501
+ "Nie. Most wpisuje znacznik zamówienia do każdego dokumentu i szuka go przed założeniem nowego, pod blokadą zamówienia i drugi raz pod blokadą Sfery. Medusa trzyma jedno zadanie na zamówienie i przejmuje je atomowo. Niejasna odpowiedź (przekroczony czas) zmienia status na Niejasna odpowiedź, a kolejna próba najpierw pyta most."
3502
+ ]
3503
+ },
3504
+ {
3505
+ q: "Dokończ ręcznie w Subiekcie po anulowaniu",
3506
+ a: [
3507
+ "Sfera nie ustawi statusu ZK Unieważnione. Most oznacza ZK na początku uwag oraz flagą własną, jeśli ją masz; status ustawia człowiek. Gdy istnieje już WZ albo dokument sprzedaży, anulowanie jest odrzucane (document_locked): potraktuj je jako zwrot albo korektę w Subiekcie."
3508
+ ]
3509
+ },
3510
+ {
3511
+ q: "Pozycje bez towaru w Subiekcie (unmatched_lines)",
3512
+ a: [
3513
+ "Most dopasowuje każdą pozycję najpierw po EAN, potem po SKU względem symbolu towaru, i nie zapisuje nic, gdy choć jedna pozycja nie ma towaru. Popraw EAN w Subiekcie (Miary, Kod kreskowy) albo SKU, potem Wyślij ponownie. Usługi i karty podarunkowe bez kodów można pominąć opcją omitLinesWithoutCode."
3514
+ ]
3515
+ },
3516
+ {
3517
+ q: "--check przechodzi, ale usługa nie loguje się do SQL Server",
3518
+ a: [
3519
+ "--check działa na Twoim koncie, usługa na własnym koncie Windows. Daj temu kontu dostęp do bazy nexo albo uruchom --check jako to konto."
3520
+ ]
3521
+ },
3522
+ {
3523
+ q: "Kontrahenci się nie zakładają",
3524
+ a: [
3525
+ "Muszą się zgadzać cztery przełączniki: Bridge:Subiekt:BuyerMode customer, Bridge:Subiekt:CreateContractors true, createContractors w medusa-config.ts i zapis kontrahentów włączony tutaj. Niepoprawny NIP nigdy nie zakłada kontrahenta; kolejka pokazuje ostrzeżenie."
3526
+ ]
3527
+ },
3528
+ {
3529
+ q: "Wariantu brakuje w planie cen",
3530
+ a: [
3531
+ "Ceny dopasowują się po EAN, potem po SKU równym symbolowi, bez obcinania końcówek SKU, więc hurtowy wariant -WH zachowuje swoją cenę. Towar bez ceny w poziomie, poziom w innej walucie albo zdublowany EAN nigdy nie dają zmiany."
3532
+ ]
3533
+ },
3534
+ {
3535
+ q: "Komputer z mostem był wyłączony przez weekend",
3536
+ a: [
3537
+ "Nic nie ginie: każde wywołanie jest najpierw zadaniem i jest ponawiane przez około dwa i pół dnia. Zadania, które nadal zawodzą, czekają potem w Wymagają uwagi z przyciskiem Wyślij ponownie."
3538
+ ]
3539
+ }
3540
+ ]
3541
+ }
3542
+ }
3543
+ };
3544
+ const pl = typeset(pl$1);
3545
+ const i18nTranslations0 = {
3546
+ en: { subiekt: en },
3547
+ pl: { subiekt: pl }
3548
+ };
3549
+ const widgetModule = { widgets: [
3550
+ {
3551
+ Component: SubiektOrderWidget,
3552
+ zone: ["order.details.side.after"]
3553
+ }
3554
+ ] };
3555
+ const routeModule = {
3556
+ routes: [
3557
+ {
3558
+ Component: SubiektPage,
3559
+ path: "/subiekt"
3560
+ }
3561
+ ]
3562
+ };
3563
+ const menuItemModule = {
3564
+ menuItems: [
3565
+ {
3566
+ label: config.label,
3567
+ icon: config.icon,
3568
+ path: "/subiekt",
3569
+ nested: void 0,
3570
+ rank: void 0,
3571
+ translationNs: void 0
3572
+ }
3573
+ ]
3574
+ };
3575
+ const formModule = { customFields: {} };
3576
+ const displayModule = {
3577
+ displays: {}
3578
+ };
3579
+ const i18nModule = { resources: i18nTranslations0 };
3580
+ const plugin = {
3581
+ widgetModule,
3582
+ routeModule,
3583
+ menuItemModule,
3584
+ formModule,
3585
+ displayModule,
3586
+ i18nModule
3587
+ };
3588
+ module.exports = plugin;