@open-mercato/core 0.4.7-develop-78d7541539 → 0.4.7-develop-74069040de

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 (187) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/modules/catalog/api/bulk-delete/route.js +86 -0
  3. package/dist/modules/catalog/api/bulk-delete/route.js.map +7 -0
  4. package/dist/modules/catalog/api/prices/route.js +39 -6
  5. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  6. package/dist/modules/catalog/api/products/route.js +6 -11
  7. package/dist/modules/catalog/api/products/route.js.map +2 -2
  8. package/dist/modules/catalog/commands/products.js +2 -0
  9. package/dist/modules/catalog/commands/products.js.map +2 -2
  10. package/dist/modules/catalog/components/products/ProductsDataTable.js +9 -1
  11. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  12. package/dist/modules/catalog/lib/bulkDelete.js +70 -0
  13. package/dist/modules/catalog/lib/bulkDelete.js.map +7 -0
  14. package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js +185 -0
  15. package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js.map +7 -0
  16. package/dist/modules/catalog/widgets/injection-table.js +9 -1
  17. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  18. package/dist/modules/catalog/workers/catalog-product-bulk-delete.js +40 -0
  19. package/dist/modules/catalog/workers/catalog-product-bulk-delete.js.map +7 -0
  20. package/dist/modules/data_sync/api/options.js +52 -0
  21. package/dist/modules/data_sync/api/options.js.map +7 -0
  22. package/dist/modules/data_sync/api/run.js +30 -35
  23. package/dist/modules/data_sync/api/run.js.map +2 -2
  24. package/dist/modules/data_sync/api/runs/[id]/cancel.js +2 -2
  25. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  26. package/dist/modules/data_sync/api/runs/[id]/retry.js +15 -30
  27. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  28. package/dist/modules/data_sync/api/schedules/[id]/route.js +109 -0
  29. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +7 -0
  30. package/dist/modules/data_sync/api/schedules/route.js +72 -0
  31. package/dist/modules/data_sync/api/schedules/route.js.map +7 -0
  32. package/dist/modules/data_sync/api/schedules/serialize.js +21 -0
  33. package/dist/modules/data_sync/api/schedules/serialize.js.map +7 -0
  34. package/dist/modules/data_sync/backend/data-sync/page.js +656 -47
  35. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  36. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +116 -34
  37. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  38. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +394 -0
  39. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +7 -0
  40. package/dist/modules/data_sync/data/validators.js +32 -0
  41. package/dist/modules/data_sync/data/validators.js.map +2 -2
  42. package/dist/modules/data_sync/di.js +2 -0
  43. package/dist/modules/data_sync/di.js.map +2 -2
  44. package/dist/modules/data_sync/lib/id-mapping.js +24 -2
  45. package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
  46. package/dist/modules/data_sync/lib/start-run.js +57 -0
  47. package/dist/modules/data_sync/lib/start-run.js.map +7 -0
  48. package/dist/modules/data_sync/lib/sync-engine.js +93 -4
  49. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  50. package/dist/modules/data_sync/lib/sync-run-service.js +5 -1
  51. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  52. package/dist/modules/data_sync/lib/sync-schedule-service.js +138 -0
  53. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +7 -0
  54. package/dist/modules/data_sync/workers/sync-export.js +28 -2
  55. package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
  56. package/dist/modules/data_sync/workers/sync-import.js +28 -2
  57. package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
  58. package/dist/modules/data_sync/workers/sync-scheduled.js +5 -0
  59. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  60. package/dist/modules/entities/api/definitions.js +5 -2
  61. package/dist/modules/entities/api/definitions.js.map +2 -2
  62. package/dist/modules/entities/lib/field-definitions.js +3 -1
  63. package/dist/modules/entities/lib/field-definitions.js.map +2 -2
  64. package/dist/modules/integrations/api/[id]/route.js +14 -15
  65. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  66. package/dist/modules/integrations/api/route.js +3 -3
  67. package/dist/modules/integrations/api/route.js.map +2 -2
  68. package/dist/modules/integrations/backend/integrations/[id]/page.js +148 -33
  69. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  70. package/dist/modules/integrations/lib/state-service.js +15 -1
  71. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  72. package/dist/modules/messages/api/[id]/route.js +24 -22
  73. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  74. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  75. package/dist/modules/progress/api/active/route.js +3 -1
  76. package/dist/modules/progress/api/active/route.js.map +2 -2
  77. package/dist/modules/progress/api/jobs/[id]/route.js +1 -1
  78. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  79. package/dist/modules/progress/api/jobs/route.js +1 -1
  80. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  81. package/dist/modules/progress/lib/events.js.map +1 -1
  82. package/dist/modules/progress/lib/progressService.js.map +2 -2
  83. package/dist/modules/progress/lib/progressServiceImpl.js +42 -1
  84. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  85. package/dist/modules/query_index/lib/document.js +35 -1
  86. package/dist/modules/query_index/lib/document.js.map +2 -2
  87. package/dist/modules/query_index/lib/engine.js +91 -4
  88. package/dist/modules/query_index/lib/engine.js.map +2 -2
  89. package/dist/modules/query_index/lib/indexer.js +2 -0
  90. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  91. package/dist/modules/sales/api/adjustment-kinds/route.js +3 -9
  92. package/dist/modules/sales/api/adjustment-kinds/route.js.map +2 -2
  93. package/dist/modules/sales/api/channels/route.js +3 -10
  94. package/dist/modules/sales/api/channels/route.js.map +2 -2
  95. package/dist/modules/sales/api/delivery-windows/route.js +3 -10
  96. package/dist/modules/sales/api/delivery-windows/route.js.map +2 -2
  97. package/dist/modules/sales/api/payment-methods/route.js +3 -11
  98. package/dist/modules/sales/api/payment-methods/route.js.map +2 -2
  99. package/dist/modules/sales/api/price-kinds/route.js +3 -5
  100. package/dist/modules/sales/api/price-kinds/route.js.map +2 -2
  101. package/dist/modules/sales/api/shipping-methods/route.js +3 -11
  102. package/dist/modules/sales/api/shipping-methods/route.js.map +2 -2
  103. package/dist/modules/sales/api/tags/route.js +3 -9
  104. package/dist/modules/sales/api/tags/route.js.map +2 -2
  105. package/dist/modules/sales/api/tax-rates/route.js +3 -13
  106. package/dist/modules/sales/api/tax-rates/route.js.map +2 -2
  107. package/dist/modules/sales/api/utils.js +9 -0
  108. package/dist/modules/sales/api/utils.js.map +2 -2
  109. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +3 -9
  110. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +2 -2
  111. package/dist/modules/workflows/api/definitions/[id]/route.js +3 -2
  112. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  113. package/dist/modules/workflows/api/definitions/route.js +4 -3
  114. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  115. package/dist/modules/workflows/api/definitions/serialize.js +25 -0
  116. package/dist/modules/workflows/api/definitions/serialize.js.map +7 -0
  117. package/package.json +3 -3
  118. package/src/modules/catalog/api/bulk-delete/route.ts +93 -0
  119. package/src/modules/catalog/api/prices/route.ts +53 -6
  120. package/src/modules/catalog/api/products/route.ts +6 -11
  121. package/src/modules/catalog/commands/products.ts +2 -0
  122. package/src/modules/catalog/components/products/ProductsDataTable.tsx +8 -0
  123. package/src/modules/catalog/i18n/de.json +10 -0
  124. package/src/modules/catalog/i18n/en.json +10 -0
  125. package/src/modules/catalog/i18n/es.json +10 -0
  126. package/src/modules/catalog/i18n/pl.json +10 -0
  127. package/src/modules/catalog/lib/bulkDelete.ts +106 -0
  128. package/src/modules/catalog/widgets/injection/product-bulk-delete/widget.ts +242 -0
  129. package/src/modules/catalog/widgets/injection-table.ts +8 -0
  130. package/src/modules/catalog/workers/catalog-product-bulk-delete.ts +48 -0
  131. package/src/modules/data_sync/AGENTS.md +11 -3
  132. package/src/modules/data_sync/api/options.ts +58 -0
  133. package/src/modules/data_sync/api/run.ts +34 -36
  134. package/src/modules/data_sync/api/runs/[id]/cancel.ts +2 -2
  135. package/src/modules/data_sync/api/runs/[id]/retry.ts +14 -31
  136. package/src/modules/data_sync/api/schedules/[id]/route.ts +130 -0
  137. package/src/modules/data_sync/api/schedules/route.ts +77 -0
  138. package/src/modules/data_sync/api/schedules/serialize.ts +31 -0
  139. package/src/modules/data_sync/backend/data-sync/page.tsx +756 -2
  140. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +179 -53
  141. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +512 -0
  142. package/src/modules/data_sync/data/validators.ts +35 -0
  143. package/src/modules/data_sync/di.ts +6 -0
  144. package/src/modules/data_sync/i18n/de.json +72 -0
  145. package/src/modules/data_sync/i18n/en.json +72 -0
  146. package/src/modules/data_sync/i18n/es.json +72 -0
  147. package/src/modules/data_sync/i18n/pl.json +72 -0
  148. package/src/modules/data_sync/lib/adapter.ts +4 -1
  149. package/src/modules/data_sync/lib/id-mapping.ts +32 -2
  150. package/src/modules/data_sync/lib/start-run.ts +90 -0
  151. package/src/modules/data_sync/lib/sync-engine.ts +111 -4
  152. package/src/modules/data_sync/lib/sync-run-service.ts +5 -1
  153. package/src/modules/data_sync/lib/sync-schedule-service.ts +207 -0
  154. package/src/modules/data_sync/workers/sync-export.ts +33 -2
  155. package/src/modules/data_sync/workers/sync-import.ts +33 -2
  156. package/src/modules/data_sync/workers/sync-scheduled.ts +7 -0
  157. package/src/modules/entities/api/definitions.ts +12 -2
  158. package/src/modules/entities/lib/field-definitions.ts +2 -0
  159. package/src/modules/integrations/AGENTS.md +16 -3
  160. package/src/modules/integrations/api/[id]/route.ts +14 -15
  161. package/src/modules/integrations/api/route.ts +3 -3
  162. package/src/modules/integrations/backend/integrations/[id]/page.tsx +176 -54
  163. package/src/modules/integrations/lib/state-service.ts +25 -1
  164. package/src/modules/messages/api/[id]/route.ts +25 -22
  165. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +3 -3
  166. package/src/modules/progress/api/active/route.ts +4 -1
  167. package/src/modules/progress/api/jobs/[id]/route.ts +1 -1
  168. package/src/modules/progress/api/jobs/route.ts +1 -1
  169. package/src/modules/progress/lib/events.ts +6 -0
  170. package/src/modules/progress/lib/progressService.ts +1 -0
  171. package/src/modules/progress/lib/progressServiceImpl.ts +47 -1
  172. package/src/modules/query_index/lib/document.ts +52 -1
  173. package/src/modules/query_index/lib/engine.ts +104 -4
  174. package/src/modules/query_index/lib/indexer.ts +2 -0
  175. package/src/modules/sales/api/adjustment-kinds/route.ts +3 -9
  176. package/src/modules/sales/api/channels/route.ts +3 -10
  177. package/src/modules/sales/api/delivery-windows/route.ts +3 -10
  178. package/src/modules/sales/api/payment-methods/route.ts +3 -11
  179. package/src/modules/sales/api/price-kinds/route.ts +3 -5
  180. package/src/modules/sales/api/shipping-methods/route.ts +3 -11
  181. package/src/modules/sales/api/tags/route.ts +3 -9
  182. package/src/modules/sales/api/tax-rates/route.ts +3 -13
  183. package/src/modules/sales/api/utils.ts +9 -0
  184. package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +3 -9
  185. package/src/modules/workflows/api/definitions/[id]/route.ts +3 -2
  186. package/src/modules/workflows/api/definitions/route.ts +4 -3
  187. package/src/modules/workflows/api/definitions/serialize.ts +23 -0
@@ -1,15 +1,37 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import Link from "next/link";
4
5
  import { useRouter } from "next/navigation";
5
6
  import { Page, PageBody } from "@open-mercato/ui/backend/Page";
6
7
  import { DataTable } from "@open-mercato/ui/backend/DataTable";
8
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
7
9
  import { Badge } from "@open-mercato/ui/primitives/badge";
10
+ import { Card, CardContent, CardHeader, CardTitle } from "@open-mercato/ui/primitives/card";
11
+ import { Button } from "@open-mercato/ui/primitives/button";
12
+ import { Input } from "@open-mercato/ui/primitives/input";
13
+ import { Label } from "@open-mercato/ui/primitives/label";
14
+ import { Notice } from "@open-mercato/ui/primitives/Notice";
15
+ import { Separator } from "@open-mercato/ui/primitives/separator";
16
+ import { Switch } from "@open-mercato/ui/primitives/switch";
8
17
  import { RowActions } from "@open-mercato/ui/backend/RowActions";
9
18
  import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
10
19
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
11
20
  import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
12
21
  import { useT } from "@open-mercato/shared/lib/i18n/context";
22
+ import {
23
+ ArrowRightLeft,
24
+ Boxes,
25
+ CalendarClock,
26
+ CircleAlert,
27
+ Clock3,
28
+ Gauge,
29
+ Play,
30
+ PlugZap,
31
+ Repeat,
32
+ Settings2,
33
+ ShieldCheck
34
+ } from "lucide-react";
13
35
  const STATUS_STYLES = {
14
36
  pending: "bg-gray-100 text-gray-800",
15
37
  running: "bg-blue-100 text-blue-800",
@@ -18,18 +40,78 @@ const STATUS_STYLES = {
18
40
  cancelled: "bg-yellow-100 text-yellow-800",
19
41
  paused: "bg-orange-100 text-orange-800"
20
42
  };
43
+ const DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
44
+ function getSummaryBadgeStyle(kind) {
45
+ if (kind === "enabled" || kind === "ready") {
46
+ return {
47
+ variant: "outline",
48
+ className: "border-emerald-500/30 bg-emerald-500/15 text-emerald-200"
49
+ };
50
+ }
51
+ if (kind === "disabled" || kind === "missing") {
52
+ return {
53
+ variant: "outline",
54
+ className: "border-red-500/30 bg-red-500/15 text-red-200"
55
+ };
56
+ }
57
+ if (kind === "paused") {
58
+ return {
59
+ variant: "outline",
60
+ className: "border-amber-500/30 bg-amber-500/15 text-amber-200"
61
+ };
62
+ }
63
+ if (kind === "scheduled") {
64
+ return {
65
+ variant: "outline",
66
+ className: "border-sky-500/30 bg-sky-500/15 text-sky-200"
67
+ };
68
+ }
69
+ return {
70
+ variant: "outline",
71
+ className: "border-muted-foreground/20 bg-muted/40 text-muted-foreground"
72
+ };
73
+ }
74
+ function formatEntityTypeLabel(entityType) {
75
+ return entityType.replace(/[_-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
76
+ }
77
+ function buildDefaultScheduleState(entityType) {
78
+ const normalized = entityType.trim().toLowerCase();
79
+ const longerInterval = normalized === "categories" || normalized === "attributes";
80
+ return {
81
+ scheduleType: "interval",
82
+ scheduleValue: longerInterval ? "6h" : "1h",
83
+ timezone: DEFAULT_TIMEZONE,
84
+ fullSync: normalized !== "products",
85
+ isEnabled: true,
86
+ lastRunAt: null
87
+ };
88
+ }
21
89
  function SyncRunsDashboardPage() {
22
90
  const router = useRouter();
23
91
  const [rows, setRows] = React.useState([]);
92
+ const [options, setOptions] = React.useState([]);
24
93
  const [page, setPage] = React.useState(1);
25
94
  const [total, setTotal] = React.useState(0);
26
95
  const [totalPages, setTotalPages] = React.useState(1);
27
96
  const [search, setSearch] = React.useState("");
28
97
  const [filterValues, setFilterValues] = React.useState({});
29
98
  const [isLoading, setIsLoading] = React.useState(true);
99
+ const [isLoadingOptions, setIsLoadingOptions] = React.useState(true);
100
+ const [selectedIntegrationId, setSelectedIntegrationId] = React.useState("");
101
+ const [selectedEntityType, setSelectedEntityType] = React.useState("");
102
+ const [selectedDirection, setSelectedDirection] = React.useState("import");
103
+ const [batchSize, setBatchSize] = React.useState("100");
104
+ const [fullSync, setFullSync] = React.useState(false);
105
+ const [scheduleEditor, setScheduleEditor] = React.useState(() => buildDefaultScheduleState(""));
106
+ const [isLoadingSchedule, setIsLoadingSchedule] = React.useState(false);
107
+ const [isSavingSchedule, setIsSavingSchedule] = React.useState(false);
108
+ const [isDeletingSchedule, setIsDeletingSchedule] = React.useState(false);
30
109
  const [reloadToken, setReloadToken] = React.useState(0);
31
110
  const scopeVersion = useOrganizationScopeVersion();
32
111
  const t = useT();
112
+ const { runMutation } = useGuardedMutation({
113
+ contextId: "data_sync.dashboard"
114
+ });
33
115
  React.useEffect(() => {
34
116
  let cancelled = false;
35
117
  async function load() {
@@ -63,6 +145,99 @@ function SyncRunsDashboardPage() {
63
145
  cancelled = true;
64
146
  };
65
147
  }, [page, filterValues, reloadToken, scopeVersion, t]);
148
+ React.useEffect(() => {
149
+ let cancelled = false;
150
+ async function loadOptions() {
151
+ setIsLoadingOptions(true);
152
+ const fallback = { items: [] };
153
+ const call = await apiCall("/api/data_sync/options", void 0, { fallback });
154
+ if (!cancelled) {
155
+ if (!call.ok) {
156
+ flash(t("data_sync.dashboard.loadError"), "error");
157
+ setOptions([]);
158
+ setIsLoadingOptions(false);
159
+ return;
160
+ }
161
+ const nextItems = Array.isArray(call.result?.items) ? call.result.items : [];
162
+ setOptions(nextItems);
163
+ setSelectedIntegrationId((current) => {
164
+ if (current && nextItems.some((item) => item.integrationId === current)) return current;
165
+ return nextItems[0]?.integrationId ?? "";
166
+ });
167
+ setIsLoadingOptions(false);
168
+ }
169
+ }
170
+ void loadOptions();
171
+ return () => {
172
+ cancelled = true;
173
+ };
174
+ }, [scopeVersion, t]);
175
+ const selectedIntegration = React.useMemo(
176
+ () => options.find((item) => item.integrationId === selectedIntegrationId) ?? null,
177
+ [options, selectedIntegrationId]
178
+ );
179
+ const entityOptions = React.useMemo(
180
+ () => selectedIntegration?.supportedEntities ?? [],
181
+ [selectedIntegration]
182
+ );
183
+ React.useEffect(() => {
184
+ if (!selectedIntegration) {
185
+ setSelectedEntityType("");
186
+ return;
187
+ }
188
+ setSelectedEntityType((current) => current && selectedIntegration.supportedEntities.includes(current) ? current : selectedIntegration.supportedEntities[0] ?? "");
189
+ setSelectedDirection(selectedIntegration.direction === "export" ? "export" : "import");
190
+ }, [selectedIntegration]);
191
+ React.useEffect(() => {
192
+ if (!selectedIntegration || !selectedEntityType) {
193
+ setScheduleEditor(buildDefaultScheduleState(selectedEntityType));
194
+ return;
195
+ }
196
+ const currentIntegration = selectedIntegration;
197
+ let cancelled = false;
198
+ async function loadSchedule() {
199
+ setIsLoadingSchedule(true);
200
+ const integrationId = currentIntegration.integrationId;
201
+ const params = new URLSearchParams({
202
+ integrationId,
203
+ entityType: selectedEntityType,
204
+ direction: selectedDirection,
205
+ page: "1",
206
+ pageSize: "1"
207
+ });
208
+ const fallback = { items: [] };
209
+ const call = await apiCall(`/api/data_sync/schedules?${params.toString()}`, void 0, { fallback });
210
+ if (cancelled) return;
211
+ if (!call.ok) {
212
+ setScheduleEditor(buildDefaultScheduleState(selectedEntityType));
213
+ setIsLoadingSchedule(false);
214
+ return;
215
+ }
216
+ const record = Array.isArray(call.result?.items) ? call.result?.items[0] : void 0;
217
+ if (!record) {
218
+ setScheduleEditor(buildDefaultScheduleState(selectedEntityType));
219
+ setIsLoadingSchedule(false);
220
+ return;
221
+ }
222
+ setScheduleEditor({
223
+ id: record.id,
224
+ scheduleType: record.scheduleType,
225
+ scheduleValue: record.scheduleValue,
226
+ timezone: record.timezone,
227
+ fullSync: record.fullSync,
228
+ isEnabled: record.isEnabled,
229
+ lastRunAt: record.lastRunAt
230
+ });
231
+ setIsLoadingSchedule(false);
232
+ }
233
+ void loadSchedule();
234
+ return () => {
235
+ cancelled = true;
236
+ };
237
+ }, [selectedDirection, selectedEntityType, selectedIntegration, scopeVersion]);
238
+ const updateScheduleEditor = React.useCallback((changes) => {
239
+ setScheduleEditor((current) => ({ ...current, ...changes }));
240
+ }, []);
66
241
  const handleCancel = React.useCallback(async (row) => {
67
242
  const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/cancel`, {
68
243
  method: "POST"
@@ -99,6 +274,140 @@ function SyncRunsDashboardPage() {
99
274
  setFilterValues({});
100
275
  setPage(1);
101
276
  }, []);
277
+ const handleStartSync = React.useCallback(async () => {
278
+ if (!selectedIntegration || !selectedEntityType) return;
279
+ const parsedBatchSize = Number.parseInt(batchSize, 10);
280
+ if (!Number.isFinite(parsedBatchSize) || parsedBatchSize < 1 || parsedBatchSize > 1e3) {
281
+ flash(t("data_sync.dashboard.start.invalidBatchSize", "Batch size must be between 1 and 1000."), "error");
282
+ return;
283
+ }
284
+ try {
285
+ const call = await runMutation({
286
+ operation: () => apiCall("/api/data_sync/run", {
287
+ method: "POST",
288
+ headers: { "Content-Type": "application/json" },
289
+ body: JSON.stringify({
290
+ integrationId: selectedIntegration.integrationId,
291
+ entityType: selectedEntityType,
292
+ direction: selectedDirection,
293
+ batchSize: parsedBatchSize,
294
+ fullSync
295
+ })
296
+ }, { fallback: null }),
297
+ mutationPayload: {
298
+ integrationId: selectedIntegration.integrationId,
299
+ entityType: selectedEntityType,
300
+ direction: selectedDirection,
301
+ batchSize: parsedBatchSize,
302
+ fullSync
303
+ },
304
+ context: {
305
+ operation: "create",
306
+ actionId: "start-sync-run",
307
+ integrationId: selectedIntegration.integrationId
308
+ }
309
+ });
310
+ if (!call.ok || !call.result?.id) {
311
+ flash(call.result?.error ?? t("data_sync.dashboard.start.error", "Failed to start sync run"), "error");
312
+ return;
313
+ }
314
+ flash(t("data_sync.dashboard.start.success", "Sync run started"), "success");
315
+ setReloadToken((token) => token + 1);
316
+ router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`);
317
+ } catch (error) {
318
+ const message = error instanceof Error ? error.message : t("data_sync.dashboard.start.error", "Failed to start sync run");
319
+ flash(message, "error");
320
+ }
321
+ }, [batchSize, fullSync, router, runMutation, selectedDirection, selectedEntityType, selectedIntegration, t]);
322
+ const handleSaveSchedule = React.useCallback(async () => {
323
+ if (!selectedIntegration || !selectedEntityType) return;
324
+ if (scheduleEditor.scheduleValue.trim().length === 0) {
325
+ flash(t("data_sync.dashboard.schedule.invalidValue", "Provide a schedule value before saving."), "error");
326
+ return;
327
+ }
328
+ setIsSavingSchedule(true);
329
+ try {
330
+ const call = await runMutation({
331
+ operation: () => apiCall("/api/data_sync/schedules", {
332
+ method: "POST",
333
+ headers: { "Content-Type": "application/json" },
334
+ body: JSON.stringify({
335
+ integrationId: selectedIntegration.integrationId,
336
+ entityType: selectedEntityType,
337
+ direction: selectedDirection,
338
+ scheduleType: scheduleEditor.scheduleType,
339
+ scheduleValue: scheduleEditor.scheduleValue.trim(),
340
+ timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,
341
+ fullSync: scheduleEditor.fullSync,
342
+ isEnabled: scheduleEditor.isEnabled
343
+ })
344
+ }, { fallback: null }),
345
+ mutationPayload: {
346
+ integrationId: selectedIntegration.integrationId,
347
+ entityType: selectedEntityType,
348
+ direction: selectedDirection,
349
+ scheduleType: scheduleEditor.scheduleType,
350
+ scheduleValue: scheduleEditor.scheduleValue.trim(),
351
+ timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,
352
+ fullSync: scheduleEditor.fullSync,
353
+ isEnabled: scheduleEditor.isEnabled
354
+ },
355
+ context: {
356
+ operation: "update",
357
+ actionId: "save-sync-schedule",
358
+ integrationId: selectedIntegration.integrationId
359
+ }
360
+ });
361
+ if (!call.ok || !call.result) {
362
+ flash(call.result?.error ?? t("data_sync.dashboard.schedule.error", "Failed to save recurring schedule"), "error");
363
+ return;
364
+ }
365
+ setScheduleEditor({
366
+ id: call.result.id,
367
+ scheduleType: call.result.scheduleType,
368
+ scheduleValue: call.result.scheduleValue,
369
+ timezone: call.result.timezone,
370
+ fullSync: call.result.fullSync,
371
+ isEnabled: call.result.isEnabled,
372
+ lastRunAt: call.result.lastRunAt
373
+ });
374
+ flash(t("data_sync.dashboard.schedule.success", "Recurring schedule saved"), "success");
375
+ } catch (error) {
376
+ const message = error instanceof Error ? error.message : t("data_sync.dashboard.schedule.error", "Failed to save recurring schedule");
377
+ flash(message, "error");
378
+ } finally {
379
+ setIsSavingSchedule(false);
380
+ }
381
+ }, [runMutation, scheduleEditor, selectedDirection, selectedEntityType, selectedIntegration, t]);
382
+ const handleDeleteSchedule = React.useCallback(async () => {
383
+ if (!scheduleEditor.id) return;
384
+ setIsDeletingSchedule(true);
385
+ try {
386
+ const call = await runMutation({
387
+ operation: () => apiCall(`/api/data_sync/schedules/${encodeURIComponent(scheduleEditor.id)}`, {
388
+ method: "DELETE"
389
+ }, { fallback: null }),
390
+ mutationPayload: {
391
+ scheduleId: scheduleEditor.id
392
+ },
393
+ context: {
394
+ operation: "delete",
395
+ actionId: "delete-sync-schedule"
396
+ }
397
+ });
398
+ if (!call.ok) {
399
+ flash(call.result?.error ?? t("data_sync.dashboard.schedule.deleteError", "Failed to remove recurring schedule"), "error");
400
+ return;
401
+ }
402
+ setScheduleEditor(buildDefaultScheduleState(selectedEntityType));
403
+ flash(t("data_sync.dashboard.schedule.deleteSuccess", "Recurring schedule removed"), "success");
404
+ } catch (error) {
405
+ const message = error instanceof Error ? error.message : t("data_sync.dashboard.schedule.deleteError", "Failed to remove recurring schedule");
406
+ flash(message, "error");
407
+ } finally {
408
+ setIsDeletingSchedule(false);
409
+ }
410
+ }, [runMutation, scheduleEditor.id, selectedEntityType, t]);
102
411
  const filters = [
103
412
  {
104
413
  id: "status",
@@ -162,53 +471,353 @@ function SyncRunsDashboardPage() {
162
471
  cell: ({ row }) => new Date(row.original.createdAt).toLocaleString()
163
472
  }
164
473
  ], [t]);
165
- return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
166
- DataTable,
167
- {
168
- title: t("data_sync.dashboard.title"),
169
- columns,
170
- data: rows,
171
- filters,
172
- filterValues,
173
- onFiltersApply: handleFiltersApply,
174
- onFiltersClear: handleFiltersClear,
175
- searchValue: search,
176
- onSearchChange: (value) => {
177
- setSearch(value);
178
- setPage(1);
179
- },
180
- perspective: { tableId: "data_sync.runs" },
181
- onRowClick: (row) => {
182
- router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`);
183
- },
184
- rowActions: (row) => /* @__PURE__ */ jsx(RowActions, { items: [
185
- {
186
- id: "view",
187
- label: t("data_sync.dashboard.actions.view"),
188
- onSelect: () => {
189
- router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`);
190
- }
191
- },
192
- ...row.status === "running" ? [{
193
- id: "cancel",
194
- label: t("data_sync.runs.detail.cancel"),
195
- destructive: true,
196
- onSelect: () => {
197
- void handleCancel(row);
198
- }
199
- }] : [],
200
- ...row.status === "failed" ? [{
201
- id: "retry",
202
- label: t("data_sync.runs.detail.retry"),
203
- onSelect: () => {
204
- void handleRetry(row);
205
- }
206
- }] : []
474
+ const canStartSelectedIntegration = Boolean(
475
+ selectedIntegration && selectedEntityType && selectedIntegration.isEnabled && selectedIntegration.hasCredentials
476
+ );
477
+ const hasSavedSchedule = Boolean(scheduleEditor.id);
478
+ const selectedEntityLabel = selectedEntityType ? formatEntityTypeLabel(selectedEntityType) : t("data_sync.dashboard.columns.entityType");
479
+ const integrationStateBadge = getSummaryBadgeStyle(selectedIntegration?.isEnabled ? "enabled" : "disabled");
480
+ const credentialsBadge = getSummaryBadgeStyle(selectedIntegration?.hasCredentials ? "ready" : "missing");
481
+ const scheduleBadge = getSummaryBadgeStyle(
482
+ hasSavedSchedule ? scheduleEditor.isEnabled ? "scheduled" : "paused" : "none"
483
+ );
484
+ return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsxs(PageBody, { className: "space-y-6", children: [
485
+ /* @__PURE__ */ jsxs(Card, { children: [
486
+ /* @__PURE__ */ jsxs(CardHeader, { className: "space-y-4", children: [
487
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between", children: [
488
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
489
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs font-medium uppercase tracking-[0.14em] text-muted-foreground", children: [
490
+ /* @__PURE__ */ jsx(Repeat, { className: "size-4" }),
491
+ /* @__PURE__ */ jsx("span", { children: t("data_sync.dashboard.start.eyebrow", "Run once or keep it recurring") })
492
+ ] }),
493
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
494
+ /* @__PURE__ */ jsx(CardTitle, { children: t("data_sync.dashboard.start.title", "Start or schedule a sync") }),
495
+ /* @__PURE__ */ jsx("p", { className: "max-w-3xl text-sm text-muted-foreground", children: t("data_sync.dashboard.start.description", "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.") })
496
+ ] })
497
+ ] }),
498
+ selectedIntegration ? /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", children: /* @__PURE__ */ jsxs(Link, { href: selectedIntegration.settingsPath, children: [
499
+ /* @__PURE__ */ jsx(Settings2, { className: "mr-2 size-4" }),
500
+ t("integrations.marketplace.configure")
501
+ ] }) }) : null
502
+ ] }),
503
+ selectedIntegration ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
504
+ /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "gap-1.5", children: [
505
+ /* @__PURE__ */ jsx(PlugZap, { className: "size-3.5" }),
506
+ selectedIntegration.title
507
+ ] }),
508
+ /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "gap-1.5", children: [
509
+ /* @__PURE__ */ jsx(ArrowRightLeft, { className: "size-3.5" }),
510
+ t(`data_sync.dashboard.direction.${selectedDirection}`)
511
+ ] }),
512
+ /* @__PURE__ */ jsxs(Badge, { variant: integrationStateBadge.variant, className: `gap-1.5 ${integrationStateBadge.className ?? ""}`, children: [
513
+ /* @__PURE__ */ jsx(ShieldCheck, { className: "size-3.5" }),
514
+ selectedIntegration.isEnabled ? t("data_sync.dashboard.start.status.enabled", "Integration enabled") : t("data_sync.dashboard.start.status.disabled", "Integration disabled")
515
+ ] }),
516
+ /* @__PURE__ */ jsxs(Badge, { variant: credentialsBadge.variant, className: `gap-1.5 ${credentialsBadge.className ?? ""}`, children: [
517
+ /* @__PURE__ */ jsx(PlugZap, { className: "size-3.5" }),
518
+ selectedIntegration.hasCredentials ? t("data_sync.dashboard.start.status.credentialsReady", "Credentials ready") : t("data_sync.dashboard.start.status.credentialsMissing", "Credentials missing")
519
+ ] }),
520
+ /* @__PURE__ */ jsxs(Badge, { variant: scheduleBadge.variant, className: `gap-1.5 ${scheduleBadge.className ?? ""}`, children: [
521
+ /* @__PURE__ */ jsx(CalendarClock, { className: "size-3.5" }),
522
+ hasSavedSchedule ? scheduleEditor.isEnabled ? t("data_sync.dashboard.schedule.status.enabled", "Recurring schedule active") : t("data_sync.dashboard.schedule.status.disabled", "Recurring schedule paused") : t("data_sync.dashboard.schedule.status.none", "No recurring schedule")
523
+ ] })
524
+ ] }) : null
207
525
  ] }),
208
- pagination: { page, pageSize: 20, total, totalPages, onPageChange: setPage },
209
- isLoading
210
- }
211
- ) }) });
526
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-6", children: [
527
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 xl:grid-cols-3", children: [
528
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 xl:col-span-1", children: [
529
+ /* @__PURE__ */ jsxs(Label, { className: "flex items-center gap-2 text-sm font-medium", children: [
530
+ /* @__PURE__ */ jsx(PlugZap, { className: "size-4 text-muted-foreground" }),
531
+ /* @__PURE__ */ jsx("span", { children: t("data_sync.dashboard.columns.integration") })
532
+ ] }),
533
+ /* @__PURE__ */ jsxs(
534
+ "select",
535
+ {
536
+ className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
537
+ value: selectedIntegrationId,
538
+ onChange: (event) => setSelectedIntegrationId(event.target.value),
539
+ disabled: isLoadingOptions || options.length === 0,
540
+ children: [
541
+ options.length === 0 ? /* @__PURE__ */ jsx("option", { value: "", children: t("integrations.marketplace.noResults", "No integrations found") }) : null,
542
+ options.map((item) => /* @__PURE__ */ jsx("option", { value: item.integrationId, children: item.title }, item.integrationId))
543
+ ]
544
+ }
545
+ )
546
+ ] }),
547
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
548
+ /* @__PURE__ */ jsxs(Label, { className: "flex items-center gap-2 text-sm font-medium", children: [
549
+ /* @__PURE__ */ jsx(Boxes, { className: "size-4 text-muted-foreground" }),
550
+ /* @__PURE__ */ jsx("span", { children: t("data_sync.dashboard.columns.entityType") })
551
+ ] }),
552
+ /* @__PURE__ */ jsx(
553
+ "select",
554
+ {
555
+ className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
556
+ value: selectedEntityType,
557
+ onChange: (event) => setSelectedEntityType(event.target.value),
558
+ disabled: entityOptions.length === 0,
559
+ children: entityOptions.map((entityType) => /* @__PURE__ */ jsx("option", { value: entityType, children: formatEntityTypeLabel(entityType) }, entityType))
560
+ }
561
+ )
562
+ ] }),
563
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
564
+ /* @__PURE__ */ jsxs(Label, { className: "flex items-center gap-2 text-sm font-medium", children: [
565
+ /* @__PURE__ */ jsx(ArrowRightLeft, { className: "size-4 text-muted-foreground" }),
566
+ /* @__PURE__ */ jsx("span", { children: t("data_sync.dashboard.columns.direction") })
567
+ ] }),
568
+ /* @__PURE__ */ jsxs(
569
+ "select",
570
+ {
571
+ className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
572
+ value: selectedDirection,
573
+ onChange: (event) => setSelectedDirection(event.target.value === "export" ? "export" : "import"),
574
+ disabled: selectedIntegration?.direction !== "bidirectional",
575
+ children: [
576
+ /* @__PURE__ */ jsx("option", { value: "import", children: t("data_sync.dashboard.direction.import") }),
577
+ selectedIntegration?.direction === "bidirectional" || selectedIntegration?.direction === "export" ? /* @__PURE__ */ jsx("option", { value: "export", children: t("data_sync.dashboard.direction.export") }) : null
578
+ ]
579
+ }
580
+ )
581
+ ] })
582
+ ] }),
583
+ selectedIntegration?.description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: selectedIntegration.description }) : null,
584
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 xl:grid-cols-2", children: [
585
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-muted/20 p-4", children: [
586
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
587
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
588
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
589
+ /* @__PURE__ */ jsx(Play, { className: "size-4 text-primary" }),
590
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t("data_sync.dashboard.start.runNowTitle", "Run once now") })
591
+ ] }),
592
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("data_sync.dashboard.start.runNowDescription", "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.") })
593
+ ] }),
594
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: selectedEntityLabel })
595
+ ] }),
596
+ /* @__PURE__ */ jsx(Separator, { className: "my-4" }),
597
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-[minmax(0,180px)_1fr]", children: [
598
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
599
+ /* @__PURE__ */ jsxs(Label, { className: "flex items-center gap-2 text-sm font-medium", children: [
600
+ /* @__PURE__ */ jsx(Gauge, { className: "size-4 text-muted-foreground" }),
601
+ /* @__PURE__ */ jsx("span", { children: t("data_sync.dashboard.start.batchSize", "Batch size") })
602
+ ] }),
603
+ /* @__PURE__ */ jsx(
604
+ Input,
605
+ {
606
+ value: batchSize,
607
+ onChange: (event) => setBatchSize(event.target.value),
608
+ inputMode: "numeric"
609
+ }
610
+ )
611
+ ] }),
612
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-background p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
613
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
614
+ /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium", children: t("data_sync.dashboard.start.fullSync", "Run as full sync") }),
615
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("data_sync.dashboard.start.fullSyncHelp", "Ignore the saved cursor and process the entire source again for this run.") })
616
+ ] }),
617
+ /* @__PURE__ */ jsx(Switch, { checked: fullSync, onCheckedChange: setFullSync })
618
+ ] }) })
619
+ ] }),
620
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap items-center justify-between gap-3", children: [
621
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("data_sync.dashboard.start.runNowFootnote", "Manual runs show progress immediately and land on the run detail page after launch.") }),
622
+ /* @__PURE__ */ jsxs(
623
+ Button,
624
+ {
625
+ type: "button",
626
+ onClick: () => void handleStartSync(),
627
+ disabled: !canStartSelectedIntegration,
628
+ children: [
629
+ /* @__PURE__ */ jsx(Play, { className: "mr-2 size-4" }),
630
+ t("data_sync.dashboard.start.submit", "Start sync")
631
+ ]
632
+ }
633
+ )
634
+ ] })
635
+ ] }),
636
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-muted/20 p-4", children: [
637
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
638
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
639
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
640
+ /* @__PURE__ */ jsx(CalendarClock, { className: "size-4 text-primary" }),
641
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t("data_sync.dashboard.schedule.title", "Recurring schedule") })
642
+ ] }),
643
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("data_sync.dashboard.schedule.description", "Save a repeating schedule for the selected integration, entity, and direction without leaving this dashboard.") })
644
+ ] }),
645
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: hasSavedSchedule ? scheduleEditor.isEnabled ? t("data_sync.dashboard.schedule.status.shortEnabled", "Scheduled") : t("data_sync.dashboard.schedule.status.shortDisabled", "Paused") : t("data_sync.dashboard.schedule.status.shortNone", "One-time only") })
646
+ ] }),
647
+ /* @__PURE__ */ jsx(Separator, { className: "my-4" }),
648
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
649
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
650
+ /* @__PURE__ */ jsxs(Label, { className: "flex items-center gap-2 text-sm font-medium", children: [
651
+ /* @__PURE__ */ jsx(Clock3, { className: "size-4 text-muted-foreground" }),
652
+ /* @__PURE__ */ jsx("span", { children: t("data_sync.dashboard.schedule.type", "Schedule type") })
653
+ ] }),
654
+ /* @__PURE__ */ jsxs(
655
+ "select",
656
+ {
657
+ className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
658
+ value: scheduleEditor.scheduleType,
659
+ onChange: (event) => updateScheduleEditor({
660
+ scheduleType: event.target.value === "cron" ? "cron" : "interval"
661
+ }),
662
+ disabled: isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType,
663
+ children: [
664
+ /* @__PURE__ */ jsx("option", { value: "interval", children: t("data_sync.dashboard.schedule.interval", "Interval") }),
665
+ /* @__PURE__ */ jsx("option", { value: "cron", children: t("data_sync.dashboard.schedule.cron", "Cron") })
666
+ ]
667
+ }
668
+ )
669
+ ] }),
670
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
671
+ /* @__PURE__ */ jsxs(Label, { className: "flex items-center gap-2 text-sm font-medium", children: [
672
+ /* @__PURE__ */ jsx(CalendarClock, { className: "size-4 text-muted-foreground" }),
673
+ /* @__PURE__ */ jsx("span", { children: scheduleEditor.scheduleType === "cron" ? t("data_sync.dashboard.schedule.cronValue", "Cron expression") : t("data_sync.dashboard.schedule.intervalValue", "Interval") })
674
+ ] }),
675
+ /* @__PURE__ */ jsx(
676
+ Input,
677
+ {
678
+ value: scheduleEditor.scheduleValue,
679
+ onChange: (event) => updateScheduleEditor({ scheduleValue: event.target.value }),
680
+ disabled: isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType,
681
+ placeholder: scheduleEditor.scheduleType === "cron" ? "0 * * * *" : "1h"
682
+ }
683
+ ),
684
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: scheduleEditor.scheduleType === "cron" ? t("data_sync.dashboard.schedule.cronHelp", "Example: `0 * * * *` runs at the start of every hour.") : t("data_sync.dashboard.schedule.intervalHelp", "Example: `1h`, `6h`, or `24h` for repeating intervals.") })
685
+ ] }),
686
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 sm:col-span-2", children: [
687
+ /* @__PURE__ */ jsxs(Label, { className: "flex items-center gap-2 text-sm font-medium", children: [
688
+ /* @__PURE__ */ jsx(Clock3, { className: "size-4 text-muted-foreground" }),
689
+ /* @__PURE__ */ jsx("span", { children: t("data_sync.dashboard.schedule.timezone", "Timezone") })
690
+ ] }),
691
+ /* @__PURE__ */ jsx(
692
+ Input,
693
+ {
694
+ value: scheduleEditor.timezone,
695
+ onChange: (event) => updateScheduleEditor({ timezone: event.target.value }),
696
+ disabled: isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType
697
+ }
698
+ )
699
+ ] })
700
+ ] }),
701
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid gap-3", children: [
702
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-background p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
703
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
704
+ /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium", children: t("data_sync.dashboard.schedule.fullSync", "Run scheduled jobs as full sync") }),
705
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("data_sync.dashboard.schedule.fullSyncHelp", "When enabled, every recurring run starts from the beginning instead of the saved cursor.") })
706
+ ] }),
707
+ /* @__PURE__ */ jsx(
708
+ Switch,
709
+ {
710
+ checked: scheduleEditor.fullSync,
711
+ onCheckedChange: (checked) => updateScheduleEditor({ fullSync: checked }),
712
+ disabled: isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType
713
+ }
714
+ )
715
+ ] }) }),
716
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-background p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
717
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
718
+ /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium", children: t("data_sync.dashboard.schedule.enabled", "Schedule enabled") }),
719
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("data_sync.dashboard.schedule.enabledHelp", "Pause the recurring job without deleting the schedule definition.") })
720
+ ] }),
721
+ /* @__PURE__ */ jsx(
722
+ Switch,
723
+ {
724
+ checked: scheduleEditor.isEnabled,
725
+ onCheckedChange: (checked) => updateScheduleEditor({ isEnabled: checked }),
726
+ disabled: isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType
727
+ }
728
+ )
729
+ ] }) })
730
+ ] }),
731
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap items-center justify-between gap-3", children: [
732
+ /* @__PURE__ */ jsx("div", { className: "space-y-1 text-xs text-muted-foreground", children: /* @__PURE__ */ jsx("div", { children: hasSavedSchedule ? scheduleEditor.lastRunAt ? t("data_sync.dashboard.schedule.lastRun", "Last scheduled run: {value}", {
733
+ value: new Date(scheduleEditor.lastRunAt).toLocaleString()
734
+ }) : t("data_sync.dashboard.schedule.neverRun", "Saved, but no scheduled execution has completed yet.") : t("data_sync.dashboard.schedule.none", "No recurring schedule saved for this target yet.") }) }),
735
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
736
+ /* @__PURE__ */ jsx(
737
+ Button,
738
+ {
739
+ type: "button",
740
+ variant: "outline",
741
+ onClick: () => void handleDeleteSchedule(),
742
+ disabled: !hasSavedSchedule || isDeletingSchedule,
743
+ children: isDeletingSchedule ? t("data_sync.dashboard.schedule.deleting", "Removing...") : t("data_sync.dashboard.schedule.delete", "Remove schedule")
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsxs(
747
+ Button,
748
+ {
749
+ type: "button",
750
+ variant: "outline",
751
+ onClick: () => void handleSaveSchedule(),
752
+ disabled: isSavingSchedule || !selectedIntegration || !selectedEntityType,
753
+ children: [
754
+ /* @__PURE__ */ jsx(CalendarClock, { className: "mr-2 size-4" }),
755
+ isSavingSchedule ? t("data_sync.dashboard.schedule.saving", "Saving...") : t("data_sync.dashboard.schedule.save", "Save recurring schedule")
756
+ ]
757
+ }
758
+ )
759
+ ] })
760
+ ] })
761
+ ] })
762
+ ] }),
763
+ selectedIntegration && !selectedIntegration.isEnabled ? /* @__PURE__ */ jsx(Notice, { compact: true, variant: "warning", children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
764
+ /* @__PURE__ */ jsx(CircleAlert, { className: "size-4" }),
765
+ /* @__PURE__ */ jsx("span", { children: t("integrations.detail.state.disabled", "This integration is disabled. Enable it on the integration settings page before starting a sync.") })
766
+ ] }) }) : null,
767
+ selectedIntegration && !selectedIntegration.hasCredentials ? /* @__PURE__ */ jsx(Notice, { compact: true, variant: "warning", children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
768
+ /* @__PURE__ */ jsx(CircleAlert, { className: "size-4" }),
769
+ /* @__PURE__ */ jsx("span", { children: t("integrations.detail.credentials.notConfigured", "Credentials are not configured yet. Save the integration credentials before starting a sync.") })
770
+ ] }) }) : null
771
+ ] })
772
+ ] }),
773
+ /* @__PURE__ */ jsx(
774
+ DataTable,
775
+ {
776
+ title: t("data_sync.dashboard.title"),
777
+ columns,
778
+ data: rows,
779
+ filters,
780
+ filterValues,
781
+ onFiltersApply: handleFiltersApply,
782
+ onFiltersClear: handleFiltersClear,
783
+ searchValue: search,
784
+ onSearchChange: (value) => {
785
+ setSearch(value);
786
+ setPage(1);
787
+ },
788
+ perspective: { tableId: "data_sync.runs" },
789
+ onRowClick: (row) => {
790
+ router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`);
791
+ },
792
+ rowActions: (row) => /* @__PURE__ */ jsx(RowActions, { items: [
793
+ {
794
+ id: "view",
795
+ label: t("data_sync.dashboard.actions.view"),
796
+ onSelect: () => {
797
+ router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`);
798
+ }
799
+ },
800
+ ...row.status === "running" ? [{
801
+ id: "cancel",
802
+ label: t("data_sync.runs.detail.cancel"),
803
+ destructive: true,
804
+ onSelect: () => {
805
+ void handleCancel(row);
806
+ }
807
+ }] : [],
808
+ ...row.status === "failed" ? [{
809
+ id: "retry",
810
+ label: t("data_sync.runs.detail.retry"),
811
+ onSelect: () => {
812
+ void handleRetry(row);
813
+ }
814
+ }] : []
815
+ ] }),
816
+ pagination: { page, pageSize: 20, total, totalPages, onPageChange: setPage },
817
+ isLoading
818
+ }
819
+ )
820
+ ] }) });
212
821
  }
213
822
  export {
214
823
  SyncRunsDashboardPage as default