@odooconnector/medusa-odoo-connector 0.1.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.
- package/.medusa/server/src/admin/index.js +2211 -0
- package/.medusa/server/src/admin/index.mjs +2212 -0
- package/.medusa/server/src/api/admin/odoo/billing/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/clear-orders/route.js +33 -0
- package/.medusa/server/src/api/admin/odoo/clear-products/route.js +30 -0
- package/.medusa/server/src/api/admin/odoo/config/route.js +44 -0
- package/.medusa/server/src/api/admin/odoo/customer-options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/inventory-options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/jobs/[id]/route.js +25 -0
- package/.medusa/server/src/api/admin/odoo/logs/route.js +27 -0
- package/.medusa/server/src/api/admin/odoo/options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/order-options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/product-options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/sections/[section]/route.js +55 -0
- package/.medusa/server/src/api/admin/odoo/settings/route.js +91 -0
- package/.medusa/server/src/api/admin/odoo/stripe/change/route.js +20 -0
- package/.medusa/server/src/api/admin/odoo/stripe/checkout/route.js +20 -0
- package/.medusa/server/src/api/admin/odoo/stripe/portal/route.js +16 -0
- package/.medusa/server/src/api/admin/odoo/sync/[entity]/route.js +99 -0
- package/.medusa/server/src/api/admin/odoo/sync-order/[id]/route.js +17 -0
- package/.medusa/server/src/api/admin/odoo/verify-connection/route.js +39 -0
- package/.medusa/server/src/api/odoo/billing/sync/route.js +42 -0
- package/.medusa/server/src/index.js +3 -0
- package/.medusa/server/src/jobs/import-worker.js +61 -0
- package/.medusa/server/src/lib/export-products.js +141 -0
- package/.medusa/server/src/lib/import-products.js +304 -0
- package/.medusa/server/src/lib/odoo-api-client.js +32 -0
- package/.medusa/server/src/lib/plan.js +83 -0
- package/.medusa/server/src/lib/queue.js +79 -0
- package/.medusa/server/src/lib/sync-customers.js +183 -0
- package/.medusa/server/src/lib/sync-inventory.js +135 -0
- package/.medusa/server/src/lib/sync-order.js +134 -0
- package/.medusa/server/src/lib/sync-products.js +32 -0
- package/.medusa/server/src/modules/odoo/index.js +13 -0
- package/.medusa/server/src/modules/odoo/migrations/Migration20260619101616.js +17 -0
- package/.medusa/server/src/modules/odoo/models/odoo-setting.js +19 -0
- package/.medusa/server/src/modules/odoo/service.js +28 -0
- package/.medusa/server/src/subscribers/order-placed.js +24 -0
- package/.medusa/server/src/workflows/index.js +3 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/package.json +81 -0
|
@@ -0,0 +1,2212 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
5
|
+
import { useState, useEffect } from "react";
|
|
6
|
+
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
7
|
+
import { Container, Heading, Badge, Text, Button, toast, Label, Input, Select, Switch, Checkbox, Tooltip, Tabs, IconButton, usePrompt, Table } from "@medusajs/ui";
|
|
8
|
+
import { Bolt, ChartBar, CubeSolid, Buildings, ShoppingCart, Users, ArrowPath, DocumentText, CreditCard, CheckCircleSolid, XCircle, BuildingStorefront, Trash, Plus } from "@medusajs/icons";
|
|
9
|
+
async function mRequest(path) {
|
|
10
|
+
const res = await fetch(path, {
|
|
11
|
+
credentials: "include",
|
|
12
|
+
headers: { "Content-Type": "application/json" }
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
throw new Error(`Medusa request failed (${res.status})`);
|
|
16
|
+
}
|
|
17
|
+
return await res.json();
|
|
18
|
+
}
|
|
19
|
+
const medusaApi = {
|
|
20
|
+
get: (path) => mRequest(path)
|
|
21
|
+
};
|
|
22
|
+
const BASE = "/admin";
|
|
23
|
+
class ApiError extends Error {
|
|
24
|
+
constructor(message, status) {
|
|
25
|
+
super(message);
|
|
26
|
+
__publicField(this, "status");
|
|
27
|
+
this.status = status;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function request(path, options = {}) {
|
|
31
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
32
|
+
credentials: "include",
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
...options.headers || {}
|
|
36
|
+
},
|
|
37
|
+
...options
|
|
38
|
+
});
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
const data = text ? JSON.parse(text) : null;
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const message = data && (data.message || data.error) || `Request failed with status ${res.status}`;
|
|
43
|
+
throw new ApiError(message, res.status);
|
|
44
|
+
}
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
const api = {
|
|
48
|
+
get: (path) => request(path, { method: "GET" }),
|
|
49
|
+
post: (path, body) => request(path, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: body ? JSON.stringify(body) : void 0
|
|
52
|
+
}),
|
|
53
|
+
del: (path) => request(path, { method: "DELETE" })
|
|
54
|
+
};
|
|
55
|
+
const OrderOdooWidget = ({ data }) => {
|
|
56
|
+
const [meta, setMeta] = useState(null);
|
|
57
|
+
const [syncing, setSyncing] = useState(false);
|
|
58
|
+
const refresh = () => medusaApi.get(`/admin/orders/${data.id}?fields=id,metadata`).then((r) => {
|
|
59
|
+
var _a;
|
|
60
|
+
return setMeta(((_a = r.order) == null ? void 0 : _a.metadata) ?? {});
|
|
61
|
+
}).catch(() => setMeta({}));
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
let active = true;
|
|
64
|
+
medusaApi.get(`/admin/orders/${data.id}?fields=id,metadata`).then((r) => {
|
|
65
|
+
var _a;
|
|
66
|
+
return active && setMeta(((_a = r.order) == null ? void 0 : _a.metadata) ?? {});
|
|
67
|
+
}).catch(() => active && setMeta({}));
|
|
68
|
+
return () => {
|
|
69
|
+
active = false;
|
|
70
|
+
};
|
|
71
|
+
}, [data.id]);
|
|
72
|
+
const orderId = (meta == null ? void 0 : meta.odoo_order_id) ?? null;
|
|
73
|
+
const sync = async () => {
|
|
74
|
+
setSyncing(true);
|
|
75
|
+
try {
|
|
76
|
+
const r = await api.post(
|
|
77
|
+
`/odoo/sync-order/${data.id}`
|
|
78
|
+
);
|
|
79
|
+
if (r.odooOrderId) {
|
|
80
|
+
toast.success("Synced to Odoo", { description: `Odoo sale order ${r.odooOrderId}.` });
|
|
81
|
+
await refresh();
|
|
82
|
+
} else {
|
|
83
|
+
toast.error("Not synced", { description: r.message || "Could not sync this order." });
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
toast.error("Sync failed", { description: e.message });
|
|
87
|
+
} finally {
|
|
88
|
+
setSyncing(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y divide-ui-border-base p-0", children: [
|
|
92
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
93
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Odoo" }),
|
|
94
|
+
meta === null ? null : orderId ? /* @__PURE__ */ jsx(Badge, { size: "small", color: "green", children: "Synced" }) : /* @__PURE__ */ jsx(Badge, { size: "small", color: "grey", children: "Not synced" })
|
|
95
|
+
] }),
|
|
96
|
+
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: meta === null ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading…" }) : orderId ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
97
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Odoo sale order" }),
|
|
98
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", className: "font-mono", children: orderId })
|
|
99
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3", children: [
|
|
100
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "This order hasn't been synced to Odoo yet." }),
|
|
101
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: sync, isLoading: syncing, children: "Sync to Odoo" })
|
|
102
|
+
] }) })
|
|
103
|
+
] });
|
|
104
|
+
};
|
|
105
|
+
defineWidgetConfig({
|
|
106
|
+
zone: "order.details.side.after"
|
|
107
|
+
});
|
|
108
|
+
const ProductOdooWidget = ({ data }) => {
|
|
109
|
+
const [meta, setMeta] = useState(null);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
let active = true;
|
|
112
|
+
medusaApi.get(`/admin/products/${data.id}?fields=id,metadata`).then((r) => {
|
|
113
|
+
var _a;
|
|
114
|
+
return active && setMeta(((_a = r.product) == null ? void 0 : _a.metadata) ?? {});
|
|
115
|
+
}).catch(() => active && setMeta({}));
|
|
116
|
+
return () => {
|
|
117
|
+
active = false;
|
|
118
|
+
};
|
|
119
|
+
}, [data.id]);
|
|
120
|
+
const odooId = (meta == null ? void 0 : meta.odoo_id) ?? null;
|
|
121
|
+
const exportId = (meta == null ? void 0 : meta.odoo_export_id) ?? null;
|
|
122
|
+
const id = odooId ?? exportId;
|
|
123
|
+
const note = odooId ? "Imported from Odoo" : exportId ? "Exported to Odoo" : null;
|
|
124
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y divide-ui-border-base p-0", children: [
|
|
125
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
126
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Odoo" }),
|
|
127
|
+
meta === null ? null : id ? /* @__PURE__ */ jsx(Badge, { size: "small", color: "green", children: "Linked" }) : /* @__PURE__ */ jsx(Badge, { size: "small", color: "grey", children: "Not linked" })
|
|
128
|
+
] }),
|
|
129
|
+
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: meta === null ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading…" }) : id ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
130
|
+
note && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: note }),
|
|
131
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
132
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Odoo product ID" }),
|
|
133
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", className: "font-mono", children: id })
|
|
134
|
+
] })
|
|
135
|
+
] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "This product isn't linked to Odoo yet." }) })
|
|
136
|
+
] });
|
|
137
|
+
};
|
|
138
|
+
defineWidgetConfig({
|
|
139
|
+
zone: "product.details.side.after"
|
|
140
|
+
});
|
|
141
|
+
const NAV = [
|
|
142
|
+
{ key: "setup", label: "Setup", to: "/app/odoo", Icon: Bolt },
|
|
143
|
+
{ key: "dashboard", label: "Dashboard", to: "/app/odoo/dashboard", Icon: ChartBar },
|
|
144
|
+
{ key: "products", label: "Product Sync", to: "/app/odoo/products", Icon: CubeSolid },
|
|
145
|
+
{ key: "inventory", label: "Inventory", to: "/app/odoo/inventory", Icon: Buildings },
|
|
146
|
+
{ key: "orders", label: "Orders", to: "/app/odoo/orders", Icon: ShoppingCart },
|
|
147
|
+
{ key: "customers", label: "Customers", to: "/app/odoo/customers", Icon: Users },
|
|
148
|
+
{ key: "sync", label: "Sync", to: "/app/odoo/sync", Icon: ArrowPath },
|
|
149
|
+
{ key: "logs", label: "Logs", to: "/app/odoo/logs", Icon: DocumentText },
|
|
150
|
+
{ key: "plans", label: "Plans", to: "/app/odoo/plans", Icon: CreditCard }
|
|
151
|
+
];
|
|
152
|
+
const OdooNav = ({ current }) => /* @__PURE__ */ jsx("div", { className: "flex items-center gap-x-1 overflow-x-auto px-3", children: NAV.map(({ key, label, to, Icon }) => {
|
|
153
|
+
const active = key === current;
|
|
154
|
+
return /* @__PURE__ */ jsxs(
|
|
155
|
+
"a",
|
|
156
|
+
{
|
|
157
|
+
href: to,
|
|
158
|
+
className: `flex items-center gap-x-2 px-3 py-3 text-sm whitespace-nowrap border-b-2 -mb-px transition-colors ${active ? "border-ui-fg-base text-ui-fg-base font-medium" : "border-transparent text-ui-fg-subtle hover:text-ui-fg-base"}`,
|
|
159
|
+
children: [
|
|
160
|
+
/* @__PURE__ */ jsx(Icon, {}),
|
|
161
|
+
label
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
key
|
|
165
|
+
);
|
|
166
|
+
}) });
|
|
167
|
+
const OdooShell = ({
|
|
168
|
+
current,
|
|
169
|
+
title,
|
|
170
|
+
actions,
|
|
171
|
+
children
|
|
172
|
+
}) => /* @__PURE__ */ jsxs(Container, { className: "p-0 divide-y divide-ui-border-base", children: [
|
|
173
|
+
/* @__PURE__ */ jsx(OdooNav, { current }),
|
|
174
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-x-2 px-6 py-4 flex-wrap gap-y-2", children: [
|
|
175
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: title }),
|
|
176
|
+
actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-x-2 flex-wrap gap-y-2", children: actions })
|
|
177
|
+
] }),
|
|
178
|
+
/* @__PURE__ */ jsx("div", { className: "px-6 py-6", children })
|
|
179
|
+
] });
|
|
180
|
+
const OdooSheet = ({
|
|
181
|
+
children,
|
|
182
|
+
style
|
|
183
|
+
}) => /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-y-8", style, children });
|
|
184
|
+
const OdooSection = ({
|
|
185
|
+
icon: Icon,
|
|
186
|
+
title,
|
|
187
|
+
sub,
|
|
188
|
+
children
|
|
189
|
+
}) => /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-y-4", children: [
|
|
190
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-x-3", children: [
|
|
191
|
+
Icon && /* @__PURE__ */ jsx("div", { className: "text-ui-fg-subtle mt-0.5", children: /* @__PURE__ */ jsx(Icon, {}) }),
|
|
192
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
193
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: title }),
|
|
194
|
+
sub && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: sub })
|
|
195
|
+
] })
|
|
196
|
+
] }),
|
|
197
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-y-4", children })
|
|
198
|
+
] });
|
|
199
|
+
const BTN_VARIANT = {
|
|
200
|
+
primary: "primary",
|
|
201
|
+
teal: "primary",
|
|
202
|
+
secondary: "secondary",
|
|
203
|
+
link: "transparent"
|
|
204
|
+
};
|
|
205
|
+
const OdooButton = ({
|
|
206
|
+
variant = "secondary",
|
|
207
|
+
onClick,
|
|
208
|
+
disabled,
|
|
209
|
+
loading,
|
|
210
|
+
icon: Icon,
|
|
211
|
+
children,
|
|
212
|
+
type
|
|
213
|
+
}) => /* @__PURE__ */ jsxs(
|
|
214
|
+
Button,
|
|
215
|
+
{
|
|
216
|
+
type: type ?? "button",
|
|
217
|
+
variant: BTN_VARIANT[variant],
|
|
218
|
+
onClick,
|
|
219
|
+
disabled,
|
|
220
|
+
isLoading: loading,
|
|
221
|
+
children: [
|
|
222
|
+
Icon ? /* @__PURE__ */ jsx(Icon, {}) : null,
|
|
223
|
+
children
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
const OdooStat = ({
|
|
228
|
+
label,
|
|
229
|
+
value,
|
|
230
|
+
icon: Icon,
|
|
231
|
+
loading
|
|
232
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base p-4 flex flex-col gap-y-2", children: [
|
|
233
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2 text-ui-fg-subtle", children: [
|
|
234
|
+
Icon && /* @__PURE__ */ jsx(Icon, {}),
|
|
235
|
+
/* @__PURE__ */ jsx(Text, { size: "small", children: label })
|
|
236
|
+
] }),
|
|
237
|
+
loading ? /* @__PURE__ */ jsx(Heading, { level: "h2", className: "text-ui-fg-muted", children: "…" }) : /* @__PURE__ */ jsx(Heading, { level: "h2", children: value })
|
|
238
|
+
] });
|
|
239
|
+
const OdooPill = ({
|
|
240
|
+
color = "grey",
|
|
241
|
+
children
|
|
242
|
+
}) => /* @__PURE__ */ jsx(Badge, { size: "small", color, children });
|
|
243
|
+
const EMPTY = { url: "", database: "", username: "", api_key: "" };
|
|
244
|
+
const Field = ({
|
|
245
|
+
id,
|
|
246
|
+
label,
|
|
247
|
+
...props
|
|
248
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1.5", children: [
|
|
249
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: id, size: "small", weight: "plus", children: label }),
|
|
250
|
+
/* @__PURE__ */ jsx(Input, { id, ...props })
|
|
251
|
+
] });
|
|
252
|
+
const ConnectionForm = ({ onSaved } = {}) => {
|
|
253
|
+
const [form, setForm] = useState(EMPTY);
|
|
254
|
+
const [hasApiKey, setHasApiKey] = useState(false);
|
|
255
|
+
const [apiKeyTouched, setApiKeyTouched] = useState(false);
|
|
256
|
+
const [loading, setLoading] = useState(true);
|
|
257
|
+
const [saving, setSaving] = useState(false);
|
|
258
|
+
const [testing, setTesting] = useState(false);
|
|
259
|
+
const [lastTest, setLastTest] = useState(null);
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
let active = true;
|
|
262
|
+
api.get("/odoo/settings").then((res) => {
|
|
263
|
+
if (!active) return;
|
|
264
|
+
const s = res.settings;
|
|
265
|
+
setForm({
|
|
266
|
+
url: s.url,
|
|
267
|
+
database: s.database,
|
|
268
|
+
username: s.username,
|
|
269
|
+
api_key: s.api_key
|
|
270
|
+
});
|
|
271
|
+
setHasApiKey(s.has_api_key);
|
|
272
|
+
}).catch((e) => toast.error("Failed to load settings", { description: e.message })).finally(() => active && setLoading(false));
|
|
273
|
+
return () => {
|
|
274
|
+
active = false;
|
|
275
|
+
};
|
|
276
|
+
}, []);
|
|
277
|
+
const update = (key) => (e) => {
|
|
278
|
+
if (key === "api_key") setApiKeyTouched(true);
|
|
279
|
+
setForm((prev) => ({ ...prev, [key]: e.target.value }));
|
|
280
|
+
};
|
|
281
|
+
const buildPayload = () => ({
|
|
282
|
+
...form,
|
|
283
|
+
api_key: apiKeyTouched ? form.api_key : hasApiKey ? "" : form.api_key
|
|
284
|
+
});
|
|
285
|
+
const validate = (payload) => {
|
|
286
|
+
if (!payload.url.trim()) return "Odoo URL is required.";
|
|
287
|
+
if (!/^https?:\/\//i.test(payload.url.trim()))
|
|
288
|
+
return "Odoo URL must start with http:// or https://";
|
|
289
|
+
if (!payload.database.trim()) return "Database is required.";
|
|
290
|
+
if (!payload.username.trim()) return "Username is required.";
|
|
291
|
+
if (!payload.api_key.trim() && !hasApiKey) return "API key is required.";
|
|
292
|
+
return null;
|
|
293
|
+
};
|
|
294
|
+
const handleTest = async () => {
|
|
295
|
+
const payload = buildPayload();
|
|
296
|
+
if (!payload.api_key.trim() && !hasApiKey) {
|
|
297
|
+
toast.warning("Enter the API key to test", {
|
|
298
|
+
description: "Provide the Odoo API key to run a connection test."
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const err = validate(payload);
|
|
303
|
+
if (err) {
|
|
304
|
+
toast.error("Invalid settings", { description: err });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
setTesting(true);
|
|
308
|
+
setLastTest(null);
|
|
309
|
+
try {
|
|
310
|
+
const result = await api.post("/odoo/verify-connection", payload);
|
|
311
|
+
setLastTest(result);
|
|
312
|
+
toast.success("Connection successful", { description: result.message });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
const result = { ok: false, message: e.message };
|
|
315
|
+
setLastTest(result);
|
|
316
|
+
toast.error("Connection failed", { description: e.message });
|
|
317
|
+
} finally {
|
|
318
|
+
setTesting(false);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const handleSave = async () => {
|
|
322
|
+
const payload = buildPayload();
|
|
323
|
+
if (!payload.api_key.trim() && !hasApiKey) {
|
|
324
|
+
toast.error("Invalid settings", { description: "API key is required." });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const err = validate({
|
|
328
|
+
...payload,
|
|
329
|
+
api_key: payload.api_key || (hasApiKey ? "__keep__" : "")
|
|
330
|
+
});
|
|
331
|
+
if (err) {
|
|
332
|
+
toast.error("Invalid settings", { description: err });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!payload.api_key.trim() && hasApiKey) {
|
|
336
|
+
toast.warning("Re-enter the API key to save", {
|
|
337
|
+
description: "For security the saved key is never returned, so saving requires re-entering it."
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
setSaving(true);
|
|
342
|
+
try {
|
|
343
|
+
const res = await api.post("/odoo/settings", payload);
|
|
344
|
+
const s = res.settings;
|
|
345
|
+
setForm({
|
|
346
|
+
url: s.url,
|
|
347
|
+
database: s.database,
|
|
348
|
+
username: s.username,
|
|
349
|
+
api_key: s.api_key
|
|
350
|
+
});
|
|
351
|
+
setHasApiKey(s.has_api_key);
|
|
352
|
+
setApiKeyTouched(false);
|
|
353
|
+
toast.success("Settings saved");
|
|
354
|
+
onSaved == null ? void 0 : onSaved();
|
|
355
|
+
} catch (e) {
|
|
356
|
+
toast.error("Failed to save settings", { description: e.message });
|
|
357
|
+
} finally {
|
|
358
|
+
setSaving(false);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
if (loading) {
|
|
362
|
+
return /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading connection settings…" });
|
|
363
|
+
}
|
|
364
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-4 max-w-xl", children: [
|
|
365
|
+
/* @__PURE__ */ jsx(
|
|
366
|
+
Field,
|
|
367
|
+
{
|
|
368
|
+
id: "odoo-url",
|
|
369
|
+
label: "Odoo URL",
|
|
370
|
+
placeholder: "https://my-org.odoo.com",
|
|
371
|
+
value: form.url,
|
|
372
|
+
onChange: update("url")
|
|
373
|
+
}
|
|
374
|
+
),
|
|
375
|
+
/* @__PURE__ */ jsx(
|
|
376
|
+
Field,
|
|
377
|
+
{
|
|
378
|
+
id: "odoo-database",
|
|
379
|
+
label: "Database",
|
|
380
|
+
placeholder: "my-org-production",
|
|
381
|
+
value: form.database,
|
|
382
|
+
onChange: update("database")
|
|
383
|
+
}
|
|
384
|
+
),
|
|
385
|
+
/* @__PURE__ */ jsx(
|
|
386
|
+
Field,
|
|
387
|
+
{
|
|
388
|
+
id: "odoo-username",
|
|
389
|
+
label: "Username",
|
|
390
|
+
placeholder: "api-user@my-org.com",
|
|
391
|
+
value: form.username,
|
|
392
|
+
onChange: update("username")
|
|
393
|
+
}
|
|
394
|
+
),
|
|
395
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1.5", children: [
|
|
396
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "odoo-api-key", size: "small", weight: "plus", children: "API Key" }),
|
|
397
|
+
/* @__PURE__ */ jsx(
|
|
398
|
+
Input,
|
|
399
|
+
{
|
|
400
|
+
id: "odoo-api-key",
|
|
401
|
+
type: "password",
|
|
402
|
+
placeholder: hasApiKey ? "•••••••• (saved — re-enter to change)" : "Odoo API key",
|
|
403
|
+
value: form.api_key,
|
|
404
|
+
onChange: update("api_key"),
|
|
405
|
+
onFocus: () => {
|
|
406
|
+
if (hasApiKey && !apiKeyTouched) {
|
|
407
|
+
setApiKeyTouched(true);
|
|
408
|
+
setForm((p) => ({ ...p, api_key: "" }));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
),
|
|
413
|
+
hasApiKey && !apiKeyTouched && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "A key is already saved and is never displayed. Focus the field to enter a new one." })
|
|
414
|
+
] }),
|
|
415
|
+
lastTest && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
416
|
+
/* @__PURE__ */ jsx(OdooPill, { color: lastTest.ok ? "green" : "red", children: lastTest.ok ? "Connected" : "Failed" }),
|
|
417
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: lastTest.message })
|
|
418
|
+
] }),
|
|
419
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-x-2", children: [
|
|
420
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "primary", onClick: handleSave, loading: saving, disabled: testing, children: "Save settings" }),
|
|
421
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "secondary", onClick: handleTest, loading: testing, disabled: saving, children: "Test connection" })
|
|
422
|
+
] })
|
|
423
|
+
] });
|
|
424
|
+
};
|
|
425
|
+
const Section = ({
|
|
426
|
+
title,
|
|
427
|
+
children
|
|
428
|
+
}) => /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-y-4", children: [
|
|
429
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: title }),
|
|
430
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-y-4", children })
|
|
431
|
+
] });
|
|
432
|
+
const HelpLabel = ({ label, help }) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-1", children: [
|
|
433
|
+
/* @__PURE__ */ jsx(Label, { size: "small", weight: "plus", children: label }),
|
|
434
|
+
help && /* @__PURE__ */ jsx(Tooltip, { content: help, children: /* @__PURE__ */ jsx("span", { className: "text-ui-fg-muted text-xs border border-ui-border-base rounded-full size-4 inline-flex items-center justify-center cursor-help", children: "?" }) })
|
|
435
|
+
] });
|
|
436
|
+
const ToggleRow = ({
|
|
437
|
+
label,
|
|
438
|
+
checked,
|
|
439
|
+
onChange,
|
|
440
|
+
hint,
|
|
441
|
+
disabled
|
|
442
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-x-3", children: [
|
|
443
|
+
/* @__PURE__ */ jsx(
|
|
444
|
+
Switch,
|
|
445
|
+
{
|
|
446
|
+
checked,
|
|
447
|
+
onCheckedChange: onChange,
|
|
448
|
+
disabled,
|
|
449
|
+
"aria-label": label,
|
|
450
|
+
className: "mt-0.5"
|
|
451
|
+
}
|
|
452
|
+
),
|
|
453
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
454
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: label }),
|
|
455
|
+
hint && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: hint })
|
|
456
|
+
] })
|
|
457
|
+
] });
|
|
458
|
+
const CheckList = ({
|
|
459
|
+
items,
|
|
460
|
+
selected,
|
|
461
|
+
onToggle,
|
|
462
|
+
empty
|
|
463
|
+
}) => /* @__PURE__ */ jsx("div", { className: "border border-ui-border-base rounded-lg max-h-48 overflow-y-auto divide-y divide-ui-border-base", children: items.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle p-3", children: empty }) : items.map((it) => /* @__PURE__ */ jsxs(
|
|
464
|
+
"label",
|
|
465
|
+
{
|
|
466
|
+
className: "flex items-center gap-x-2 px-3 py-2 cursor-pointer hover:bg-ui-bg-base-hover",
|
|
467
|
+
children: [
|
|
468
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: selected.has(it.id), onCheckedChange: () => onToggle(it.id) }),
|
|
469
|
+
/* @__PURE__ */ jsx(Text, { size: "small", children: it.label })
|
|
470
|
+
]
|
|
471
|
+
},
|
|
472
|
+
it.id
|
|
473
|
+
)) });
|
|
474
|
+
const SelectField = ({
|
|
475
|
+
label,
|
|
476
|
+
help,
|
|
477
|
+
value,
|
|
478
|
+
onChange,
|
|
479
|
+
options,
|
|
480
|
+
placeholder,
|
|
481
|
+
disabled
|
|
482
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1.5 max-w-md", children: [
|
|
483
|
+
/* @__PURE__ */ jsx(HelpLabel, { label, help }),
|
|
484
|
+
/* @__PURE__ */ jsxs(Select, { value, onValueChange: onChange, disabled, children: [
|
|
485
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: placeholder ?? "Select" }) }),
|
|
486
|
+
/* @__PURE__ */ jsx(Select.Content, { children: options.map((o) => /* @__PURE__ */ jsx(Select.Item, { value: o.value, children: o.label }, o.value)) })
|
|
487
|
+
] })
|
|
488
|
+
] });
|
|
489
|
+
const GeneralConfigForm = ({ onSaved }) => {
|
|
490
|
+
const [loading, setLoading] = useState(true);
|
|
491
|
+
const [optionsLoading, setOptionsLoading] = useState(true);
|
|
492
|
+
const [saving, setSaving] = useState(false);
|
|
493
|
+
const [options, setOptions] = useState({
|
|
494
|
+
connected: false,
|
|
495
|
+
companies: [],
|
|
496
|
+
saleJournals: []
|
|
497
|
+
});
|
|
498
|
+
const [skuMapping, setSkuMapping] = useState("sku");
|
|
499
|
+
const [companyId, setCompanyId] = useState("");
|
|
500
|
+
const [journalId, setJournalId] = useState("");
|
|
501
|
+
const [fixedTax, setFixedTax] = useState("no");
|
|
502
|
+
useEffect(() => {
|
|
503
|
+
let active = true;
|
|
504
|
+
api.get("/odoo/config").then((cfg) => {
|
|
505
|
+
if (!active) return;
|
|
506
|
+
const c = cfg.config;
|
|
507
|
+
setSkuMapping(c.sku_mapping);
|
|
508
|
+
setCompanyId(c.company_id != null ? String(c.company_id) : "");
|
|
509
|
+
setJournalId(c.sale_journal_id != null ? String(c.sale_journal_id) : "");
|
|
510
|
+
setFixedTax(c.fixed_tax_mapping ? "yes" : "no");
|
|
511
|
+
}).catch((e) => toast.error("Failed to load configuration", { description: e.message })).finally(() => active && setLoading(false));
|
|
512
|
+
api.get("/odoo/options").then((opts) => active && setOptions(opts)).catch(() => {
|
|
513
|
+
}).finally(() => active && setOptionsLoading(false));
|
|
514
|
+
return () => {
|
|
515
|
+
active = false;
|
|
516
|
+
};
|
|
517
|
+
}, []);
|
|
518
|
+
const handleSave = async () => {
|
|
519
|
+
const company = options.companies.find((c) => String(c.id) === companyId);
|
|
520
|
+
const journal = options.saleJournals.find((j) => String(j.id) === journalId);
|
|
521
|
+
const payload = {
|
|
522
|
+
sku_mapping: skuMapping,
|
|
523
|
+
company_id: (company == null ? void 0 : company.id) ?? null,
|
|
524
|
+
company_label: (company == null ? void 0 : company.name) ?? null,
|
|
525
|
+
sale_journal_id: (journal == null ? void 0 : journal.id) ?? null,
|
|
526
|
+
sale_journal_label: (journal == null ? void 0 : journal.name) ?? null,
|
|
527
|
+
fixed_tax_mapping: fixedTax === "yes"
|
|
528
|
+
};
|
|
529
|
+
setSaving(true);
|
|
530
|
+
try {
|
|
531
|
+
await api.post("/odoo/config", payload);
|
|
532
|
+
toast.success("Configuration saved");
|
|
533
|
+
onSaved == null ? void 0 : onSaved();
|
|
534
|
+
} catch (e) {
|
|
535
|
+
toast.error("Failed to save", { description: e.message });
|
|
536
|
+
} finally {
|
|
537
|
+
setSaving(false);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
if (loading) {
|
|
541
|
+
return /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading configuration…" });
|
|
542
|
+
}
|
|
543
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-4 max-w-xl", children: [
|
|
544
|
+
!options.connected && /* @__PURE__ */ jsx("div", { className: "bg-ui-bg-subtle border border-ui-border-base rounded-lg px-4 py-3", children: /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
545
|
+
options.error ? `Couldn't load Odoo data: ${options.error}. ` : "Connect to Odoo first to load companies and journals. ",
|
|
546
|
+
"You can still pick a mapping and save."
|
|
547
|
+
] }) }),
|
|
548
|
+
/* @__PURE__ */ jsx(
|
|
549
|
+
SelectField,
|
|
550
|
+
{
|
|
551
|
+
label: "Odoo SKU / Barcode mapping",
|
|
552
|
+
help: "Which Odoo field maps to the Medusa variant SKU when matching products.",
|
|
553
|
+
value: skuMapping,
|
|
554
|
+
onChange: (v) => setSkuMapping(v),
|
|
555
|
+
options: [
|
|
556
|
+
{ value: "sku", label: "SKU (Internal Reference)" },
|
|
557
|
+
{ value: "barcode", label: "Barcode" }
|
|
558
|
+
]
|
|
559
|
+
}
|
|
560
|
+
),
|
|
561
|
+
/* @__PURE__ */ jsx(
|
|
562
|
+
SelectField,
|
|
563
|
+
{
|
|
564
|
+
label: "Company",
|
|
565
|
+
help: "The Odoo company that records created from Medusa belong to.",
|
|
566
|
+
value: companyId,
|
|
567
|
+
onChange: setCompanyId,
|
|
568
|
+
options: options.companies.map((c) => ({ value: String(c.id), label: c.name })),
|
|
569
|
+
placeholder: optionsLoading ? "Loading from Odoo…" : options.companies.length ? "Select company" : "No companies — connect to Odoo",
|
|
570
|
+
disabled: optionsLoading || !options.companies.length
|
|
571
|
+
}
|
|
572
|
+
),
|
|
573
|
+
/* @__PURE__ */ jsx(
|
|
574
|
+
SelectField,
|
|
575
|
+
{
|
|
576
|
+
label: "Sale invoice journal",
|
|
577
|
+
help: "The Odoo journal used when creating customer invoices for synced orders.",
|
|
578
|
+
value: journalId,
|
|
579
|
+
onChange: setJournalId,
|
|
580
|
+
options: options.saleJournals.map((j) => ({ value: String(j.id), label: j.name })),
|
|
581
|
+
placeholder: optionsLoading ? "Loading from Odoo…" : options.saleJournals.length ? "Select journal" : "No journals — connect to Odoo",
|
|
582
|
+
disabled: optionsLoading || !options.saleJournals.length
|
|
583
|
+
}
|
|
584
|
+
),
|
|
585
|
+
/* @__PURE__ */ jsx(
|
|
586
|
+
SelectField,
|
|
587
|
+
{
|
|
588
|
+
label: "Enable fixed tax mapping",
|
|
589
|
+
help: "When enabled, taxes are mapped to fixed Odoo tax records instead of being computed.",
|
|
590
|
+
value: fixedTax,
|
|
591
|
+
onChange: (v) => setFixedTax(v),
|
|
592
|
+
options: [
|
|
593
|
+
{ value: "no", label: "No" },
|
|
594
|
+
{ value: "yes", label: "Yes" }
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
),
|
|
598
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(OdooButton, { variant: "primary", onClick: handleSave, loading: saving, children: "Save changes" }) })
|
|
599
|
+
] });
|
|
600
|
+
};
|
|
601
|
+
const Item = ({ done, label }) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
602
|
+
/* @__PURE__ */ jsx("span", { className: done ? "text-ui-tag-green-icon" : "text-ui-fg-muted", children: done ? /* @__PURE__ */ jsx(CheckCircleSolid, {}) : /* @__PURE__ */ jsx(XCircle, {}) }),
|
|
603
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: done ? void 0 : "text-ui-fg-subtle", children: label })
|
|
604
|
+
] });
|
|
605
|
+
const ReadyToGo = ({ connectionDone, configDone, finished, onFinish }) => {
|
|
606
|
+
const allDone = connectionDone && configDone;
|
|
607
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-4 max-w-xl", children: [
|
|
608
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
609
|
+
/* @__PURE__ */ jsx(Item, { done: connectionDone, label: "Odoo connection configured" }),
|
|
610
|
+
/* @__PURE__ */ jsx(Item, { done: configDone, label: "General configuration saved" })
|
|
611
|
+
] }),
|
|
612
|
+
/* @__PURE__ */ jsx("div", { className: "border-t border-ui-border-base pt-4", children: allDone ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2 items-start", children: [
|
|
613
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: finished ? "You're all set!" : "Everything looks good" }),
|
|
614
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Your Odoo connector is configured. Head to the Sync page to push and pull data, or review past runs in Logs." }),
|
|
615
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "primary", onClick: onFinish, disabled: finished, children: finished ? "Setup complete" : "Finish setup" })
|
|
616
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
617
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: "Almost there" }),
|
|
618
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Complete the steps above to finish setting up the connector." })
|
|
619
|
+
] }) })
|
|
620
|
+
] });
|
|
621
|
+
};
|
|
622
|
+
const SUPPORT_EMAIL = "support@odooconnector.cloud";
|
|
623
|
+
const DOCS_URL = "https://docs.medusajs.com";
|
|
624
|
+
const OdooSetupGuide = () => {
|
|
625
|
+
const [tab, setTab] = useState("connection");
|
|
626
|
+
const [connectionDone, setConnectionDone] = useState(false);
|
|
627
|
+
const [configDone, setConfigDone] = useState(false);
|
|
628
|
+
const [finished, setFinished] = useState(false);
|
|
629
|
+
const refresh = async () => {
|
|
630
|
+
try {
|
|
631
|
+
const [settings, config2] = await Promise.all([
|
|
632
|
+
api.get("/odoo/settings"),
|
|
633
|
+
api.get("/odoo/config")
|
|
634
|
+
]);
|
|
635
|
+
setConnectionDone(settings.configured);
|
|
636
|
+
setConfigDone(config2.configured);
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
useEffect(() => {
|
|
641
|
+
refresh();
|
|
642
|
+
}, []);
|
|
643
|
+
const completed = (connectionDone ? 1 : 0) + (configDone ? 1 : 0) + (finished ? 1 : 0);
|
|
644
|
+
const pct = Math.round(completed / 3 * 100);
|
|
645
|
+
return /* @__PURE__ */ jsxs(Container, { className: "flex flex-col gap-y-6 p-0", children: [
|
|
646
|
+
/* @__PURE__ */ jsx("div", { className: "border-b border-ui-border-base", children: /* @__PURE__ */ jsx(OdooNav, { current: "setup" }) }),
|
|
647
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 pt-6", children: [
|
|
648
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Setup guide" }),
|
|
649
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle mt-1", children: "Use this guide to get your store connected and running." }),
|
|
650
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-4 mt-4", children: [
|
|
651
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", weight: "plus", className: "whitespace-nowrap", children: [
|
|
652
|
+
completed,
|
|
653
|
+
" / 3 completed"
|
|
654
|
+
] }),
|
|
655
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 flex-1 bg-ui-bg-component rounded-full overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
656
|
+
"div",
|
|
657
|
+
{
|
|
658
|
+
className: "h-full bg-ui-fg-interactive rounded-full transition-all",
|
|
659
|
+
style: { width: `${pct}%` }
|
|
660
|
+
}
|
|
661
|
+
) })
|
|
662
|
+
] })
|
|
663
|
+
] }),
|
|
664
|
+
/* @__PURE__ */ jsx("div", { className: "px-6", children: /* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: setTab, children: [
|
|
665
|
+
/* @__PURE__ */ jsxs(Tabs.List, { children: [
|
|
666
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "connection", children: "Odoo Connection" }),
|
|
667
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "general", children: "General Configuration" }),
|
|
668
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "ready", children: "Ready to go!" })
|
|
669
|
+
] }),
|
|
670
|
+
/* @__PURE__ */ jsxs("div", { className: "py-6", children: [
|
|
671
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle mb-6", children: [
|
|
672
|
+
"Need help?",
|
|
673
|
+
" ",
|
|
674
|
+
/* @__PURE__ */ jsx("a", { className: "text-ui-fg-interactive", href: DOCS_URL, target: "_blank", rel: "noreferrer", children: "Check out our documentation" }),
|
|
675
|
+
" ",
|
|
676
|
+
"or",
|
|
677
|
+
" ",
|
|
678
|
+
/* @__PURE__ */ jsx("a", { className: "text-ui-fg-interactive", href: `mailto:${SUPPORT_EMAIL}`, children: "Schedule a meeting" })
|
|
679
|
+
] }),
|
|
680
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "connection", children: /* @__PURE__ */ jsx(ConnectionForm, { onSaved: refresh }) }),
|
|
681
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "general", children: /* @__PURE__ */ jsx(GeneralConfigForm, { onSaved: refresh }) }),
|
|
682
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "ready", children: /* @__PURE__ */ jsx(
|
|
683
|
+
ReadyToGo,
|
|
684
|
+
{
|
|
685
|
+
connectionDone,
|
|
686
|
+
configDone,
|
|
687
|
+
finished,
|
|
688
|
+
onFinish: () => setFinished(true)
|
|
689
|
+
}
|
|
690
|
+
) })
|
|
691
|
+
] })
|
|
692
|
+
] }) }),
|
|
693
|
+
/* @__PURE__ */ jsx("div", { className: "border-t border-ui-border-base px-6 py-4", children: /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
694
|
+
"Need help? Contact us at",
|
|
695
|
+
" ",
|
|
696
|
+
/* @__PURE__ */ jsx("a", { className: "text-ui-fg-interactive", href: `mailto:${SUPPORT_EMAIL}`, children: SUPPORT_EMAIL })
|
|
697
|
+
] }) })
|
|
698
|
+
] });
|
|
699
|
+
};
|
|
700
|
+
const config$8 = defineRouteConfig({
|
|
701
|
+
label: "Odoo Connector",
|
|
702
|
+
icon: Bolt
|
|
703
|
+
});
|
|
704
|
+
const DEFAULTS$3 = {
|
|
705
|
+
direction: "odoo_to_medusa",
|
|
706
|
+
enableStock: true,
|
|
707
|
+
qtyType: "available",
|
|
708
|
+
frequency: "hourly",
|
|
709
|
+
locationMappings: [],
|
|
710
|
+
doNotSyncZeroStock: false
|
|
711
|
+
};
|
|
712
|
+
const InventoryForm = () => {
|
|
713
|
+
const [loading, setLoading] = useState(true);
|
|
714
|
+
const [optionsLoading, setOptionsLoading] = useState(true);
|
|
715
|
+
const [saving, setSaving] = useState(false);
|
|
716
|
+
const [running, setRunning] = useState(false);
|
|
717
|
+
const [s, setS] = useState(DEFAULTS$3);
|
|
718
|
+
const [warehouses, setWarehouses] = useState([]);
|
|
719
|
+
const [locations, setLocations] = useState([]);
|
|
720
|
+
const set = (k, v) => setS((p) => ({ ...p, [k]: v }));
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
let active = true;
|
|
723
|
+
api.get("/odoo/sections/inventory").then((r) => active && r.data && setS({ ...DEFAULTS$3, ...r.data })).catch((e) => toast.error("Failed to load", { description: e.message })).finally(() => active && setLoading(false));
|
|
724
|
+
Promise.all([
|
|
725
|
+
api.get("/odoo/inventory-options"),
|
|
726
|
+
medusaApi.get("/admin/stock-locations?limit=100").catch(() => ({ stock_locations: [] }))
|
|
727
|
+
]).then(([opts, locs]) => {
|
|
728
|
+
if (!active) return;
|
|
729
|
+
setWarehouses(opts.warehouses ?? []);
|
|
730
|
+
setLocations(locs.stock_locations ?? []);
|
|
731
|
+
}).catch(() => {
|
|
732
|
+
}).finally(() => active && setOptionsLoading(false));
|
|
733
|
+
return () => {
|
|
734
|
+
active = false;
|
|
735
|
+
};
|
|
736
|
+
}, []);
|
|
737
|
+
const addMapping = () => set("locationMappings", [
|
|
738
|
+
...s.locationMappings,
|
|
739
|
+
{ medusaLocationId: "", odooWarehouseId: null }
|
|
740
|
+
]);
|
|
741
|
+
const removeMapping = (i) => set("locationMappings", s.locationMappings.filter((_, idx) => idx !== i));
|
|
742
|
+
const updateMapping = (i, patch) => set(
|
|
743
|
+
"locationMappings",
|
|
744
|
+
s.locationMappings.map((m, idx) => idx === i ? { ...m, ...patch } : m)
|
|
745
|
+
);
|
|
746
|
+
const handleSave = async () => {
|
|
747
|
+
setSaving(true);
|
|
748
|
+
try {
|
|
749
|
+
await api.post("/odoo/sections/inventory", s);
|
|
750
|
+
toast.success("Inventory settings saved");
|
|
751
|
+
} catch (e) {
|
|
752
|
+
toast.error("Failed to save", { description: e.message });
|
|
753
|
+
} finally {
|
|
754
|
+
setSaving(false);
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
const syncNow = async () => {
|
|
758
|
+
setRunning(true);
|
|
759
|
+
try {
|
|
760
|
+
const r = await api.post("/odoo/sync/inventory");
|
|
761
|
+
toast.success("Inventory sync started", { description: r.message });
|
|
762
|
+
} catch (e) {
|
|
763
|
+
toast.error("Sync failed", { description: e.message });
|
|
764
|
+
} finally {
|
|
765
|
+
setRunning(false);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
return /* @__PURE__ */ jsx(
|
|
769
|
+
OdooShell,
|
|
770
|
+
{
|
|
771
|
+
current: "inventory",
|
|
772
|
+
title: "Inventory",
|
|
773
|
+
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
774
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "teal", icon: ArrowPath, onClick: syncNow, loading: running, children: "Sync now" }),
|
|
775
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "primary", onClick: handleSave, loading: saving, children: "Save" })
|
|
776
|
+
] }),
|
|
777
|
+
children: /* @__PURE__ */ jsx(OdooSheet, { children: loading ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading inventory settings…" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
778
|
+
/* @__PURE__ */ jsxs(OdooSection, { icon: BuildingStorefront, title: "Stock synchronization", sub: "Keep Medusa and Odoo stock levels aligned.", children: [
|
|
779
|
+
/* @__PURE__ */ jsx(
|
|
780
|
+
SelectField,
|
|
781
|
+
{
|
|
782
|
+
label: "Sync direction",
|
|
783
|
+
value: s.direction,
|
|
784
|
+
onChange: (v) => set("direction", v),
|
|
785
|
+
options: [
|
|
786
|
+
{ value: "odoo_to_medusa", label: "Odoo → Medusa" },
|
|
787
|
+
{ value: "medusa_to_odoo", label: "Medusa → Odoo" }
|
|
788
|
+
]
|
|
789
|
+
}
|
|
790
|
+
),
|
|
791
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Enable stock sync", checked: s.enableStock, onChange: (v) => set("enableStock", v) }),
|
|
792
|
+
/* @__PURE__ */ jsx(
|
|
793
|
+
SelectField,
|
|
794
|
+
{
|
|
795
|
+
label: "Quantity type",
|
|
796
|
+
help: "Which Odoo quantity to sync.",
|
|
797
|
+
value: s.qtyType,
|
|
798
|
+
onChange: (v) => set("qtyType", v),
|
|
799
|
+
options: [
|
|
800
|
+
{ value: "available", label: "Available (forecasted)" },
|
|
801
|
+
{ value: "on_hand", label: "On hand" }
|
|
802
|
+
]
|
|
803
|
+
}
|
|
804
|
+
),
|
|
805
|
+
/* @__PURE__ */ jsx(
|
|
806
|
+
SelectField,
|
|
807
|
+
{
|
|
808
|
+
label: "Sync frequency",
|
|
809
|
+
value: s.frequency,
|
|
810
|
+
onChange: (v) => set("frequency", v),
|
|
811
|
+
options: [
|
|
812
|
+
{ value: "realtime", label: "Real-time" },
|
|
813
|
+
{ value: "15min", label: "Every 15 minutes" },
|
|
814
|
+
{ value: "hourly", label: "Hourly" },
|
|
815
|
+
{ value: "daily", label: "Daily" }
|
|
816
|
+
]
|
|
817
|
+
}
|
|
818
|
+
),
|
|
819
|
+
/* @__PURE__ */ jsx(
|
|
820
|
+
ToggleRow,
|
|
821
|
+
{
|
|
822
|
+
label: "Don't sync zero stock",
|
|
823
|
+
hint: "Skip pushing items whose quantity is zero.",
|
|
824
|
+
checked: s.doNotSyncZeroStock,
|
|
825
|
+
onChange: (v) => set("doNotSyncZeroStock", v)
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
] }),
|
|
829
|
+
/* @__PURE__ */ jsxs(Section, { title: "Location mapping", children: [
|
|
830
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
831
|
+
"Map each Medusa stock location to an Odoo warehouse.",
|
|
832
|
+
optionsLoading && " (loading from Odoo…)"
|
|
833
|
+
] }),
|
|
834
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
835
|
+
s.locationMappings.length === 0 && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No mappings yet." }),
|
|
836
|
+
s.locationMappings.map((m, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
837
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxs(
|
|
838
|
+
Select,
|
|
839
|
+
{
|
|
840
|
+
value: m.medusaLocationId,
|
|
841
|
+
onValueChange: (v) => updateMapping(i, { medusaLocationId: v }),
|
|
842
|
+
children: [
|
|
843
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Medusa location" }) }),
|
|
844
|
+
/* @__PURE__ */ jsx(Select.Content, { children: locations.map((l) => /* @__PURE__ */ jsx(Select.Item, { value: l.id, children: l.name }, l.id)) })
|
|
845
|
+
]
|
|
846
|
+
}
|
|
847
|
+
) }),
|
|
848
|
+
/* @__PURE__ */ jsx("span", { className: "text-ui-fg-muted", children: "↔" }),
|
|
849
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxs(
|
|
850
|
+
Select,
|
|
851
|
+
{
|
|
852
|
+
value: m.odooWarehouseId != null ? String(m.odooWarehouseId) : "",
|
|
853
|
+
onValueChange: (v) => updateMapping(i, { odooWarehouseId: v ? Number(v) : null }),
|
|
854
|
+
children: [
|
|
855
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Odoo warehouse" }) }),
|
|
856
|
+
/* @__PURE__ */ jsx(Select.Content, { children: warehouses.map((w) => /* @__PURE__ */ jsx(Select.Item, { value: String(w.id), children: w.name }, w.id)) })
|
|
857
|
+
]
|
|
858
|
+
}
|
|
859
|
+
) }),
|
|
860
|
+
/* @__PURE__ */ jsx(IconButton, { variant: "transparent", onClick: () => removeMapping(i), "aria-label": "Remove mapping", children: /* @__PURE__ */ jsx(Trash, {}) })
|
|
861
|
+
] }, i))
|
|
862
|
+
] }),
|
|
863
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(OdooButton, { variant: "secondary", icon: Plus, onClick: addMapping, children: "Add mapping" }) })
|
|
864
|
+
] })
|
|
865
|
+
] }) })
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
};
|
|
869
|
+
const OdooInventoryPage = () => /* @__PURE__ */ jsx(InventoryForm, {});
|
|
870
|
+
const config$7 = defineRouteConfig({
|
|
871
|
+
label: "Inventory",
|
|
872
|
+
icon: Buildings,
|
|
873
|
+
rank: 3
|
|
874
|
+
});
|
|
875
|
+
const TYPE_LABEL = {
|
|
876
|
+
product_sync: "Product Sync",
|
|
877
|
+
order_sync: "Order Sync",
|
|
878
|
+
inventory_sync: "Inventory Sync",
|
|
879
|
+
customer_sync: "Customer Sync"
|
|
880
|
+
};
|
|
881
|
+
const STATUS_PILL = {
|
|
882
|
+
success: { color: "green", label: "Success" },
|
|
883
|
+
failed: { color: "red", label: "Failed" },
|
|
884
|
+
skipped: { color: "grey", label: "Skipped" }
|
|
885
|
+
};
|
|
886
|
+
function StatusBadge({ status }) {
|
|
887
|
+
const b = STATUS_PILL[status] ?? { color: "grey", label: status };
|
|
888
|
+
return /* @__PURE__ */ jsx(OdooPill, { color: b.color, children: b.label });
|
|
889
|
+
}
|
|
890
|
+
function formatTimestamp(iso) {
|
|
891
|
+
const d = new Date(iso);
|
|
892
|
+
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
|
|
893
|
+
}
|
|
894
|
+
const PAGE_SIZE = 20;
|
|
895
|
+
const LogTable = () => {
|
|
896
|
+
const [logs, setLogs] = useState([]);
|
|
897
|
+
const [loading, setLoading] = useState(true);
|
|
898
|
+
const [clearing, setClearing] = useState(false);
|
|
899
|
+
const [typeFilter, setTypeFilter] = useState("all");
|
|
900
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
901
|
+
const [fromDate, setFromDate] = useState("");
|
|
902
|
+
const [toDate, setToDate] = useState("");
|
|
903
|
+
const [page, setPage] = useState(0);
|
|
904
|
+
const [total, setTotal] = useState(0);
|
|
905
|
+
const prompt = usePrompt();
|
|
906
|
+
const buildParams = () => {
|
|
907
|
+
const params = new URLSearchParams();
|
|
908
|
+
if (typeFilter !== "all") params.set("type", typeFilter);
|
|
909
|
+
if (statusFilter !== "all") params.set("status", statusFilter);
|
|
910
|
+
if (fromDate) params.set("from", fromDate);
|
|
911
|
+
if (toDate) params.set("to", toDate);
|
|
912
|
+
return params;
|
|
913
|
+
};
|
|
914
|
+
const load = async (pageArg = page) => {
|
|
915
|
+
setLoading(true);
|
|
916
|
+
const params = buildParams();
|
|
917
|
+
params.set("limit", String(PAGE_SIZE));
|
|
918
|
+
params.set("offset", String(pageArg * PAGE_SIZE));
|
|
919
|
+
try {
|
|
920
|
+
const res = await api.get(`/odoo/logs?${params}`);
|
|
921
|
+
setLogs(res.logs);
|
|
922
|
+
setTotal(res.count);
|
|
923
|
+
} catch (e) {
|
|
924
|
+
toast.error("Failed to load logs", { description: e.message });
|
|
925
|
+
} finally {
|
|
926
|
+
setLoading(false);
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
const onFilter = (fn) => {
|
|
930
|
+
fn();
|
|
931
|
+
setPage(0);
|
|
932
|
+
};
|
|
933
|
+
const filtered = typeFilter !== "all" || statusFilter !== "all" || !!fromDate || !!toDate;
|
|
934
|
+
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
935
|
+
const clear = async () => {
|
|
936
|
+
const confirmed = await prompt({
|
|
937
|
+
title: filtered ? "Clear filtered logs?" : "Clear all logs?",
|
|
938
|
+
description: filtered ? "This permanently deletes the log entries matching the current filters. This cannot be undone." : "This permanently deletes the entire sync history. This cannot be undone.",
|
|
939
|
+
confirmText: "Clear logs",
|
|
940
|
+
cancelText: "Cancel"
|
|
941
|
+
});
|
|
942
|
+
if (!confirmed) return;
|
|
943
|
+
const params = buildParams();
|
|
944
|
+
setClearing(true);
|
|
945
|
+
try {
|
|
946
|
+
const res = await api.del(
|
|
947
|
+
`/odoo/logs${params.toString() ? `?${params}` : ""}`
|
|
948
|
+
);
|
|
949
|
+
toast.success(`Cleared ${res.deleted} log${res.deleted === 1 ? "" : "s"}`);
|
|
950
|
+
setPage(0);
|
|
951
|
+
await load(0);
|
|
952
|
+
} catch (e) {
|
|
953
|
+
toast.error("Failed to clear logs", { description: e.message });
|
|
954
|
+
} finally {
|
|
955
|
+
setClearing(false);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
useEffect(() => {
|
|
959
|
+
load();
|
|
960
|
+
}, [typeFilter, statusFilter, fromDate, toDate, page]);
|
|
961
|
+
return /* @__PURE__ */ jsx(
|
|
962
|
+
OdooShell,
|
|
963
|
+
{
|
|
964
|
+
current: "logs",
|
|
965
|
+
title: "Logs",
|
|
966
|
+
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
967
|
+
/* @__PURE__ */ jsx(
|
|
968
|
+
OdooButton,
|
|
969
|
+
{
|
|
970
|
+
variant: "secondary",
|
|
971
|
+
icon: Trash,
|
|
972
|
+
onClick: clear,
|
|
973
|
+
loading: clearing,
|
|
974
|
+
disabled: loading || logs.length === 0,
|
|
975
|
+
children: "Clear logs"
|
|
976
|
+
}
|
|
977
|
+
),
|
|
978
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "secondary", icon: ArrowPath, onClick: () => load(), loading, children: "Refresh" })
|
|
979
|
+
] }),
|
|
980
|
+
children: /* @__PURE__ */ jsxs("div", { className: "border border-ui-border-base rounded-lg overflow-hidden", children: [
|
|
981
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-x-2 p-3 border-b border-ui-border-base flex-wrap gap-y-2", children: [
|
|
982
|
+
/* @__PURE__ */ jsxs(Select, { value: typeFilter, onValueChange: (v) => onFilter(() => setTypeFilter(v)), size: "small", children: [
|
|
983
|
+
/* @__PURE__ */ jsx(Select.Trigger, { className: "w-44", children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Filter by type" }) }),
|
|
984
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
985
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "all", children: "All types" }),
|
|
986
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "product_sync", children: "Product Sync" }),
|
|
987
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "order_sync", children: "Order Sync" }),
|
|
988
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "inventory_sync", children: "Inventory Sync" }),
|
|
989
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "customer_sync", children: "Customer Sync" })
|
|
990
|
+
] })
|
|
991
|
+
] }),
|
|
992
|
+
/* @__PURE__ */ jsxs(Select, { value: statusFilter, onValueChange: (v) => onFilter(() => setStatusFilter(v)), size: "small", children: [
|
|
993
|
+
/* @__PURE__ */ jsx(Select.Trigger, { className: "w-40", children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Filter by status" }) }),
|
|
994
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
995
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "all", children: "All statuses" }),
|
|
996
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "success", children: "Success" }),
|
|
997
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "failed", children: "Failed" }),
|
|
998
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "skipped", children: "Skipped" })
|
|
999
|
+
] })
|
|
1000
|
+
] }),
|
|
1001
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-1.5", children: [
|
|
1002
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "From" }),
|
|
1003
|
+
/* @__PURE__ */ jsx(
|
|
1004
|
+
"input",
|
|
1005
|
+
{
|
|
1006
|
+
type: "date",
|
|
1007
|
+
value: fromDate,
|
|
1008
|
+
max: toDate || void 0,
|
|
1009
|
+
onChange: (e) => onFilter(() => setFromDate(e.target.value)),
|
|
1010
|
+
className: "h-8 rounded-md border border-ui-border-base bg-ui-bg-field px-2 text-sm text-ui-fg-base focus:outline-none focus:shadow-borders-interactive-with-active"
|
|
1011
|
+
}
|
|
1012
|
+
),
|
|
1013
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "To" }),
|
|
1014
|
+
/* @__PURE__ */ jsx(
|
|
1015
|
+
"input",
|
|
1016
|
+
{
|
|
1017
|
+
type: "date",
|
|
1018
|
+
value: toDate,
|
|
1019
|
+
min: fromDate || void 0,
|
|
1020
|
+
onChange: (e) => onFilter(() => setToDate(e.target.value)),
|
|
1021
|
+
className: "h-8 rounded-md border border-ui-border-base bg-ui-bg-field px-2 text-sm text-ui-fg-base focus:outline-none focus:shadow-borders-interactive-with-active"
|
|
1022
|
+
}
|
|
1023
|
+
),
|
|
1024
|
+
(fromDate || toDate) && /* @__PURE__ */ jsx(
|
|
1025
|
+
"button",
|
|
1026
|
+
{
|
|
1027
|
+
type: "button",
|
|
1028
|
+
onClick: () => onFilter(() => {
|
|
1029
|
+
setFromDate("");
|
|
1030
|
+
setToDate("");
|
|
1031
|
+
}),
|
|
1032
|
+
className: "text-ui-fg-muted hover:text-ui-fg-base text-sm px-1",
|
|
1033
|
+
"aria-label": "Clear date range",
|
|
1034
|
+
title: "Clear date range",
|
|
1035
|
+
children: "✕"
|
|
1036
|
+
}
|
|
1037
|
+
)
|
|
1038
|
+
] })
|
|
1039
|
+
] }),
|
|
1040
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
1041
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
1042
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Timestamp" }),
|
|
1043
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Type" }),
|
|
1044
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Status" }),
|
|
1045
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Message" })
|
|
1046
|
+
] }) }),
|
|
1047
|
+
/* @__PURE__ */ jsxs(Table.Body, { children: [
|
|
1048
|
+
logs.length === 0 && !loading && /* @__PURE__ */ jsx(Table.Row, { children: /* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle py-2", children: "No logs found." }) }) }),
|
|
1049
|
+
logs.map((log) => /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
1050
|
+
/* @__PURE__ */ jsx(Table.Cell, { className: "whitespace-nowrap", children: formatTimestamp(log.timestamp) }),
|
|
1051
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: TYPE_LABEL[log.type] ?? log.type }),
|
|
1052
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(StatusBadge, { status: log.status }) }),
|
|
1053
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: log.message })
|
|
1054
|
+
] }, log.id))
|
|
1055
|
+
] })
|
|
1056
|
+
] }),
|
|
1057
|
+
total > 0 && /* @__PURE__ */ jsx(
|
|
1058
|
+
Table.Pagination,
|
|
1059
|
+
{
|
|
1060
|
+
count: total,
|
|
1061
|
+
pageSize: PAGE_SIZE,
|
|
1062
|
+
pageIndex: page,
|
|
1063
|
+
pageCount,
|
|
1064
|
+
canPreviousPage: page > 0,
|
|
1065
|
+
canNextPage: page < pageCount - 1,
|
|
1066
|
+
previousPage: () => setPage((p) => Math.max(0, p - 1)),
|
|
1067
|
+
nextPage: () => setPage((p) => Math.min(pageCount - 1, p + 1))
|
|
1068
|
+
}
|
|
1069
|
+
)
|
|
1070
|
+
] })
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
};
|
|
1074
|
+
const OdooLogsPage = () => /* @__PURE__ */ jsx(LogTable, {});
|
|
1075
|
+
const config$6 = defineRouteConfig({
|
|
1076
|
+
label: "Logs",
|
|
1077
|
+
icon: DocumentText,
|
|
1078
|
+
rank: 7
|
|
1079
|
+
});
|
|
1080
|
+
const customerName = (o) => {
|
|
1081
|
+
var _a, _b, _c;
|
|
1082
|
+
const full = [(_a = o.customer) == null ? void 0 : _a.first_name, (_b = o.customer) == null ? void 0 : _b.last_name].filter(Boolean).join(" ");
|
|
1083
|
+
return full || ((_c = o.customer) == null ? void 0 : _c.email) || o.email || "—";
|
|
1084
|
+
};
|
|
1085
|
+
const orderStatus = (o) => {
|
|
1086
|
+
if (o.status === "canceled") return { label: "Canceled", color: "red" };
|
|
1087
|
+
if (o.status === "completed" || o.status === "archived")
|
|
1088
|
+
return { label: "Completed", color: "green" };
|
|
1089
|
+
const pay = o.payment_status;
|
|
1090
|
+
const ful = o.fulfillment_status;
|
|
1091
|
+
const delivered = ful === "delivered" || ful === "fulfilled";
|
|
1092
|
+
if (delivered && pay === "captured") return { label: "Completed", color: "green" };
|
|
1093
|
+
if (pay === "refunded") return { label: "Refunded", color: "red" };
|
|
1094
|
+
if (pay === "partially_refunded") return { label: "Part. Refunded", color: "orange" };
|
|
1095
|
+
if (delivered) return { label: "Delivered", color: "green" };
|
|
1096
|
+
if (ful === "shipped" || ful === "partially_shipped") return { label: "Shipped", color: "blue" };
|
|
1097
|
+
if (ful === "partially_delivered" || ful === "partially_fulfilled")
|
|
1098
|
+
return { label: "Fulfilling", color: "orange" };
|
|
1099
|
+
if (pay === "captured") return { label: "Paid", color: "blue" };
|
|
1100
|
+
if (pay === "not_paid") return { label: "Awaiting Payment", color: "grey" };
|
|
1101
|
+
return { label: "Pending", color: "grey" };
|
|
1102
|
+
};
|
|
1103
|
+
const money = (amount, currency) => {
|
|
1104
|
+
try {
|
|
1105
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency: currency.toUpperCase() }).format(amount);
|
|
1106
|
+
} catch {
|
|
1107
|
+
return `${amount} ${(currency == null ? void 0 : currency.toUpperCase()) ?? ""}`;
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
const fmtDate = (iso) => {
|
|
1111
|
+
try {
|
|
1112
|
+
return new Date(iso).toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" });
|
|
1113
|
+
} catch {
|
|
1114
|
+
return iso;
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
const DashboardView = () => {
|
|
1118
|
+
const [loading, setLoading] = useState(true);
|
|
1119
|
+
const [connected, setConnected] = useState(null);
|
|
1120
|
+
const [counts, setCounts] = useState({ orders: 0, products: 0, customers: 0, synced: 0 });
|
|
1121
|
+
const [orders, setOrders] = useState([]);
|
|
1122
|
+
const load = async () => {
|
|
1123
|
+
setLoading(true);
|
|
1124
|
+
try {
|
|
1125
|
+
const [settings, ordersRes, products, customers, logs] = await Promise.all([
|
|
1126
|
+
api.get("/odoo/settings").catch(() => null),
|
|
1127
|
+
medusaApi.get(
|
|
1128
|
+
"/admin/orders?limit=10&order=-created_at&fields=id,display_id,status,payment_status,fulfillment_status,total,currency_code,created_at,email,customer.first_name,customer.last_name,customer.email"
|
|
1129
|
+
).catch(() => ({ orders: [], count: 0 })),
|
|
1130
|
+
medusaApi.get("/admin/products?limit=1").catch(() => ({ count: 0 })),
|
|
1131
|
+
medusaApi.get("/admin/customers?limit=1").catch(() => ({ count: 0 })),
|
|
1132
|
+
api.get("/odoo/logs?limit=1").catch(() => ({ count: 0 }))
|
|
1133
|
+
]);
|
|
1134
|
+
setConnected(settings ? settings.configured : null);
|
|
1135
|
+
setCounts({
|
|
1136
|
+
orders: ordersRes.count ?? 0,
|
|
1137
|
+
products: products.count ?? 0,
|
|
1138
|
+
customers: customers.count ?? 0,
|
|
1139
|
+
synced: logs.count ?? 0
|
|
1140
|
+
});
|
|
1141
|
+
setOrders(ordersRes.orders ?? []);
|
|
1142
|
+
} finally {
|
|
1143
|
+
setLoading(false);
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
useEffect(() => {
|
|
1147
|
+
load();
|
|
1148
|
+
}, []);
|
|
1149
|
+
return /* @__PURE__ */ jsxs(
|
|
1150
|
+
OdooShell,
|
|
1151
|
+
{
|
|
1152
|
+
current: "dashboard",
|
|
1153
|
+
title: "Dashboard",
|
|
1154
|
+
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1155
|
+
connected === null ? null : connected ? /* @__PURE__ */ jsx(OdooPill, { color: "green", children: "Odoo connected" }) : /* @__PURE__ */ jsx(OdooPill, { color: "grey", children: "Not connected" }),
|
|
1156
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "secondary", icon: ArrowPath, onClick: load, loading, children: "Refresh" })
|
|
1157
|
+
] }),
|
|
1158
|
+
children: [
|
|
1159
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4 mb-6", children: [
|
|
1160
|
+
/* @__PURE__ */ jsx(OdooStat, { label: "Orders", value: counts.orders, icon: ShoppingCart, loading }),
|
|
1161
|
+
/* @__PURE__ */ jsx(OdooStat, { label: "Products", value: counts.products, icon: CubeSolid, loading }),
|
|
1162
|
+
/* @__PURE__ */ jsx(OdooStat, { label: "Customers", value: counts.customers, icon: Users, loading }),
|
|
1163
|
+
/* @__PURE__ */ jsx(OdooStat, { label: "Sync log entries", value: counts.synced, icon: DocumentText, loading })
|
|
1164
|
+
] }),
|
|
1165
|
+
/* @__PURE__ */ jsxs("div", { className: "border border-ui-border-base rounded-lg overflow-hidden", children: [
|
|
1166
|
+
/* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-b border-ui-border-base", children: /* @__PURE__ */ jsx(Text, { weight: "plus", children: "Recent orders" }) }),
|
|
1167
|
+
orders.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle p-4", children: loading ? "Loading orders…" : "No orders yet." }) : /* @__PURE__ */ jsxs(Table, { children: [
|
|
1168
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
1169
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Order" }),
|
|
1170
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Customer" }),
|
|
1171
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Date" }),
|
|
1172
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Status" }),
|
|
1173
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { className: "text-right", children: "Total" })
|
|
1174
|
+
] }) }),
|
|
1175
|
+
/* @__PURE__ */ jsx(Table.Body, { children: orders.map((o) => {
|
|
1176
|
+
const s = orderStatus(o);
|
|
1177
|
+
return /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
1178
|
+
/* @__PURE__ */ jsxs(Table.Cell, { children: [
|
|
1179
|
+
"#",
|
|
1180
|
+
o.display_id
|
|
1181
|
+
] }),
|
|
1182
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: customerName(o) }),
|
|
1183
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: fmtDate(o.created_at) }),
|
|
1184
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(OdooPill, { color: s.color, children: s.label }) }),
|
|
1185
|
+
/* @__PURE__ */ jsx(Table.Cell, { className: "text-right", children: money(o.total, o.currency_code) })
|
|
1186
|
+
] }, o.id);
|
|
1187
|
+
}) })
|
|
1188
|
+
] })
|
|
1189
|
+
] })
|
|
1190
|
+
]
|
|
1191
|
+
}
|
|
1192
|
+
);
|
|
1193
|
+
};
|
|
1194
|
+
const OdooDashboardPage = () => /* @__PURE__ */ jsx(DashboardView, {});
|
|
1195
|
+
const config$5 = defineRouteConfig({
|
|
1196
|
+
label: "Dashboard",
|
|
1197
|
+
icon: ChartBar,
|
|
1198
|
+
rank: 1
|
|
1199
|
+
});
|
|
1200
|
+
const DEFAULTS$2 = {
|
|
1201
|
+
direction: "odoo_to_medusa",
|
|
1202
|
+
customerSync: true,
|
|
1203
|
+
creationMode: "create_update",
|
|
1204
|
+
tagFilterEnabled: false,
|
|
1205
|
+
odooTagIds: [],
|
|
1206
|
+
medusaGroupIds: []
|
|
1207
|
+
};
|
|
1208
|
+
const CustomerSettingsForm = () => {
|
|
1209
|
+
const [loading, setLoading] = useState(true);
|
|
1210
|
+
const [optionsLoading, setOptionsLoading] = useState(true);
|
|
1211
|
+
const [saving, setSaving] = useState(false);
|
|
1212
|
+
const [running, setRunning] = useState(false);
|
|
1213
|
+
const [s, setS] = useState(DEFAULTS$2);
|
|
1214
|
+
const [tags, setTags] = useState([]);
|
|
1215
|
+
const [groups, setGroups] = useState([]);
|
|
1216
|
+
const set = (k, v) => setS((p) => ({ ...p, [k]: v }));
|
|
1217
|
+
useEffect(() => {
|
|
1218
|
+
let active = true;
|
|
1219
|
+
api.get("/odoo/sections/customers").then((r) => active && r.data && setS({ ...DEFAULTS$2, ...r.data })).catch((e) => toast.error("Failed to load", { description: e.message })).finally(() => active && setLoading(false));
|
|
1220
|
+
Promise.all([
|
|
1221
|
+
api.get("/odoo/customer-options"),
|
|
1222
|
+
medusaApi.get("/admin/customer-groups?limit=100").catch(() => ({ customer_groups: [] }))
|
|
1223
|
+
]).then(([opts, grp]) => {
|
|
1224
|
+
if (!active) return;
|
|
1225
|
+
setTags(opts.tags ?? []);
|
|
1226
|
+
setGroups(grp.customer_groups ?? []);
|
|
1227
|
+
}).catch(() => {
|
|
1228
|
+
}).finally(() => active && setOptionsLoading(false));
|
|
1229
|
+
return () => {
|
|
1230
|
+
active = false;
|
|
1231
|
+
};
|
|
1232
|
+
}, []);
|
|
1233
|
+
const toggleTag = (id) => {
|
|
1234
|
+
const n = Number(id);
|
|
1235
|
+
set("odooTagIds", s.odooTagIds.includes(n) ? s.odooTagIds.filter((x) => x !== n) : [...s.odooTagIds, n]);
|
|
1236
|
+
};
|
|
1237
|
+
const toggleGroup = (id) => set("medusaGroupIds", s.medusaGroupIds.includes(id) ? s.medusaGroupIds.filter((x) => x !== id) : [...s.medusaGroupIds, id]);
|
|
1238
|
+
const handleSave = async () => {
|
|
1239
|
+
setSaving(true);
|
|
1240
|
+
try {
|
|
1241
|
+
await api.post("/odoo/sections/customers", s);
|
|
1242
|
+
toast.success("Customer settings saved");
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
toast.error("Failed to save", { description: e.message });
|
|
1245
|
+
} finally {
|
|
1246
|
+
setSaving(false);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
const run = async (label) => {
|
|
1250
|
+
setRunning(true);
|
|
1251
|
+
try {
|
|
1252
|
+
const r = await api.post("/odoo/sync/customers");
|
|
1253
|
+
toast.success(label, { description: r.message });
|
|
1254
|
+
} catch (e) {
|
|
1255
|
+
toast.error("Sync failed", { description: e.message });
|
|
1256
|
+
} finally {
|
|
1257
|
+
setRunning(false);
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
const odooToMedusa = s.direction === "odoo_to_medusa";
|
|
1261
|
+
return /* @__PURE__ */ jsx(
|
|
1262
|
+
OdooShell,
|
|
1263
|
+
{
|
|
1264
|
+
current: "customers",
|
|
1265
|
+
title: "Customers",
|
|
1266
|
+
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1267
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "teal", icon: ArrowPath, onClick: () => run(odooToMedusa ? "Import started" : "Export started"), loading: running, children: odooToMedusa ? "Import now" : "Export now" }),
|
|
1268
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "primary", onClick: handleSave, loading: saving, children: "Save" })
|
|
1269
|
+
] }),
|
|
1270
|
+
children: /* @__PURE__ */ jsx(OdooSheet, { children: loading ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading customer settings…" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1271
|
+
/* @__PURE__ */ jsxs(
|
|
1272
|
+
OdooSection,
|
|
1273
|
+
{
|
|
1274
|
+
icon: Users,
|
|
1275
|
+
title: "Customer synchronization",
|
|
1276
|
+
sub: odooToMedusa ? "Customers are pulled from Odoo into Medusa." : "Customers are pushed from Medusa into Odoo.",
|
|
1277
|
+
children: [
|
|
1278
|
+
/* @__PURE__ */ jsx(
|
|
1279
|
+
SelectField,
|
|
1280
|
+
{
|
|
1281
|
+
label: "Sync direction",
|
|
1282
|
+
value: s.direction,
|
|
1283
|
+
onChange: (v) => set("direction", v),
|
|
1284
|
+
options: [
|
|
1285
|
+
{ value: "odoo_to_medusa", label: "Odoo → Medusa (import)" },
|
|
1286
|
+
{ value: "medusa_to_odoo", label: "Medusa → Odoo (export)" }
|
|
1287
|
+
]
|
|
1288
|
+
}
|
|
1289
|
+
),
|
|
1290
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Enable customer sync", checked: s.customerSync, onChange: (v) => set("customerSync", v) }),
|
|
1291
|
+
/* @__PURE__ */ jsx(
|
|
1292
|
+
SelectField,
|
|
1293
|
+
{
|
|
1294
|
+
label: "Creation mode",
|
|
1295
|
+
value: s.creationMode,
|
|
1296
|
+
onChange: (v) => set("creationMode", v),
|
|
1297
|
+
options: [
|
|
1298
|
+
{ value: "create", label: "Create new only" },
|
|
1299
|
+
{ value: "update", label: "Update existing only" },
|
|
1300
|
+
{ value: "create_update", label: "Create & update" }
|
|
1301
|
+
]
|
|
1302
|
+
}
|
|
1303
|
+
)
|
|
1304
|
+
]
|
|
1305
|
+
}
|
|
1306
|
+
),
|
|
1307
|
+
/* @__PURE__ */ jsxs(Section, { title: "Tag filter", children: [
|
|
1308
|
+
/* @__PURE__ */ jsx(
|
|
1309
|
+
ToggleRow,
|
|
1310
|
+
{
|
|
1311
|
+
label: "Filter by tags",
|
|
1312
|
+
hint: "Only sync customers with the selected tags.",
|
|
1313
|
+
checked: s.tagFilterEnabled,
|
|
1314
|
+
onChange: (v) => set("tagFilterEnabled", v)
|
|
1315
|
+
}
|
|
1316
|
+
),
|
|
1317
|
+
s.tagFilterEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1318
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1.5", children: [
|
|
1319
|
+
/* @__PURE__ */ jsx(Label, { size: "small", weight: "plus", children: "Odoo partner tags" }),
|
|
1320
|
+
/* @__PURE__ */ jsx(
|
|
1321
|
+
CheckList,
|
|
1322
|
+
{
|
|
1323
|
+
items: tags.map((t) => ({ id: String(t.id), label: t.name })),
|
|
1324
|
+
selected: new Set(s.odooTagIds.map(String)),
|
|
1325
|
+
onToggle: toggleTag,
|
|
1326
|
+
empty: optionsLoading ? "Loading from Odoo…" : "No partner tags in Odoo."
|
|
1327
|
+
}
|
|
1328
|
+
)
|
|
1329
|
+
] }),
|
|
1330
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1.5", children: [
|
|
1331
|
+
/* @__PURE__ */ jsx(Label, { size: "small", weight: "plus", children: "Medusa customer groups" }),
|
|
1332
|
+
/* @__PURE__ */ jsx(
|
|
1333
|
+
CheckList,
|
|
1334
|
+
{
|
|
1335
|
+
items: groups.map((g) => ({ id: g.id, label: g.name })),
|
|
1336
|
+
selected: new Set(s.medusaGroupIds),
|
|
1337
|
+
onToggle: toggleGroup,
|
|
1338
|
+
empty: "No customer groups in Medusa."
|
|
1339
|
+
}
|
|
1340
|
+
)
|
|
1341
|
+
] })
|
|
1342
|
+
] })
|
|
1343
|
+
] })
|
|
1344
|
+
] }) })
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
};
|
|
1348
|
+
const OdooCustomersPage = () => /* @__PURE__ */ jsx(CustomerSettingsForm, {});
|
|
1349
|
+
const config$4 = defineRouteConfig({
|
|
1350
|
+
label: "Customers",
|
|
1351
|
+
icon: Users,
|
|
1352
|
+
rank: 5
|
|
1353
|
+
});
|
|
1354
|
+
const DEFAULTS$1 = {
|
|
1355
|
+
orderSync: false,
|
|
1356
|
+
exportInvoice: false,
|
|
1357
|
+
invoiceMode: "paid",
|
|
1358
|
+
markInvoicePaid: false,
|
|
1359
|
+
syncRefunds: false,
|
|
1360
|
+
restock: false,
|
|
1361
|
+
syncOrderStatus: false,
|
|
1362
|
+
createPayment: false,
|
|
1363
|
+
paymentJournalId: null
|
|
1364
|
+
};
|
|
1365
|
+
const OrderSettingsForm = () => {
|
|
1366
|
+
const [loading, setLoading] = useState(true);
|
|
1367
|
+
const [optionsLoading, setOptionsLoading] = useState(true);
|
|
1368
|
+
const [saving, setSaving] = useState(false);
|
|
1369
|
+
const [running, setRunning] = useState(false);
|
|
1370
|
+
const [clearing, setClearing] = useState(false);
|
|
1371
|
+
const [s, setS] = useState(DEFAULTS$1);
|
|
1372
|
+
const [journals, setJournals] = useState([]);
|
|
1373
|
+
const set = (k, v) => setS((p) => ({ ...p, [k]: v }));
|
|
1374
|
+
useEffect(() => {
|
|
1375
|
+
let active = true;
|
|
1376
|
+
api.get("/odoo/sections/orders").then((r) => active && r.data && setS({ ...DEFAULTS$1, ...r.data })).catch((e) => toast.error("Failed to load", { description: e.message })).finally(() => active && setLoading(false));
|
|
1377
|
+
api.get("/odoo/order-options").then((o) => active && setJournals(o.journals ?? [])).catch(() => {
|
|
1378
|
+
}).finally(() => active && setOptionsLoading(false));
|
|
1379
|
+
return () => {
|
|
1380
|
+
active = false;
|
|
1381
|
+
};
|
|
1382
|
+
}, []);
|
|
1383
|
+
const handleSave = async () => {
|
|
1384
|
+
setSaving(true);
|
|
1385
|
+
try {
|
|
1386
|
+
await api.post("/odoo/sections/orders", s);
|
|
1387
|
+
toast.success("Order settings saved");
|
|
1388
|
+
} catch (e) {
|
|
1389
|
+
toast.error("Failed to save", { description: e.message });
|
|
1390
|
+
} finally {
|
|
1391
|
+
setSaving(false);
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
const syncStatus = async () => {
|
|
1395
|
+
setRunning(true);
|
|
1396
|
+
try {
|
|
1397
|
+
const r = await api.post("/odoo/sync/orders");
|
|
1398
|
+
toast.success("Order status sync started", { description: r.message });
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
toast.error("Sync failed", { description: e.message });
|
|
1401
|
+
} finally {
|
|
1402
|
+
setRunning(false);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
const clearLinks = async () => {
|
|
1406
|
+
if (!window.confirm(
|
|
1407
|
+
"Remove the Odoo link (odoo_order_id) from synced orders? The orders are kept and nothing changes in Odoo — they just become eligible to sync again (a re-sync would create fresh sale orders in Odoo)."
|
|
1408
|
+
))
|
|
1409
|
+
return;
|
|
1410
|
+
setClearing(true);
|
|
1411
|
+
try {
|
|
1412
|
+
const r = await api.post("/odoo/clear-orders");
|
|
1413
|
+
toast.success("Cleared Odoo links", {
|
|
1414
|
+
description: `${r.cleared} order(s) unlinked. Orders kept.`
|
|
1415
|
+
});
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
toast.error("Failed to clear", { description: e.message });
|
|
1418
|
+
} finally {
|
|
1419
|
+
setClearing(false);
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
const gate = !s.orderSync;
|
|
1423
|
+
const journalOptions = journals.map((j) => ({
|
|
1424
|
+
value: String(j.id),
|
|
1425
|
+
label: `${j.name} (${j.type})`
|
|
1426
|
+
}));
|
|
1427
|
+
return /* @__PURE__ */ jsx(
|
|
1428
|
+
OdooShell,
|
|
1429
|
+
{
|
|
1430
|
+
current: "orders",
|
|
1431
|
+
title: "Orders",
|
|
1432
|
+
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1433
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "teal", icon: ArrowPath, onClick: syncStatus, loading: running, disabled: gate, children: "Sync now" }),
|
|
1434
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "secondary", onClick: clearLinks, loading: clearing, children: "Clear links" }),
|
|
1435
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "primary", onClick: handleSave, loading: saving, children: "Save" })
|
|
1436
|
+
] }),
|
|
1437
|
+
children: /* @__PURE__ */ jsx(OdooSheet, { children: loading ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading order settings…" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1438
|
+
/* @__PURE__ */ jsx(
|
|
1439
|
+
OdooSection,
|
|
1440
|
+
{
|
|
1441
|
+
icon: ShoppingCart,
|
|
1442
|
+
title: "Order synchronization",
|
|
1443
|
+
sub: "Push Medusa orders to Odoo as sale orders.",
|
|
1444
|
+
children: /* @__PURE__ */ jsx(
|
|
1445
|
+
ToggleRow,
|
|
1446
|
+
{
|
|
1447
|
+
label: "Enable order sync",
|
|
1448
|
+
hint: "Automatically export new orders when they are placed.",
|
|
1449
|
+
checked: s.orderSync,
|
|
1450
|
+
onChange: (v) => set("orderSync", v)
|
|
1451
|
+
}
|
|
1452
|
+
)
|
|
1453
|
+
}
|
|
1454
|
+
),
|
|
1455
|
+
/* @__PURE__ */ jsxs(Section, { title: "Invoicing", children: [
|
|
1456
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Export invoice", checked: s.exportInvoice, onChange: (v) => set("exportInvoice", v), disabled: gate }),
|
|
1457
|
+
s.exportInvoice && !gate && /* @__PURE__ */ jsx(
|
|
1458
|
+
SelectField,
|
|
1459
|
+
{
|
|
1460
|
+
label: "Create invoice for",
|
|
1461
|
+
value: s.invoiceMode,
|
|
1462
|
+
onChange: (v) => set("invoiceMode", v),
|
|
1463
|
+
options: [
|
|
1464
|
+
{ value: "all", label: "All orders" },
|
|
1465
|
+
{ value: "paid", label: "Paid orders only" },
|
|
1466
|
+
{ value: "fulfilled", label: "Fulfilled orders only" }
|
|
1467
|
+
]
|
|
1468
|
+
}
|
|
1469
|
+
),
|
|
1470
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Mark invoice as paid", checked: s.markInvoicePaid, onChange: (v) => set("markInvoicePaid", v), disabled: gate || !s.exportInvoice })
|
|
1471
|
+
] }),
|
|
1472
|
+
/* @__PURE__ */ jsxs(Section, { title: "Refunds & status", children: [
|
|
1473
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Sync refunds", checked: s.syncRefunds, onChange: (v) => set("syncRefunds", v ? (set("restock", false), true) : false), disabled: gate }),
|
|
1474
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Restock on refund", hint: "Mutually exclusive with sync refunds.", checked: s.restock, onChange: (v) => set("restock", v ? (set("syncRefunds", false), true) : false), disabled: gate }),
|
|
1475
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Sync order status (Odoo → Medusa)", checked: s.syncOrderStatus, onChange: (v) => set("syncOrderStatus", v), disabled: gate })
|
|
1476
|
+
] }),
|
|
1477
|
+
/* @__PURE__ */ jsxs(Section, { title: "Payments", children: [
|
|
1478
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Create payment in Odoo", checked: s.createPayment, onChange: (v) => set("createPayment", v), disabled: gate }),
|
|
1479
|
+
s.createPayment && !gate && /* @__PURE__ */ jsx(
|
|
1480
|
+
SelectField,
|
|
1481
|
+
{
|
|
1482
|
+
label: "Payment journal",
|
|
1483
|
+
help: "Odoo journal used to register the payment.",
|
|
1484
|
+
value: s.paymentJournalId != null ? String(s.paymentJournalId) : "",
|
|
1485
|
+
onChange: (v) => set("paymentJournalId", v ? Number(v) : null),
|
|
1486
|
+
options: journalOptions,
|
|
1487
|
+
placeholder: optionsLoading ? "Loading from Odoo…" : journalOptions.length ? "Select journal" : "No journals",
|
|
1488
|
+
disabled: optionsLoading || !journalOptions.length
|
|
1489
|
+
}
|
|
1490
|
+
)
|
|
1491
|
+
] })
|
|
1492
|
+
] }) })
|
|
1493
|
+
}
|
|
1494
|
+
);
|
|
1495
|
+
};
|
|
1496
|
+
const OdooOrdersPage = () => /* @__PURE__ */ jsx(OrderSettingsForm, {});
|
|
1497
|
+
const config$3 = defineRouteConfig({
|
|
1498
|
+
label: "Orders",
|
|
1499
|
+
icon: ShoppingCart,
|
|
1500
|
+
rank: 4
|
|
1501
|
+
});
|
|
1502
|
+
const PLANS = [
|
|
1503
|
+
{
|
|
1504
|
+
id: "free",
|
|
1505
|
+
name: "Free",
|
|
1506
|
+
monthly: 0,
|
|
1507
|
+
blurb: "Try the connector with a single store.",
|
|
1508
|
+
features: ["One sync direction", "Manual sync only", "Up to 100 products", "Community support"]
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
id: "starter",
|
|
1512
|
+
name: "Starter",
|
|
1513
|
+
monthly: 19,
|
|
1514
|
+
blurb: "For small stores getting started with Odoo.",
|
|
1515
|
+
features: ["Both sync directions", "Daily scheduled sync", "Up to 1,000 products", "Order sync", "Email support"]
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
id: "pro",
|
|
1519
|
+
name: "Pro",
|
|
1520
|
+
monthly: 49,
|
|
1521
|
+
blurb: "For growing stores that need automation.",
|
|
1522
|
+
popular: true,
|
|
1523
|
+
features: ["Everything in Starter", "Hourly scheduled sync", "Up to 10,000 products", "Inventory & customer sync", "Priority support"]
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
id: "enterprise",
|
|
1527
|
+
name: "Enterprise",
|
|
1528
|
+
monthly: null,
|
|
1529
|
+
blurb: "For high-volume and multi-warehouse operations.",
|
|
1530
|
+
features: ["Unlimited products", "Real-time sync", "Multi-warehouse mapping", "Dedicated support & SLA", "Custom onboarding"]
|
|
1531
|
+
}
|
|
1532
|
+
];
|
|
1533
|
+
const priceLabel = (plan, cycle) => {
|
|
1534
|
+
if (plan.monthly === null) return "Custom";
|
|
1535
|
+
if (plan.monthly === 0) return "$0";
|
|
1536
|
+
const perMonth = cycle === "yearly" ? Math.round(plan.monthly * 10 / 12) : plan.monthly;
|
|
1537
|
+
return `$${perMonth}`;
|
|
1538
|
+
};
|
|
1539
|
+
const PlansView = () => {
|
|
1540
|
+
const [loading, setLoading] = useState(true);
|
|
1541
|
+
const [busy, setBusy] = useState(null);
|
|
1542
|
+
const [billing, setBilling] = useState({
|
|
1543
|
+
stripeConfigured: false,
|
|
1544
|
+
planId: "free",
|
|
1545
|
+
status: "inactive",
|
|
1546
|
+
cycle: "monthly"
|
|
1547
|
+
});
|
|
1548
|
+
const [cycle, setCycle] = useState("monthly");
|
|
1549
|
+
const load = async () => {
|
|
1550
|
+
try {
|
|
1551
|
+
const b = await api.get("/odoo/billing");
|
|
1552
|
+
setBilling(b);
|
|
1553
|
+
if (b.cycle) setCycle(b.cycle);
|
|
1554
|
+
} catch {
|
|
1555
|
+
} finally {
|
|
1556
|
+
setLoading(false);
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
useEffect(() => {
|
|
1560
|
+
const params = new URLSearchParams(window.location.search);
|
|
1561
|
+
const r = params.get("billing");
|
|
1562
|
+
if (r === "success") toast.success("Subscription updated — welcome aboard!");
|
|
1563
|
+
if (r === "cancelled") toast.info("Checkout cancelled");
|
|
1564
|
+
if (r) window.history.replaceState({}, "", window.location.pathname);
|
|
1565
|
+
load();
|
|
1566
|
+
}, []);
|
|
1567
|
+
const activePlan = billing.planId || "free";
|
|
1568
|
+
const onPaid = activePlan !== "free";
|
|
1569
|
+
const manageBilling = async () => {
|
|
1570
|
+
setBusy("portal");
|
|
1571
|
+
try {
|
|
1572
|
+
const res = await api.post("/odoo/stripe/portal");
|
|
1573
|
+
if (res.url) window.location.href = res.url;
|
|
1574
|
+
else toast.error(res.error || "Could not open billing portal");
|
|
1575
|
+
} catch (e) {
|
|
1576
|
+
toast.error("Could not open billing portal", { description: e.message });
|
|
1577
|
+
} finally {
|
|
1578
|
+
setBusy(null);
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
const choose = async (plan) => {
|
|
1582
|
+
if (plan.id === activePlan) return;
|
|
1583
|
+
if (plan.monthly === null) {
|
|
1584
|
+
window.open("mailto:sales@odooconnector.cloud?subject=Enterprise%20plan%20enquiry", "_blank");
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (plan.id === "free") {
|
|
1588
|
+
if (onPaid) return manageBilling();
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (!billing.stripeConfigured) {
|
|
1592
|
+
toast.error("Stripe is not configured", {
|
|
1593
|
+
description: "Set STRIPE_SECRET_KEY and the plan price IDs on the odoo-api service."
|
|
1594
|
+
});
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
setBusy(plan.id);
|
|
1598
|
+
try {
|
|
1599
|
+
const res = await api.post(
|
|
1600
|
+
"/odoo/stripe/change",
|
|
1601
|
+
{ planId: plan.id, cycle }
|
|
1602
|
+
);
|
|
1603
|
+
if (res.url) {
|
|
1604
|
+
window.location.href = res.url;
|
|
1605
|
+
} else if (res.updated) {
|
|
1606
|
+
toast.success(`Switched to the ${plan.name} plan`, { description: "Prorated on your next invoice." });
|
|
1607
|
+
await load();
|
|
1608
|
+
} else {
|
|
1609
|
+
toast.error(res.error || "Could not change plan");
|
|
1610
|
+
}
|
|
1611
|
+
} catch (e) {
|
|
1612
|
+
toast.error("Could not change plan", { description: e.message });
|
|
1613
|
+
} finally {
|
|
1614
|
+
setBusy(null);
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
const statusBadge = () => {
|
|
1618
|
+
if (!onPaid) return null;
|
|
1619
|
+
const map = {
|
|
1620
|
+
active: { color: "green", label: "Active" },
|
|
1621
|
+
trialing: { color: "green", label: "Trial" },
|
|
1622
|
+
past_due: { color: "orange", label: "Past due" },
|
|
1623
|
+
canceled: { color: "red", label: "Canceled" }
|
|
1624
|
+
};
|
|
1625
|
+
const b = map[billing.status] ?? { color: "green", label: billing.status };
|
|
1626
|
+
return /* @__PURE__ */ jsx(Badge, { size: "small", color: b.color, children: b.label });
|
|
1627
|
+
};
|
|
1628
|
+
return /* @__PURE__ */ jsx(
|
|
1629
|
+
OdooShell,
|
|
1630
|
+
{
|
|
1631
|
+
current: "plans",
|
|
1632
|
+
title: "Plans",
|
|
1633
|
+
actions: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-3", children: [
|
|
1634
|
+
onPaid && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: manageBilling, isLoading: busy === "portal", children: "Manage billing" }),
|
|
1635
|
+
/* @__PURE__ */ jsx("div", { className: "inline-flex items-center rounded-full border border-ui-border-base p-0.5", children: ["monthly", "yearly"].map((c) => /* @__PURE__ */ jsxs(
|
|
1636
|
+
"button",
|
|
1637
|
+
{
|
|
1638
|
+
type: "button",
|
|
1639
|
+
onClick: () => setCycle(c),
|
|
1640
|
+
className: `px-3 py-1 text-xs rounded-full capitalize transition-colors ${cycle === c ? "bg-ui-bg-base shadow-borders-base text-ui-fg-base" : "text-ui-fg-subtle"}`,
|
|
1641
|
+
children: [
|
|
1642
|
+
c,
|
|
1643
|
+
c === "yearly" && " (2 months free)"
|
|
1644
|
+
]
|
|
1645
|
+
},
|
|
1646
|
+
c
|
|
1647
|
+
)) })
|
|
1648
|
+
] }),
|
|
1649
|
+
children: loading ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading plans…" }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-4", children: [
|
|
1650
|
+
!billing.stripeConfigured && /* @__PURE__ */ jsx("div", { className: "bg-ui-bg-subtle border border-ui-border-base rounded-lg px-4 py-3", children: /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
1651
|
+
"Stripe isn't configured yet, so paid plans can't be purchased. Set",
|
|
1652
|
+
" ",
|
|
1653
|
+
/* @__PURE__ */ jsx("code", { children: "STRIPE_SECRET_KEY" }),
|
|
1654
|
+
", ",
|
|
1655
|
+
/* @__PURE__ */ jsx("code", { children: "STRIPE_WEBHOOK_SECRET" }),
|
|
1656
|
+
" and the plan price IDs on the odoo-api service, then point a Stripe webhook at",
|
|
1657
|
+
" ",
|
|
1658
|
+
/* @__PURE__ */ jsx("code", { children: "/stripe/webhook" }),
|
|
1659
|
+
"."
|
|
1660
|
+
] }) }),
|
|
1661
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4", children: PLANS.map((plan) => {
|
|
1662
|
+
const isActive = plan.id === activePlan;
|
|
1663
|
+
return /* @__PURE__ */ jsxs(
|
|
1664
|
+
"div",
|
|
1665
|
+
{
|
|
1666
|
+
className: `relative flex flex-col gap-y-4 rounded-lg border p-5 ${isActive ? "border-ui-fg-base shadow-elevation-card-rest" : "border-ui-border-base"}`,
|
|
1667
|
+
children: [
|
|
1668
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1669
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: plan.name }),
|
|
1670
|
+
isActive ? statusBadge() ?? /* @__PURE__ */ jsx(Badge, { size: "small", color: "green", children: "Current plan" }) : plan.popular ? /* @__PURE__ */ jsx(Badge, { size: "small", color: "purple", children: "Most popular" }) : null
|
|
1671
|
+
] }),
|
|
1672
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-x-1", children: [
|
|
1673
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: priceLabel(plan, cycle) }),
|
|
1674
|
+
plan.monthly !== null && plan.monthly > 0 && /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
1675
|
+
"/mo",
|
|
1676
|
+
cycle === "yearly" ? ", billed yearly" : ""
|
|
1677
|
+
] })
|
|
1678
|
+
] }),
|
|
1679
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: plan.blurb }),
|
|
1680
|
+
/* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-y-2 flex-1", children: plan.features.map((f) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-x-2", children: [
|
|
1681
|
+
/* @__PURE__ */ jsx("span", { className: "text-ui-tag-green-icon mt-0.5", children: /* @__PURE__ */ jsx(CheckCircleSolid, {}) }),
|
|
1682
|
+
/* @__PURE__ */ jsx(Text, { size: "small", children: f })
|
|
1683
|
+
] }, f)) }),
|
|
1684
|
+
/* @__PURE__ */ jsx(
|
|
1685
|
+
Button,
|
|
1686
|
+
{
|
|
1687
|
+
variant: isActive ? "secondary" : plan.popular ? "primary" : "secondary",
|
|
1688
|
+
disabled: isActive && !onPaid,
|
|
1689
|
+
isLoading: busy === plan.id,
|
|
1690
|
+
onClick: () => choose(plan),
|
|
1691
|
+
className: "w-full",
|
|
1692
|
+
children: isActive ? onPaid ? "Manage billing" : "Current plan" : plan.monthly === null ? "Contact sales" : plan.id === "free" ? "Downgrade" : "Choose plan"
|
|
1693
|
+
}
|
|
1694
|
+
)
|
|
1695
|
+
]
|
|
1696
|
+
},
|
|
1697
|
+
plan.id
|
|
1698
|
+
);
|
|
1699
|
+
}) })
|
|
1700
|
+
] })
|
|
1701
|
+
}
|
|
1702
|
+
);
|
|
1703
|
+
};
|
|
1704
|
+
const OdooPlansPage = () => /* @__PURE__ */ jsx(PlansView, {});
|
|
1705
|
+
const config$2 = defineRouteConfig({
|
|
1706
|
+
label: "Plans",
|
|
1707
|
+
icon: CreditCard,
|
|
1708
|
+
rank: 8
|
|
1709
|
+
});
|
|
1710
|
+
const DEFAULTS = {
|
|
1711
|
+
direction: "odoo_to_medusa",
|
|
1712
|
+
creationMode: "create_update",
|
|
1713
|
+
autoPublish: true,
|
|
1714
|
+
syncQuantity: true,
|
|
1715
|
+
syncImages: true,
|
|
1716
|
+
createDraft: false,
|
|
1717
|
+
updateFields: ["price", "title"],
|
|
1718
|
+
odooCategoryIds: [],
|
|
1719
|
+
pricelistId: null,
|
|
1720
|
+
medusaCollectionIds: [],
|
|
1721
|
+
importSchedule: "off"
|
|
1722
|
+
};
|
|
1723
|
+
const UPDATE_FIELDS = [
|
|
1724
|
+
{ value: "price", label: "Price" },
|
|
1725
|
+
{ value: "title", label: "Title" },
|
|
1726
|
+
{ value: "description", label: "Description" },
|
|
1727
|
+
{ value: "tags", label: "Tags" },
|
|
1728
|
+
{ value: "cost", label: "Cost" }
|
|
1729
|
+
];
|
|
1730
|
+
const ProductSyncForm = ({ onSaved }) => {
|
|
1731
|
+
const [loading, setLoading] = useState(true);
|
|
1732
|
+
const [optionsLoading, setOptionsLoading] = useState(true);
|
|
1733
|
+
const [saving, setSaving] = useState(false);
|
|
1734
|
+
const [running, setRunning] = useState(false);
|
|
1735
|
+
const [clearing, setClearing] = useState(false);
|
|
1736
|
+
const [s, setS] = useState(DEFAULTS);
|
|
1737
|
+
const [options, setOptions] = useState({
|
|
1738
|
+
connected: false,
|
|
1739
|
+
categories: [],
|
|
1740
|
+
pricelists: []
|
|
1741
|
+
});
|
|
1742
|
+
const [collections, setCollections] = useState([]);
|
|
1743
|
+
const set = (k, v) => setS((prev) => ({ ...prev, [k]: v }));
|
|
1744
|
+
useEffect(() => {
|
|
1745
|
+
let active = true;
|
|
1746
|
+
api.get("/odoo/sections/product").then((saved) => {
|
|
1747
|
+
if (active && saved.data) setS({ ...DEFAULTS, ...saved.data });
|
|
1748
|
+
}).catch((e) => toast.error("Failed to load", { description: e.message })).finally(() => active && setLoading(false));
|
|
1749
|
+
Promise.all([
|
|
1750
|
+
api.get("/odoo/product-options"),
|
|
1751
|
+
medusaApi.get("/admin/collections?limit=100").catch(() => ({ collections: [] }))
|
|
1752
|
+
]).then(([opts, cols]) => {
|
|
1753
|
+
if (!active) return;
|
|
1754
|
+
setOptions(opts);
|
|
1755
|
+
setCollections(cols.collections ?? []);
|
|
1756
|
+
}).catch(() => {
|
|
1757
|
+
}).finally(() => active && setOptionsLoading(false));
|
|
1758
|
+
return () => {
|
|
1759
|
+
active = false;
|
|
1760
|
+
};
|
|
1761
|
+
}, []);
|
|
1762
|
+
const toggleField = (f) => set(
|
|
1763
|
+
"updateFields",
|
|
1764
|
+
s.updateFields.includes(f) ? s.updateFields.filter((x) => x !== f) : [...s.updateFields, f]
|
|
1765
|
+
);
|
|
1766
|
+
const toggleCategory = (id) => {
|
|
1767
|
+
const n = Number(id);
|
|
1768
|
+
set(
|
|
1769
|
+
"odooCategoryIds",
|
|
1770
|
+
s.odooCategoryIds.includes(n) ? s.odooCategoryIds.filter((x) => x !== n) : [...s.odooCategoryIds, n]
|
|
1771
|
+
);
|
|
1772
|
+
};
|
|
1773
|
+
const toggleCollection = (id) => set(
|
|
1774
|
+
"medusaCollectionIds",
|
|
1775
|
+
s.medusaCollectionIds.includes(id) ? s.medusaCollectionIds.filter((x) => x !== id) : [...s.medusaCollectionIds, id]
|
|
1776
|
+
);
|
|
1777
|
+
const handleSave = async () => {
|
|
1778
|
+
setSaving(true);
|
|
1779
|
+
try {
|
|
1780
|
+
await api.post("/odoo/sections/product", s);
|
|
1781
|
+
toast.success("Product sync settings saved");
|
|
1782
|
+
onSaved == null ? void 0 : onSaved();
|
|
1783
|
+
} catch (e) {
|
|
1784
|
+
toast.error("Failed to save", { description: e.message });
|
|
1785
|
+
} finally {
|
|
1786
|
+
setSaving(false);
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
const runSync = async (label) => {
|
|
1790
|
+
setRunning(true);
|
|
1791
|
+
try {
|
|
1792
|
+
const { jobId } = await api.post("/odoo/sync/products");
|
|
1793
|
+
toast.info(`${label} queued`, { description: "Running in the background…" });
|
|
1794
|
+
const poll = async () => {
|
|
1795
|
+
var _a;
|
|
1796
|
+
try {
|
|
1797
|
+
const st = await api.get(
|
|
1798
|
+
`/odoo/jobs/${jobId}`
|
|
1799
|
+
);
|
|
1800
|
+
if (st.state === "completed") {
|
|
1801
|
+
toast.success("Import complete", { description: ((_a = st.result) == null ? void 0 : _a.message) ?? "Done." });
|
|
1802
|
+
setRunning(false);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (st.state === "failed") {
|
|
1806
|
+
toast.error("Import failed", { description: st.failedReason ?? "Unknown error" });
|
|
1807
|
+
setRunning(false);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
setTimeout(poll, 1500);
|
|
1811
|
+
} catch {
|
|
1812
|
+
setRunning(false);
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
setTimeout(poll, 1500);
|
|
1816
|
+
} catch (e) {
|
|
1817
|
+
toast.error("Sync failed", { description: e.message });
|
|
1818
|
+
setRunning(false);
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
const clearImported = async () => {
|
|
1822
|
+
if (!window.confirm(
|
|
1823
|
+
"Remove the Odoo link from imported products? The products are kept — they just won't be matched on the next import (a re-import would create fresh copies)."
|
|
1824
|
+
))
|
|
1825
|
+
return;
|
|
1826
|
+
setClearing(true);
|
|
1827
|
+
try {
|
|
1828
|
+
const r = await api.post("/odoo/clear-products");
|
|
1829
|
+
toast.success("Cleared Odoo links", {
|
|
1830
|
+
description: `${r.cleared} product(s) unlinked. Products kept.`
|
|
1831
|
+
});
|
|
1832
|
+
} catch (e) {
|
|
1833
|
+
toast.error("Failed to clear", { description: e.message });
|
|
1834
|
+
} finally {
|
|
1835
|
+
setClearing(false);
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
const odooToMedusa = s.direction === "odoo_to_medusa";
|
|
1839
|
+
return /* @__PURE__ */ jsx(
|
|
1840
|
+
OdooShell,
|
|
1841
|
+
{
|
|
1842
|
+
current: "products",
|
|
1843
|
+
title: "Products",
|
|
1844
|
+
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1845
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "teal", icon: ArrowPath, onClick: () => runSync(odooToMedusa ? "Import started" : "Export started"), loading: running, children: odooToMedusa ? "Import now" : "Export now" }),
|
|
1846
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "secondary", onClick: clearImported, loading: clearing, children: "Clear links" }),
|
|
1847
|
+
/* @__PURE__ */ jsx(OdooButton, { variant: "primary", onClick: handleSave, loading: saving, children: "Save" })
|
|
1848
|
+
] }),
|
|
1849
|
+
children: /* @__PURE__ */ jsx(OdooSheet, { children: loading ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading product sync settings…" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1850
|
+
/* @__PURE__ */ jsxs(
|
|
1851
|
+
OdooSection,
|
|
1852
|
+
{
|
|
1853
|
+
icon: CubeSolid,
|
|
1854
|
+
title: "Product synchronization",
|
|
1855
|
+
sub: odooToMedusa ? "Products are pulled from Odoo into Medusa." : "Products are pushed from Medusa into Odoo.",
|
|
1856
|
+
children: [
|
|
1857
|
+
/* @__PURE__ */ jsx(
|
|
1858
|
+
SelectField,
|
|
1859
|
+
{
|
|
1860
|
+
label: "Sync direction",
|
|
1861
|
+
value: s.direction,
|
|
1862
|
+
onChange: (v) => set("direction", v),
|
|
1863
|
+
options: [
|
|
1864
|
+
{ value: "odoo_to_medusa", label: "Odoo → Medusa (import)" },
|
|
1865
|
+
{ value: "medusa_to_odoo", label: "Medusa → Odoo (export)" }
|
|
1866
|
+
]
|
|
1867
|
+
}
|
|
1868
|
+
),
|
|
1869
|
+
/* @__PURE__ */ jsx(
|
|
1870
|
+
SelectField,
|
|
1871
|
+
{
|
|
1872
|
+
label: "Product creation mode",
|
|
1873
|
+
value: s.creationMode,
|
|
1874
|
+
onChange: (v) => set("creationMode", v),
|
|
1875
|
+
options: [
|
|
1876
|
+
{ value: "create", label: "Create new only" },
|
|
1877
|
+
{ value: "update", label: "Update existing only" },
|
|
1878
|
+
{ value: "create_update", label: "Create & update" }
|
|
1879
|
+
]
|
|
1880
|
+
}
|
|
1881
|
+
)
|
|
1882
|
+
]
|
|
1883
|
+
}
|
|
1884
|
+
),
|
|
1885
|
+
/* @__PURE__ */ jsxs(Section, { title: "Options", children: [
|
|
1886
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Auto-publish", hint: "Publish products immediately after import.", checked: s.autoPublish, onChange: (v) => set("autoPublish", v) }),
|
|
1887
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Sync quantity", hint: "Keep stock quantities in sync.", checked: s.syncQuantity, onChange: (v) => set("syncQuantity", v) }),
|
|
1888
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Sync images", hint: "Import product images.", checked: s.syncImages, onChange: (v) => set("syncImages", v) }),
|
|
1889
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: "Create as draft", hint: "Imported products start as drafts.", checked: s.createDraft, onChange: (v) => set("createDraft", v) })
|
|
1890
|
+
] }),
|
|
1891
|
+
/* @__PURE__ */ jsxs(Section, { title: "Fields to update", children: [
|
|
1892
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "When updating existing products, only these fields are overwritten." }),
|
|
1893
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: UPDATE_FIELDS.map((f) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-x-2 cursor-pointer", children: [
|
|
1894
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: s.updateFields.includes(f.value), onCheckedChange: () => toggleField(f.value) }),
|
|
1895
|
+
/* @__PURE__ */ jsx(Text, { size: "small", children: f.label })
|
|
1896
|
+
] }, f.value)) })
|
|
1897
|
+
] }),
|
|
1898
|
+
/* @__PURE__ */ jsxs(Section, { title: "Odoo categories", children: [
|
|
1899
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Limit sync to these Odoo product categories (none = all)." }),
|
|
1900
|
+
/* @__PURE__ */ jsx(
|
|
1901
|
+
CheckList,
|
|
1902
|
+
{
|
|
1903
|
+
items: options.categories.map((c) => ({ id: String(c.id), label: c.name })),
|
|
1904
|
+
selected: new Set(s.odooCategoryIds.map(String)),
|
|
1905
|
+
onToggle: toggleCategory,
|
|
1906
|
+
empty: optionsLoading ? "Loading from Odoo…" : options.connected ? "No categories found in Odoo." : "Connect to Odoo to load categories."
|
|
1907
|
+
}
|
|
1908
|
+
)
|
|
1909
|
+
] }),
|
|
1910
|
+
/* @__PURE__ */ jsxs(Section, { title: "Medusa collections", children: [
|
|
1911
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Assign imported products to these Medusa collections." }),
|
|
1912
|
+
/* @__PURE__ */ jsx(
|
|
1913
|
+
CheckList,
|
|
1914
|
+
{
|
|
1915
|
+
items: collections.map((c) => ({ id: c.id, label: c.title })),
|
|
1916
|
+
selected: new Set(s.medusaCollectionIds),
|
|
1917
|
+
onToggle: toggleCollection,
|
|
1918
|
+
empty: "No collections found in Medusa."
|
|
1919
|
+
}
|
|
1920
|
+
)
|
|
1921
|
+
] }),
|
|
1922
|
+
odooToMedusa && /* @__PURE__ */ jsx(Section, { title: "Price list", children: /* @__PURE__ */ jsx(
|
|
1923
|
+
SelectField,
|
|
1924
|
+
{
|
|
1925
|
+
label: "Odoo pricelist",
|
|
1926
|
+
help: "Odoo pricelist used for product prices.",
|
|
1927
|
+
value: s.pricelistId != null ? String(s.pricelistId) : "",
|
|
1928
|
+
onChange: (v) => set("pricelistId", v ? Number(v) : null),
|
|
1929
|
+
options: options.pricelists.map((p) => ({ value: String(p.id), label: p.name })),
|
|
1930
|
+
placeholder: options.pricelists.length ? "Select price list" : "No pricelists — connect to Odoo",
|
|
1931
|
+
disabled: !options.pricelists.length
|
|
1932
|
+
}
|
|
1933
|
+
) }),
|
|
1934
|
+
/* @__PURE__ */ jsx(Section, { title: "Schedule", children: /* @__PURE__ */ jsx(
|
|
1935
|
+
SelectField,
|
|
1936
|
+
{
|
|
1937
|
+
label: "Automatic import",
|
|
1938
|
+
help: "Runs the import automatically in the background on this schedule.",
|
|
1939
|
+
value: s.importSchedule,
|
|
1940
|
+
onChange: (v) => set("importSchedule", v),
|
|
1941
|
+
options: [
|
|
1942
|
+
{ value: "off", label: "Off (manual only)" },
|
|
1943
|
+
{ value: "30min", label: "Every 30 minutes" },
|
|
1944
|
+
{ value: "hourly", label: "Hourly" },
|
|
1945
|
+
{ value: "twice_daily", label: "Twice a day (00:00 & 12:00)" },
|
|
1946
|
+
{ value: "daily", label: "Daily (00:00)" }
|
|
1947
|
+
]
|
|
1948
|
+
}
|
|
1949
|
+
) })
|
|
1950
|
+
] }) })
|
|
1951
|
+
}
|
|
1952
|
+
);
|
|
1953
|
+
};
|
|
1954
|
+
const OdooProductsPage = () => /* @__PURE__ */ jsx(ProductSyncForm, {});
|
|
1955
|
+
const config$1 = defineRouteConfig({
|
|
1956
|
+
label: "Product Sync",
|
|
1957
|
+
icon: CubeSolid,
|
|
1958
|
+
rank: 2
|
|
1959
|
+
});
|
|
1960
|
+
const SYNCS = [
|
|
1961
|
+
{
|
|
1962
|
+
entity: "products",
|
|
1963
|
+
title: "Sync Products",
|
|
1964
|
+
description: "Pull the product catalog from Odoo into Medusa.",
|
|
1965
|
+
icon: CubeSolid
|
|
1966
|
+
},
|
|
1967
|
+
{
|
|
1968
|
+
entity: "orders",
|
|
1969
|
+
title: "Sync Orders",
|
|
1970
|
+
description: "Push Medusa orders to Odoo as sale orders.",
|
|
1971
|
+
icon: ShoppingCart
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
entity: "inventory",
|
|
1975
|
+
title: "Sync Inventory",
|
|
1976
|
+
description: "Reconcile stock levels between Odoo and Medusa.",
|
|
1977
|
+
icon: BuildingStorefront
|
|
1978
|
+
}
|
|
1979
|
+
];
|
|
1980
|
+
const pillColor = (s) => s === "error" ? "red" : s === "done" ? "green" : "purple";
|
|
1981
|
+
const pillLabel = (s) => s === "queued" ? "Queued" : s === "running" ? "Running" : s === "done" ? "Done" : "Error";
|
|
1982
|
+
const SyncButtons = () => {
|
|
1983
|
+
const [running, setRunning] = useState(null);
|
|
1984
|
+
const [results, setResults] = useState({});
|
|
1985
|
+
const set = (entity, r) => setResults((prev) => ({ ...prev, [entity]: r }));
|
|
1986
|
+
const pollJob = (entity, jobId) => {
|
|
1987
|
+
const tick = async () => {
|
|
1988
|
+
var _a, _b;
|
|
1989
|
+
try {
|
|
1990
|
+
const st = await api.get(
|
|
1991
|
+
`/odoo/jobs/${jobId}`
|
|
1992
|
+
);
|
|
1993
|
+
if (st.state === "completed") {
|
|
1994
|
+
set(entity, { status: "done", message: (_a = st.result) == null ? void 0 : _a.message });
|
|
1995
|
+
toast.success("Sync complete", { description: (_b = st.result) == null ? void 0 : _b.message });
|
|
1996
|
+
setRunning(null);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (st.state === "failed") {
|
|
2000
|
+
set(entity, { status: "error", message: st.failedReason });
|
|
2001
|
+
toast.error("Sync failed", { description: st.failedReason });
|
|
2002
|
+
setRunning(null);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
set(entity, { status: "running" });
|
|
2006
|
+
setTimeout(tick, 1500);
|
|
2007
|
+
} catch (e) {
|
|
2008
|
+
set(entity, { status: "error", message: e.message });
|
|
2009
|
+
setRunning(null);
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
setTimeout(tick, 1500);
|
|
2013
|
+
};
|
|
2014
|
+
const trigger = async (entity) => {
|
|
2015
|
+
setRunning(entity);
|
|
2016
|
+
set(entity, { status: "queued", message: "Queued…" });
|
|
2017
|
+
try {
|
|
2018
|
+
const res = await api.post(`/odoo/sync/${entity}`);
|
|
2019
|
+
if (res == null ? void 0 : res.jobId) {
|
|
2020
|
+
toast.info("Sync queued", { description: "Running in the background…" });
|
|
2021
|
+
pollJob(entity, res.jobId);
|
|
2022
|
+
} else {
|
|
2023
|
+
set(entity, { status: (res == null ? void 0 : res.ok) === false ? "error" : "done", message: res == null ? void 0 : res.message });
|
|
2024
|
+
toast.success("Sync queued", { description: res == null ? void 0 : res.message });
|
|
2025
|
+
setRunning(null);
|
|
2026
|
+
}
|
|
2027
|
+
} catch (e) {
|
|
2028
|
+
set(entity, { status: "error", message: e.message });
|
|
2029
|
+
toast.error("Sync failed", { description: e.message });
|
|
2030
|
+
setRunning(null);
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
return /* @__PURE__ */ jsx(OdooShell, { current: "sync", title: "Sync", children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4", children: SYNCS.map(({ entity, title, description, icon: Icon }) => {
|
|
2034
|
+
const result = results[entity];
|
|
2035
|
+
return /* @__PURE__ */ jsxs(
|
|
2036
|
+
"div",
|
|
2037
|
+
{
|
|
2038
|
+
className: "border border-ui-border-base rounded-lg p-4 flex flex-col gap-y-3",
|
|
2039
|
+
children: [
|
|
2040
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
2041
|
+
/* @__PURE__ */ jsx("span", { className: "text-ui-fg-subtle", children: /* @__PURE__ */ jsx(Icon, {}) }),
|
|
2042
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: title })
|
|
2043
|
+
] }),
|
|
2044
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle flex-1", children: description }),
|
|
2045
|
+
result && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
2046
|
+
/* @__PURE__ */ jsx(OdooPill, { color: pillColor(result.status), children: pillLabel(result.status) }),
|
|
2047
|
+
result.message && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle truncate", children: result.message })
|
|
2048
|
+
] }),
|
|
2049
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
|
|
2050
|
+
OdooButton,
|
|
2051
|
+
{
|
|
2052
|
+
variant: "primary",
|
|
2053
|
+
onClick: () => trigger(entity),
|
|
2054
|
+
loading: running === entity,
|
|
2055
|
+
disabled: running !== null,
|
|
2056
|
+
children: title
|
|
2057
|
+
}
|
|
2058
|
+
) })
|
|
2059
|
+
]
|
|
2060
|
+
},
|
|
2061
|
+
entity
|
|
2062
|
+
);
|
|
2063
|
+
}) }) });
|
|
2064
|
+
};
|
|
2065
|
+
const OdooSyncPage = () => /* @__PURE__ */ jsx(SyncButtons, {});
|
|
2066
|
+
const config = defineRouteConfig({
|
|
2067
|
+
label: "Sync",
|
|
2068
|
+
icon: ArrowPath,
|
|
2069
|
+
rank: 6
|
|
2070
|
+
});
|
|
2071
|
+
const widgetModule = { widgets: [
|
|
2072
|
+
{
|
|
2073
|
+
Component: OrderOdooWidget,
|
|
2074
|
+
zone: ["order.details.side.after"]
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
Component: ProductOdooWidget,
|
|
2078
|
+
zone: ["product.details.side.after"]
|
|
2079
|
+
}
|
|
2080
|
+
] };
|
|
2081
|
+
const routeModule = {
|
|
2082
|
+
routes: [
|
|
2083
|
+
{
|
|
2084
|
+
Component: OdooSetupGuide,
|
|
2085
|
+
path: "/odoo"
|
|
2086
|
+
},
|
|
2087
|
+
{
|
|
2088
|
+
Component: OdooInventoryPage,
|
|
2089
|
+
path: "/odoo/inventory"
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
Component: OdooLogsPage,
|
|
2093
|
+
path: "/odoo/logs"
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
Component: OdooDashboardPage,
|
|
2097
|
+
path: "/odoo/dashboard"
|
|
2098
|
+
},
|
|
2099
|
+
{
|
|
2100
|
+
Component: OdooCustomersPage,
|
|
2101
|
+
path: "/odoo/customers"
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
Component: OdooOrdersPage,
|
|
2105
|
+
path: "/odoo/orders"
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
Component: OdooPlansPage,
|
|
2109
|
+
path: "/odoo/plans"
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
Component: OdooProductsPage,
|
|
2113
|
+
path: "/odoo/products"
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
Component: OdooSyncPage,
|
|
2117
|
+
path: "/odoo/sync"
|
|
2118
|
+
}
|
|
2119
|
+
]
|
|
2120
|
+
};
|
|
2121
|
+
const menuItemModule = {
|
|
2122
|
+
menuItems: [
|
|
2123
|
+
{
|
|
2124
|
+
label: config$8.label,
|
|
2125
|
+
icon: config$8.icon,
|
|
2126
|
+
path: "/odoo",
|
|
2127
|
+
nested: void 0,
|
|
2128
|
+
rank: void 0,
|
|
2129
|
+
translationNs: void 0
|
|
2130
|
+
},
|
|
2131
|
+
{
|
|
2132
|
+
label: config$4.label,
|
|
2133
|
+
icon: config$4.icon,
|
|
2134
|
+
path: "/odoo/customers",
|
|
2135
|
+
nested: void 0,
|
|
2136
|
+
rank: 5,
|
|
2137
|
+
translationNs: void 0
|
|
2138
|
+
},
|
|
2139
|
+
{
|
|
2140
|
+
label: config$5.label,
|
|
2141
|
+
icon: config$5.icon,
|
|
2142
|
+
path: "/odoo/dashboard",
|
|
2143
|
+
nested: void 0,
|
|
2144
|
+
rank: 1,
|
|
2145
|
+
translationNs: void 0
|
|
2146
|
+
},
|
|
2147
|
+
{
|
|
2148
|
+
label: config$7.label,
|
|
2149
|
+
icon: config$7.icon,
|
|
2150
|
+
path: "/odoo/inventory",
|
|
2151
|
+
nested: void 0,
|
|
2152
|
+
rank: 3,
|
|
2153
|
+
translationNs: void 0
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
label: config$6.label,
|
|
2157
|
+
icon: config$6.icon,
|
|
2158
|
+
path: "/odoo/logs",
|
|
2159
|
+
nested: void 0,
|
|
2160
|
+
rank: 7,
|
|
2161
|
+
translationNs: void 0
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
label: config$2.label,
|
|
2165
|
+
icon: config$2.icon,
|
|
2166
|
+
path: "/odoo/plans",
|
|
2167
|
+
nested: void 0,
|
|
2168
|
+
rank: 8,
|
|
2169
|
+
translationNs: void 0
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
label: config$3.label,
|
|
2173
|
+
icon: config$3.icon,
|
|
2174
|
+
path: "/odoo/orders",
|
|
2175
|
+
nested: void 0,
|
|
2176
|
+
rank: 4,
|
|
2177
|
+
translationNs: void 0
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
label: config$1.label,
|
|
2181
|
+
icon: config$1.icon,
|
|
2182
|
+
path: "/odoo/products",
|
|
2183
|
+
nested: void 0,
|
|
2184
|
+
rank: 2,
|
|
2185
|
+
translationNs: void 0
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
label: config.label,
|
|
2189
|
+
icon: config.icon,
|
|
2190
|
+
path: "/odoo/sync",
|
|
2191
|
+
nested: void 0,
|
|
2192
|
+
rank: 6,
|
|
2193
|
+
translationNs: void 0
|
|
2194
|
+
}
|
|
2195
|
+
]
|
|
2196
|
+
};
|
|
2197
|
+
const formModule = { customFields: {} };
|
|
2198
|
+
const displayModule = {
|
|
2199
|
+
displays: {}
|
|
2200
|
+
};
|
|
2201
|
+
const i18nModule = { resources: {} };
|
|
2202
|
+
const plugin = {
|
|
2203
|
+
widgetModule,
|
|
2204
|
+
routeModule,
|
|
2205
|
+
menuItemModule,
|
|
2206
|
+
formModule,
|
|
2207
|
+
displayModule,
|
|
2208
|
+
i18nModule
|
|
2209
|
+
};
|
|
2210
|
+
export {
|
|
2211
|
+
plugin as default
|
|
2212
|
+
};
|