@growflowstudio/billing-admin-ui 1.0.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/LICENSE +21 -0
- package/dist/index.d.mts +329 -0
- package/dist/index.d.ts +329 -0
- package/dist/index.js +2152 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2139 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +86 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2139 @@
|
|
|
1
|
+
// src/provider.tsx
|
|
2
|
+
import React, { useMemo } from "react";
|
|
3
|
+
import {
|
|
4
|
+
BillingAdminClientContext,
|
|
5
|
+
createBillingAdminClient
|
|
6
|
+
} from "@growflowstudio/billing-admin-core";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
var DEFAULT_MODULES = {
|
|
9
|
+
plans: true,
|
|
10
|
+
subscriptions: true,
|
|
11
|
+
products: true,
|
|
12
|
+
payments: true,
|
|
13
|
+
invoices: true,
|
|
14
|
+
features: true
|
|
15
|
+
};
|
|
16
|
+
var BillingAdminConfigContext = React.createContext(null);
|
|
17
|
+
function useBillingAdminConfig() {
|
|
18
|
+
const config = React.useContext(BillingAdminConfigContext);
|
|
19
|
+
if (!config) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"useBillingAdminConfig must be used within a BillingAdminProvider."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
function useEnabledModules() {
|
|
27
|
+
const config = useBillingAdminConfig();
|
|
28
|
+
return { ...DEFAULT_MODULES, ...config.modules };
|
|
29
|
+
}
|
|
30
|
+
function BillingAdminProvider({ config, children }) {
|
|
31
|
+
const client = useMemo(
|
|
32
|
+
() => createBillingAdminClient({
|
|
33
|
+
basePath: config.basePath,
|
|
34
|
+
fetcher: config.fetcher
|
|
35
|
+
}),
|
|
36
|
+
[config.basePath, config.fetcher]
|
|
37
|
+
);
|
|
38
|
+
return /* @__PURE__ */ jsx(BillingAdminConfigContext.Provider, { value: config, children: /* @__PURE__ */ jsx(BillingAdminClientContext.Provider, { value: client, children }) });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/pages/PlansPage.tsx
|
|
42
|
+
import { useState as useState2 } from "react";
|
|
43
|
+
import { toast } from "sonner";
|
|
44
|
+
import { Plus, RefreshCw, LayoutGrid } from "lucide-react";
|
|
45
|
+
import {
|
|
46
|
+
useAdminPlans,
|
|
47
|
+
useCreatePlan,
|
|
48
|
+
useUpdatePlan,
|
|
49
|
+
useDeletePlan,
|
|
50
|
+
useSyncPlanToStripe,
|
|
51
|
+
validateSlug,
|
|
52
|
+
validateRequired
|
|
53
|
+
} from "@growflowstudio/billing-admin-core";
|
|
54
|
+
|
|
55
|
+
// src/components/plans/PlansTable.tsx
|
|
56
|
+
import { Pencil, Trash2, Upload, Check, X } from "lucide-react";
|
|
57
|
+
import { formatCurrency } from "@growflowstudio/billing-admin-core";
|
|
58
|
+
|
|
59
|
+
// src/components/plans/plan-colors.ts
|
|
60
|
+
var PLAN_COLORS = [
|
|
61
|
+
{ value: "gray", label: "Grigio", className: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400" },
|
|
62
|
+
{ value: "green", label: "Verde", className: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" },
|
|
63
|
+
{ value: "blue", label: "Blu", className: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400" },
|
|
64
|
+
{ value: "purple", label: "Viola", className: "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400" },
|
|
65
|
+
{ value: "amber", label: "Ambra", className: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" },
|
|
66
|
+
{ value: "red", label: "Rosso", className: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" },
|
|
67
|
+
{ value: "pink", label: "Rosa", className: "bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400" },
|
|
68
|
+
{ value: "indigo", label: "Indaco", className: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400" }
|
|
69
|
+
];
|
|
70
|
+
function getPlanColorClass(color) {
|
|
71
|
+
const found = PLAN_COLORS.find((c) => c.value === color);
|
|
72
|
+
return found?.className || PLAN_COLORS[0].className;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/primitives/utils.ts
|
|
76
|
+
import { clsx } from "clsx";
|
|
77
|
+
import { twMerge } from "tailwind-merge";
|
|
78
|
+
function cn(...inputs) {
|
|
79
|
+
return twMerge(clsx(inputs));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/components/shared/StatusBadge.tsx
|
|
83
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
84
|
+
function StatusBadge({ label, colorClass, className }) {
|
|
85
|
+
return /* @__PURE__ */ jsx2(
|
|
86
|
+
"span",
|
|
87
|
+
{
|
|
88
|
+
className: cn(
|
|
89
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
|
|
90
|
+
colorClass,
|
|
91
|
+
className
|
|
92
|
+
),
|
|
93
|
+
children: label
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/components/shared/SkeletonRows.tsx
|
|
99
|
+
import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
|
|
100
|
+
function SkeletonRows({ rows = 5, columns = 1 }) {
|
|
101
|
+
return /* @__PURE__ */ jsx3(Fragment, { children: Array.from({ length: rows }).map((_, i) => /* @__PURE__ */ jsx3("tr", { children: /* @__PURE__ */ jsx3("td", { colSpan: columns, className: "p-4", children: /* @__PURE__ */ jsx3("div", { className: "h-10 w-full animate-pulse rounded-md bg-muted" }) }) }, i)) });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/components/plans/PlansTable.tsx
|
|
105
|
+
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
106
|
+
var pricingModelLabels = {
|
|
107
|
+
flat: "Fisso",
|
|
108
|
+
tiered: "A Scaglioni",
|
|
109
|
+
per_unit: "Per Unit\xE0",
|
|
110
|
+
usage_based: "A Consumo"
|
|
111
|
+
};
|
|
112
|
+
function PlansTable({
|
|
113
|
+
plans,
|
|
114
|
+
isLoading,
|
|
115
|
+
onEdit,
|
|
116
|
+
onDelete,
|
|
117
|
+
onSync,
|
|
118
|
+
isSyncing
|
|
119
|
+
}) {
|
|
120
|
+
return /* @__PURE__ */ jsx4("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full caption-bottom text-sm", children: [
|
|
121
|
+
/* @__PURE__ */ jsx4("thead", { className: "[&_tr]:border-b", children: /* @__PURE__ */ jsxs("tr", { className: "border-b transition-colors hover:bg-muted/50", children: [
|
|
122
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Slug" }),
|
|
123
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Nome" }),
|
|
124
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Colore" }),
|
|
125
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Modello" }),
|
|
126
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Prezzo Mensile" }),
|
|
127
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Prezzo Annuale" }),
|
|
128
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Trial" }),
|
|
129
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Attivo" }),
|
|
130
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-left align-middle font-medium text-muted-foreground", children: "Stripe" }),
|
|
131
|
+
/* @__PURE__ */ jsx4("th", { className: "h-12 px-4 text-right align-middle font-medium text-muted-foreground", children: "Azioni" })
|
|
132
|
+
] }) }),
|
|
133
|
+
/* @__PURE__ */ jsx4("tbody", { className: "[&_tr:last-child]:border-0", children: isLoading ? /* @__PURE__ */ jsx4(SkeletonRows, { rows: 3, columns: 10 }) : plans.length === 0 ? /* @__PURE__ */ jsx4("tr", { children: /* @__PURE__ */ jsx4("td", { colSpan: 10, className: "p-4 text-center py-8 text-muted-foreground", children: "Nessun piano configurato" }) }) : plans.map((plan) => /* @__PURE__ */ jsxs("tr", { className: "border-b transition-colors hover:bg-muted/50", children: [
|
|
134
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle", children: /* @__PURE__ */ jsx4("code", { className: "text-sm bg-muted px-2 py-0.5 rounded", children: plan.slug }) }),
|
|
135
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle font-medium", children: plan.name }),
|
|
136
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle", children: /* @__PURE__ */ jsx4(StatusBadge, { label: plan.color, colorClass: getPlanColorClass(plan.color) }) }),
|
|
137
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle", children: /* @__PURE__ */ jsx4(StatusBadge, { label: pricingModelLabels[plan.pricing_model], colorClass: "border" }) }),
|
|
138
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle", children: plan.contact_sales ? /* @__PURE__ */ jsx4("span", { className: "text-muted-foreground", children: "Contattaci" }) : formatCurrency(plan.price_monthly ?? 0, plan.currency) }),
|
|
139
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle", children: plan.contact_sales ? /* @__PURE__ */ jsx4("span", { className: "text-muted-foreground", children: "Contattaci" }) : formatCurrency(plan.price_yearly ?? 0, plan.currency) }),
|
|
140
|
+
/* @__PURE__ */ jsxs("td", { className: "p-4 align-middle", children: [
|
|
141
|
+
plan.trial_days,
|
|
142
|
+
" giorni"
|
|
143
|
+
] }),
|
|
144
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle", children: plan.is_active ? /* @__PURE__ */ jsx4(Check, { className: "w-4 h-4 text-green-600" }) : /* @__PURE__ */ jsx4(X, { className: "w-4 h-4 text-muted-foreground" }) }),
|
|
145
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle", children: plan.stripe_price_id_monthly ? /* @__PURE__ */ jsx4(StatusBadge, { label: "Synced", colorClass: "bg-green-50 text-green-700 border-green-200" }) : /* @__PURE__ */ jsx4(
|
|
146
|
+
"button",
|
|
147
|
+
{
|
|
148
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 w-9 hover:bg-accent hover:text-accent-foreground",
|
|
149
|
+
onClick: () => onSync(plan.id),
|
|
150
|
+
disabled: isSyncing,
|
|
151
|
+
children: /* @__PURE__ */ jsx4(Upload, { className: "w-4 h-4" })
|
|
152
|
+
}
|
|
153
|
+
) }),
|
|
154
|
+
/* @__PURE__ */ jsx4("td", { className: "p-4 align-middle text-right", children: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1", children: [
|
|
155
|
+
/* @__PURE__ */ jsx4(
|
|
156
|
+
"button",
|
|
157
|
+
{
|
|
158
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 w-9 hover:bg-accent hover:text-accent-foreground",
|
|
159
|
+
onClick: () => onEdit(plan),
|
|
160
|
+
children: /* @__PURE__ */ jsx4(Pencil, { className: "w-4 h-4" })
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
/* @__PURE__ */ jsx4(
|
|
164
|
+
"button",
|
|
165
|
+
{
|
|
166
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 w-9 text-destructive hover:bg-accent hover:text-destructive",
|
|
167
|
+
onClick: () => onDelete(plan),
|
|
168
|
+
children: /* @__PURE__ */ jsx4(Trash2, { className: "w-4 h-4" })
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
] }) })
|
|
172
|
+
] }, plan.id)) })
|
|
173
|
+
] }) });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/components/plans/PlanFormDialog.tsx
|
|
177
|
+
import { useState, useEffect } from "react";
|
|
178
|
+
import { useQuery } from "@tanstack/react-query";
|
|
179
|
+
import { Loader2, X as XIcon } from "lucide-react";
|
|
180
|
+
|
|
181
|
+
// src/components/plans/LimitInput.tsx
|
|
182
|
+
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
183
|
+
function LimitInput({
|
|
184
|
+
id,
|
|
185
|
+
label,
|
|
186
|
+
value,
|
|
187
|
+
isUnlimited,
|
|
188
|
+
onValueChange,
|
|
189
|
+
onUnlimitedChange,
|
|
190
|
+
min = 1
|
|
191
|
+
}) {
|
|
192
|
+
return /* @__PURE__ */ jsxs2("div", { className: "space-y-2 p-3 border rounded-lg", children: [
|
|
193
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
|
|
194
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: id, className: "text-sm font-medium leading-none", children: label }),
|
|
195
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
|
|
196
|
+
/* @__PURE__ */ jsx5(
|
|
197
|
+
"button",
|
|
198
|
+
{
|
|
199
|
+
type: "button",
|
|
200
|
+
role: "switch",
|
|
201
|
+
"aria-checked": isUnlimited,
|
|
202
|
+
onClick: () => onUnlimitedChange(!isUnlimited),
|
|
203
|
+
className: `peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors ${isUnlimited ? "bg-primary" : "bg-input"}`,
|
|
204
|
+
children: /* @__PURE__ */ jsx5(
|
|
205
|
+
"span",
|
|
206
|
+
{
|
|
207
|
+
className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform ${isUnlimited ? "translate-x-4" : "translate-x-0"}`
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
),
|
|
212
|
+
/* @__PURE__ */ jsx5("span", { className: "text-xs text-muted-foreground", children: "Illimitati" })
|
|
213
|
+
] })
|
|
214
|
+
] }),
|
|
215
|
+
/* @__PURE__ */ jsx5(
|
|
216
|
+
"input",
|
|
217
|
+
{
|
|
218
|
+
id,
|
|
219
|
+
type: "number",
|
|
220
|
+
min,
|
|
221
|
+
value,
|
|
222
|
+
onChange: (e) => onValueChange(parseInt(e.target.value) || min),
|
|
223
|
+
disabled: isUnlimited,
|
|
224
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
] });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/components/plans/PlanFormDialog.tsx
|
|
231
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
232
|
+
function createDefaultPlanFormData(limitFields = []) {
|
|
233
|
+
const limits = {};
|
|
234
|
+
const limitsUnlimited = {};
|
|
235
|
+
for (const field of limitFields) {
|
|
236
|
+
limits[field.key] = field.defaultValue ?? 1;
|
|
237
|
+
limitsUnlimited[field.key] = false;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
slug: "",
|
|
241
|
+
name: "",
|
|
242
|
+
description: "",
|
|
243
|
+
pricing_model: "flat",
|
|
244
|
+
price_monthly: "",
|
|
245
|
+
price_yearly: "",
|
|
246
|
+
currency: "eur",
|
|
247
|
+
trial_days: 14,
|
|
248
|
+
is_active: true,
|
|
249
|
+
contact_sales: false,
|
|
250
|
+
is_public: true,
|
|
251
|
+
color: "gray",
|
|
252
|
+
limits,
|
|
253
|
+
limits_unlimited: limitsUnlimited,
|
|
254
|
+
features: "",
|
|
255
|
+
sort_order: 0
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function PlanFormDialog({
|
|
259
|
+
open,
|
|
260
|
+
onOpenChange,
|
|
261
|
+
formData,
|
|
262
|
+
onFormDataChange,
|
|
263
|
+
formErrors,
|
|
264
|
+
selectedPlan,
|
|
265
|
+
onSubmit,
|
|
266
|
+
isPending
|
|
267
|
+
}) {
|
|
268
|
+
const config = useBillingAdminConfig();
|
|
269
|
+
const limitFields = config.plans?.limitFields || [];
|
|
270
|
+
const featuresConfig = config.plans?.features;
|
|
271
|
+
const { data: featuresData } = useQuery({
|
|
272
|
+
queryKey: ["billing-admin-available-features"],
|
|
273
|
+
queryFn: () => featuresConfig?.fetchFn?.() ?? Promise.resolve({ features: [] }),
|
|
274
|
+
enabled: !!featuresConfig?.enabled && !!featuresConfig?.fetchFn
|
|
275
|
+
});
|
|
276
|
+
const availableFeatures = featuresData?.features || [];
|
|
277
|
+
const [selectedFeatureSlugs, setSelectedFeatureSlugs] = useState([]);
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (formData.features) {
|
|
280
|
+
const slugs = formData.features.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
281
|
+
setSelectedFeatureSlugs(slugs);
|
|
282
|
+
}
|
|
283
|
+
}, [formData.features]);
|
|
284
|
+
const handleFeatureToggle = (slug, checked) => {
|
|
285
|
+
const updated = checked ? [...selectedFeatureSlugs, slug] : selectedFeatureSlugs.filter((s) => s !== slug);
|
|
286
|
+
setSelectedFeatureSlugs(updated);
|
|
287
|
+
onFormDataChange({ features: updated.join("\n") });
|
|
288
|
+
};
|
|
289
|
+
if (!open) return null;
|
|
290
|
+
return /* @__PURE__ */ jsxs3("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
291
|
+
/* @__PURE__ */ jsx6("div", { className: "fixed inset-0 bg-black/80", onClick: () => onOpenChange(false) }),
|
|
292
|
+
/* @__PURE__ */ jsxs3("div", { className: "relative z-50 w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-lg border bg-background p-6 shadow-lg", children: [
|
|
293
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex flex-col space-y-1.5 pb-4", children: [
|
|
294
|
+
/* @__PURE__ */ jsx6("h2", { className: "text-lg font-semibold leading-none tracking-tight", children: selectedPlan ? "Modifica Piano" : "Nuovo Piano" }),
|
|
295
|
+
/* @__PURE__ */ jsx6("p", { className: "text-sm text-muted-foreground", children: selectedPlan ? "Modifica le impostazioni del piano selezionato" : "Crea un nuovo piano di abbonamento" })
|
|
296
|
+
] }),
|
|
297
|
+
/* @__PURE__ */ jsxs3("div", { className: "grid gap-4 py-4", children: [
|
|
298
|
+
/* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
299
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
300
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", htmlFor: "slug", children: "Slug *" }),
|
|
301
|
+
/* @__PURE__ */ jsx6(
|
|
302
|
+
"input",
|
|
303
|
+
{
|
|
304
|
+
id: "slug",
|
|
305
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
|
|
306
|
+
value: formData.slug,
|
|
307
|
+
onChange: (e) => onFormDataChange({ slug: e.target.value }),
|
|
308
|
+
placeholder: "es. pro, enterprise",
|
|
309
|
+
disabled: !!selectedPlan
|
|
310
|
+
}
|
|
311
|
+
),
|
|
312
|
+
formErrors.slug && /* @__PURE__ */ jsx6("p", { className: "text-xs text-destructive", children: formErrors.slug })
|
|
313
|
+
] }),
|
|
314
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
315
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", htmlFor: "name", children: "Nome *" }),
|
|
316
|
+
/* @__PURE__ */ jsx6(
|
|
317
|
+
"input",
|
|
318
|
+
{
|
|
319
|
+
id: "name",
|
|
320
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
321
|
+
value: formData.name,
|
|
322
|
+
onChange: (e) => onFormDataChange({ name: e.target.value }),
|
|
323
|
+
placeholder: "es. Piano Pro"
|
|
324
|
+
}
|
|
325
|
+
),
|
|
326
|
+
formErrors.name && /* @__PURE__ */ jsx6("p", { className: "text-xs text-destructive", children: formErrors.name })
|
|
327
|
+
] })
|
|
328
|
+
] }),
|
|
329
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
330
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", htmlFor: "description", children: "Descrizione" }),
|
|
331
|
+
/* @__PURE__ */ jsx6(
|
|
332
|
+
"textarea",
|
|
333
|
+
{
|
|
334
|
+
id: "description",
|
|
335
|
+
className: "flex min-h-[60px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
336
|
+
value: formData.description,
|
|
337
|
+
onChange: (e) => onFormDataChange({ description: e.target.value }),
|
|
338
|
+
placeholder: "Descrizione del piano...",
|
|
339
|
+
rows: 2
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
] }),
|
|
343
|
+
/* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-4", children: [
|
|
344
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
345
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", children: "Modello Pricing" }),
|
|
346
|
+
/* @__PURE__ */ jsxs3(
|
|
347
|
+
"select",
|
|
348
|
+
{
|
|
349
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
350
|
+
value: formData.pricing_model,
|
|
351
|
+
onChange: (e) => onFormDataChange({ pricing_model: e.target.value }),
|
|
352
|
+
children: [
|
|
353
|
+
/* @__PURE__ */ jsx6("option", { value: "flat", children: "Fisso" }),
|
|
354
|
+
/* @__PURE__ */ jsx6("option", { value: "tiered", children: "A Scaglioni" }),
|
|
355
|
+
/* @__PURE__ */ jsx6("option", { value: "per_unit", children: "Per Unit\xE0" }),
|
|
356
|
+
/* @__PURE__ */ jsx6("option", { value: "usage_based", children: "A Consumo" })
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
] }),
|
|
361
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
362
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", htmlFor: "price_monthly", children: "Prezzo Mensile" }),
|
|
363
|
+
/* @__PURE__ */ jsx6(
|
|
364
|
+
"input",
|
|
365
|
+
{
|
|
366
|
+
id: "price_monthly",
|
|
367
|
+
type: "number",
|
|
368
|
+
step: "0.01",
|
|
369
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
370
|
+
value: formData.price_monthly,
|
|
371
|
+
onChange: (e) => onFormDataChange({ price_monthly: e.target.value }),
|
|
372
|
+
placeholder: "0.00"
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
] }),
|
|
376
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
377
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", htmlFor: "price_yearly", children: "Prezzo Annuale" }),
|
|
378
|
+
/* @__PURE__ */ jsx6(
|
|
379
|
+
"input",
|
|
380
|
+
{
|
|
381
|
+
id: "price_yearly",
|
|
382
|
+
type: "number",
|
|
383
|
+
step: "0.01",
|
|
384
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
385
|
+
value: formData.price_yearly,
|
|
386
|
+
onChange: (e) => onFormDataChange({ price_yearly: e.target.value }),
|
|
387
|
+
placeholder: "0.00"
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
] })
|
|
391
|
+
] }),
|
|
392
|
+
/* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-4", children: [
|
|
393
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
394
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", children: "Valuta" }),
|
|
395
|
+
/* @__PURE__ */ jsxs3(
|
|
396
|
+
"select",
|
|
397
|
+
{
|
|
398
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
399
|
+
value: formData.currency,
|
|
400
|
+
onChange: (e) => onFormDataChange({ currency: e.target.value }),
|
|
401
|
+
children: [
|
|
402
|
+
/* @__PURE__ */ jsx6("option", { value: "eur", children: "EUR" }),
|
|
403
|
+
/* @__PURE__ */ jsx6("option", { value: "usd", children: "USD" }),
|
|
404
|
+
/* @__PURE__ */ jsx6("option", { value: "gbp", children: "GBP" })
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
] }),
|
|
409
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
410
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", htmlFor: "trial_days", children: "Giorni Trial" }),
|
|
411
|
+
/* @__PURE__ */ jsx6("input", { id: "trial_days", type: "number", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.trial_days, onChange: (e) => onFormDataChange({ trial_days: parseInt(e.target.value) || 0 }) })
|
|
412
|
+
] }),
|
|
413
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
414
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", htmlFor: "sort_order", children: "Ordine" }),
|
|
415
|
+
/* @__PURE__ */ jsx6("input", { id: "sort_order", type: "number", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.sort_order, onChange: (e) => onFormDataChange({ sort_order: parseInt(e.target.value) || 0 }) })
|
|
416
|
+
] })
|
|
417
|
+
] }),
|
|
418
|
+
/* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
419
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
420
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", children: "Colore Badge" }),
|
|
421
|
+
/* @__PURE__ */ jsx6(
|
|
422
|
+
"select",
|
|
423
|
+
{
|
|
424
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
425
|
+
value: formData.color,
|
|
426
|
+
onChange: (e) => onFormDataChange({ color: e.target.value }),
|
|
427
|
+
children: PLAN_COLORS.map((color) => /* @__PURE__ */ jsx6("option", { value: color.value, children: color.label }, color.value))
|
|
428
|
+
}
|
|
429
|
+
),
|
|
430
|
+
/* @__PURE__ */ jsxs3("p", { className: "text-xs text-muted-foreground", children: [
|
|
431
|
+
"Anteprima: ",
|
|
432
|
+
/* @__PURE__ */ jsx6("span", { className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold ${getPlanColorClass(formData.color)}`, children: formData.name || "Piano" })
|
|
433
|
+
] })
|
|
434
|
+
] }),
|
|
435
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-4 pt-6", children: [
|
|
436
|
+
/* @__PURE__ */ jsx6(ToggleSwitch, { label: "Piano Attivo", checked: formData.is_active, onChange: (v) => onFormDataChange({ is_active: v }) }),
|
|
437
|
+
/* @__PURE__ */ jsx6(ToggleSwitch, { label: "Contattaci per Prezzi", checked: formData.contact_sales, onChange: (v) => onFormDataChange({ contact_sales: v }) }),
|
|
438
|
+
/* @__PURE__ */ jsx6(ToggleSwitch, { label: "Visibile in Homepage", checked: formData.is_public, onChange: (v) => onFormDataChange({ is_public: v }) })
|
|
439
|
+
] })
|
|
440
|
+
] }),
|
|
441
|
+
limitFields.length > 0 && /* @__PURE__ */ jsxs3("div", { className: "space-y-3", children: [
|
|
442
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", children: "Limiti Piano" }),
|
|
443
|
+
/* @__PURE__ */ jsx6("div", { className: "grid grid-cols-2 gap-3", children: limitFields.map((field) => /* @__PURE__ */ jsx6(
|
|
444
|
+
LimitInput,
|
|
445
|
+
{
|
|
446
|
+
id: `limit_${field.key}`,
|
|
447
|
+
label: field.label,
|
|
448
|
+
value: formData.limits[field.key] ?? field.defaultValue ?? 1,
|
|
449
|
+
isUnlimited: formData.limits_unlimited[field.key] ?? false,
|
|
450
|
+
onValueChange: (v) => onFormDataChange({ limits: { ...formData.limits, [field.key]: v } }),
|
|
451
|
+
onUnlimitedChange: (v) => onFormDataChange({ limits_unlimited: { ...formData.limits_unlimited, [field.key]: v } })
|
|
452
|
+
},
|
|
453
|
+
field.key
|
|
454
|
+
)) })
|
|
455
|
+
] }),
|
|
456
|
+
featuresConfig?.enabled && availableFeatures.length > 0 && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
457
|
+
/* @__PURE__ */ jsx6("label", { className: "text-sm font-medium leading-none", children: "Features Incluse nel Piano" }),
|
|
458
|
+
/* @__PURE__ */ jsx6("div", { className: "border rounded-lg p-3 space-y-2 max-h-64 overflow-y-auto", children: availableFeatures.map((feature) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 p-2 hover:bg-muted/50 rounded", children: [
|
|
459
|
+
/* @__PURE__ */ jsx6(
|
|
460
|
+
"input",
|
|
461
|
+
{
|
|
462
|
+
type: "checkbox",
|
|
463
|
+
id: `feature-${feature.slug}`,
|
|
464
|
+
checked: selectedFeatureSlugs.includes(feature.slug),
|
|
465
|
+
onChange: (e) => handleFeatureToggle(feature.slug, e.target.checked),
|
|
466
|
+
className: "h-4 w-4 rounded border-input"
|
|
467
|
+
}
|
|
468
|
+
),
|
|
469
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex-1", children: [
|
|
470
|
+
/* @__PURE__ */ jsx6("label", { htmlFor: `feature-${feature.slug}`, className: "text-sm font-medium cursor-pointer", children: feature.name }),
|
|
471
|
+
feature.description && /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: feature.description })
|
|
472
|
+
] }),
|
|
473
|
+
/* @__PURE__ */ jsx6("code", { className: "text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded", children: feature.slug })
|
|
474
|
+
] }, feature.slug)) }),
|
|
475
|
+
selectedFeatureSlugs.length > 0 && /* @__PURE__ */ jsx6("div", { className: "flex flex-wrap gap-2 pt-2", children: selectedFeatureSlugs.map((slug) => {
|
|
476
|
+
const feature = availableFeatures.find((f) => f.slug === slug);
|
|
477
|
+
return /* @__PURE__ */ jsxs3("span", { className: "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-secondary text-secondary-foreground gap-1", children: [
|
|
478
|
+
feature?.name || slug,
|
|
479
|
+
/* @__PURE__ */ jsx6("button", { type: "button", onClick: () => handleFeatureToggle(slug, false), className: "ml-1 hover:bg-destructive/20 rounded-full", children: /* @__PURE__ */ jsx6(XIcon, { className: "h-3 w-3" }) })
|
|
480
|
+
] }, slug);
|
|
481
|
+
}) })
|
|
482
|
+
] })
|
|
483
|
+
] }),
|
|
484
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4", children: [
|
|
485
|
+
/* @__PURE__ */ jsx6(
|
|
486
|
+
"button",
|
|
487
|
+
{
|
|
488
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground mt-2 sm:mt-0",
|
|
489
|
+
onClick: () => onOpenChange(false),
|
|
490
|
+
children: "Annulla"
|
|
491
|
+
}
|
|
492
|
+
),
|
|
493
|
+
/* @__PURE__ */ jsxs3(
|
|
494
|
+
"button",
|
|
495
|
+
{
|
|
496
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
|
|
497
|
+
onClick: onSubmit,
|
|
498
|
+
disabled: isPending,
|
|
499
|
+
children: [
|
|
500
|
+
isPending && /* @__PURE__ */ jsx6(Loader2, { className: "w-4 h-4 animate-spin mr-2" }),
|
|
501
|
+
selectedPlan ? "Salva Modifiche" : "Crea Piano"
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
] })
|
|
506
|
+
] })
|
|
507
|
+
] });
|
|
508
|
+
}
|
|
509
|
+
function ToggleSwitch({ label, checked, onChange }) {
|
|
510
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
511
|
+
/* @__PURE__ */ jsx6(
|
|
512
|
+
"button",
|
|
513
|
+
{
|
|
514
|
+
type: "button",
|
|
515
|
+
role: "switch",
|
|
516
|
+
"aria-checked": checked,
|
|
517
|
+
onClick: () => onChange(!checked),
|
|
518
|
+
className: `peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors ${checked ? "bg-primary" : "bg-input"}`,
|
|
519
|
+
children: /* @__PURE__ */ jsx6("span", { className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform ${checked ? "translate-x-4" : "translate-x-0"}` })
|
|
520
|
+
}
|
|
521
|
+
),
|
|
522
|
+
/* @__PURE__ */ jsx6("span", { className: "text-sm font-medium leading-none", children: label })
|
|
523
|
+
] });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/components/shared/DeleteConfirmDialog.tsx
|
|
527
|
+
import { Loader2 as Loader22 } from "lucide-react";
|
|
528
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
529
|
+
function DeleteConfirmDialog({
|
|
530
|
+
isOpen,
|
|
531
|
+
onOpenChange,
|
|
532
|
+
title = "Sei sicuro?",
|
|
533
|
+
description,
|
|
534
|
+
onConfirm,
|
|
535
|
+
isDeleting,
|
|
536
|
+
confirmLabel = "Elimina",
|
|
537
|
+
cancelLabel = "Annulla"
|
|
538
|
+
}) {
|
|
539
|
+
if (!isOpen) return null;
|
|
540
|
+
return /* @__PURE__ */ jsxs4("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
541
|
+
/* @__PURE__ */ jsx7(
|
|
542
|
+
"div",
|
|
543
|
+
{
|
|
544
|
+
className: "fixed inset-0 bg-black/80",
|
|
545
|
+
onClick: () => onOpenChange(false)
|
|
546
|
+
}
|
|
547
|
+
),
|
|
548
|
+
/* @__PURE__ */ jsxs4("div", { className: "relative z-50 w-full max-w-lg rounded-lg border bg-background p-6 shadow-lg", children: [
|
|
549
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-col space-y-2 text-center sm:text-left", children: [
|
|
550
|
+
/* @__PURE__ */ jsx7("h2", { className: "text-lg font-semibold", children: title }),
|
|
551
|
+
/* @__PURE__ */ jsx7("div", { className: "text-sm text-muted-foreground", children: description })
|
|
552
|
+
] }),
|
|
553
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-4", children: [
|
|
554
|
+
/* @__PURE__ */ jsx7(
|
|
555
|
+
"button",
|
|
556
|
+
{
|
|
557
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground mt-2 sm:mt-0",
|
|
558
|
+
onClick: () => onOpenChange(false),
|
|
559
|
+
children: cancelLabel
|
|
560
|
+
}
|
|
561
|
+
),
|
|
562
|
+
/* @__PURE__ */ jsxs4(
|
|
563
|
+
"button",
|
|
564
|
+
{
|
|
565
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
566
|
+
onClick: onConfirm,
|
|
567
|
+
disabled: isDeleting,
|
|
568
|
+
children: [
|
|
569
|
+
isDeleting && /* @__PURE__ */ jsx7(Loader22, { className: "w-4 h-4 animate-spin mr-2" }),
|
|
570
|
+
confirmLabel
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
)
|
|
574
|
+
] })
|
|
575
|
+
] })
|
|
576
|
+
] });
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/pages/PlansPage.tsx
|
|
580
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
581
|
+
function PlansPage({ wrapper: Wrapper, header }) {
|
|
582
|
+
const config = useBillingAdminConfig();
|
|
583
|
+
const limitFields = config.plans?.limitFields || [];
|
|
584
|
+
const publicPlansConfig = config.plans?.publicPlans;
|
|
585
|
+
const [showForm, setShowForm] = useState2(false);
|
|
586
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState2(false);
|
|
587
|
+
const [selectedPlan, setSelectedPlan] = useState2(null);
|
|
588
|
+
const [formData, setFormData] = useState2(createDefaultPlanFormData(limitFields));
|
|
589
|
+
const [formErrors, setFormErrors] = useState2({});
|
|
590
|
+
const { data, isLoading, refetch } = useAdminPlans();
|
|
591
|
+
const createMutation = useCreatePlan();
|
|
592
|
+
const updateMutation = useUpdatePlan();
|
|
593
|
+
const deleteMutation = useDeletePlan();
|
|
594
|
+
const syncMutation = useSyncPlanToStripe();
|
|
595
|
+
const [publicPlanSlugs, setPublicPlanSlugs] = useState2([]);
|
|
596
|
+
useState2(() => {
|
|
597
|
+
if (publicPlansConfig?.enabled && publicPlansConfig.fetchFn) {
|
|
598
|
+
publicPlansConfig.fetchFn().then((r) => setPublicPlanSlugs(r.public_plan_slugs));
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
const plans = data?.items || [];
|
|
602
|
+
const resetForm = () => {
|
|
603
|
+
setFormData(createDefaultPlanFormData(limitFields));
|
|
604
|
+
setSelectedPlan(null);
|
|
605
|
+
setFormErrors({});
|
|
606
|
+
};
|
|
607
|
+
const openCreateForm = () => {
|
|
608
|
+
resetForm();
|
|
609
|
+
setShowForm(true);
|
|
610
|
+
};
|
|
611
|
+
const openEditForm = (plan) => {
|
|
612
|
+
setSelectedPlan(plan);
|
|
613
|
+
const planLimits = plan.limits || {};
|
|
614
|
+
const limits = {};
|
|
615
|
+
const limitsUnlimited = {};
|
|
616
|
+
for (const field of limitFields) {
|
|
617
|
+
const val = planLimits[field.key];
|
|
618
|
+
limits[field.key] = val === -1 ? field.defaultValue ?? 1 : val || field.defaultValue || 1;
|
|
619
|
+
limitsUnlimited[field.key] = val === -1;
|
|
620
|
+
}
|
|
621
|
+
setFormData({
|
|
622
|
+
slug: plan.slug,
|
|
623
|
+
name: plan.name,
|
|
624
|
+
description: plan.description || "",
|
|
625
|
+
pricing_model: plan.pricing_model,
|
|
626
|
+
price_monthly: plan.price_monthly ? plan.price_monthly.toString() : "",
|
|
627
|
+
price_yearly: plan.price_yearly ? plan.price_yearly.toString() : "",
|
|
628
|
+
currency: plan.currency,
|
|
629
|
+
trial_days: plan.trial_days,
|
|
630
|
+
is_active: plan.is_active,
|
|
631
|
+
contact_sales: plan.contact_sales,
|
|
632
|
+
is_public: publicPlanSlugs.includes(plan.slug),
|
|
633
|
+
color: plan.color || "gray",
|
|
634
|
+
limits,
|
|
635
|
+
limits_unlimited: limitsUnlimited,
|
|
636
|
+
features: plan.features?.join("\n") || "",
|
|
637
|
+
sort_order: plan.sort_order
|
|
638
|
+
});
|
|
639
|
+
setShowForm(true);
|
|
640
|
+
};
|
|
641
|
+
const handleValidate = () => {
|
|
642
|
+
const errors = {};
|
|
643
|
+
const slugErr = validateSlug(formData.slug);
|
|
644
|
+
if (slugErr) errors.slug = slugErr;
|
|
645
|
+
const nameErr = validateRequired(formData.name, "Il nome");
|
|
646
|
+
if (nameErr) errors.name = nameErr;
|
|
647
|
+
setFormErrors(errors);
|
|
648
|
+
return Object.keys(errors).length === 0;
|
|
649
|
+
};
|
|
650
|
+
const handleSubmit = async () => {
|
|
651
|
+
if (!handleValidate()) return;
|
|
652
|
+
const limits = {};
|
|
653
|
+
for (const field of limitFields) {
|
|
654
|
+
limits[field.key] = formData.limits_unlimited[field.key] ? -1 : formData.limits[field.key] ?? 1;
|
|
655
|
+
}
|
|
656
|
+
const planData = {
|
|
657
|
+
slug: formData.slug,
|
|
658
|
+
name: formData.name,
|
|
659
|
+
description: formData.description || void 0,
|
|
660
|
+
pricing_model: formData.pricing_model,
|
|
661
|
+
price_monthly: formData.price_monthly ? parseFloat(formData.price_monthly) : void 0,
|
|
662
|
+
price_yearly: formData.price_yearly ? parseFloat(formData.price_yearly) : void 0,
|
|
663
|
+
currency: formData.currency,
|
|
664
|
+
trial_days: formData.trial_days,
|
|
665
|
+
is_active: formData.is_active,
|
|
666
|
+
contact_sales: formData.contact_sales,
|
|
667
|
+
color: formData.color,
|
|
668
|
+
limits: Object.keys(limits).length > 0 ? limits : void 0,
|
|
669
|
+
features: formData.features.split("\n").filter((f) => f.trim()),
|
|
670
|
+
sort_order: formData.sort_order
|
|
671
|
+
};
|
|
672
|
+
if (publicPlansConfig?.enabled && publicPlansConfig.updateFn) {
|
|
673
|
+
const isCurrentlyPublic = publicPlanSlugs.includes(formData.slug);
|
|
674
|
+
if (formData.is_public !== isCurrentlyPublic) {
|
|
675
|
+
try {
|
|
676
|
+
const newSlugs = formData.is_public ? [...publicPlanSlugs, formData.slug] : publicPlanSlugs.filter((s) => s !== formData.slug);
|
|
677
|
+
await publicPlansConfig.updateFn(newSlugs);
|
|
678
|
+
setPublicPlanSlugs(newSlugs);
|
|
679
|
+
} catch {
|
|
680
|
+
toast.error("Errore nell'aggiornamento della visibilit\xE0 pubblica");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (selectedPlan) {
|
|
685
|
+
updateMutation.mutate({ id: selectedPlan.id, data: planData }, {
|
|
686
|
+
onSuccess: () => {
|
|
687
|
+
toast.success("Piano aggiornato con successo");
|
|
688
|
+
setShowForm(false);
|
|
689
|
+
resetForm();
|
|
690
|
+
},
|
|
691
|
+
onError: (err) => toast.error(`Errore: ${err.message}`)
|
|
692
|
+
});
|
|
693
|
+
} else {
|
|
694
|
+
createMutation.mutate(planData, {
|
|
695
|
+
onSuccess: () => {
|
|
696
|
+
toast.success("Piano creato con successo");
|
|
697
|
+
setShowForm(false);
|
|
698
|
+
resetForm();
|
|
699
|
+
},
|
|
700
|
+
onError: (err) => toast.error(`Errore: ${err.message}`)
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
const handleDelete = () => {
|
|
705
|
+
if (selectedPlan) {
|
|
706
|
+
deleteMutation.mutate(selectedPlan.id, {
|
|
707
|
+
onSuccess: () => {
|
|
708
|
+
toast.success("Piano eliminato con successo");
|
|
709
|
+
setShowDeleteDialog(false);
|
|
710
|
+
setSelectedPlan(null);
|
|
711
|
+
},
|
|
712
|
+
onError: (err) => toast.error(`Errore: ${err.message}`)
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
const content = /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
717
|
+
header,
|
|
718
|
+
/* @__PURE__ */ jsxs5("div", { className: "space-y-4", children: [
|
|
719
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between", children: [
|
|
720
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
721
|
+
/* @__PURE__ */ jsx8(LayoutGrid, { className: "w-4 h-4" }),
|
|
722
|
+
/* @__PURE__ */ jsxs5("span", { children: [
|
|
723
|
+
plans.length,
|
|
724
|
+
" piani configurati"
|
|
725
|
+
] })
|
|
726
|
+
] }),
|
|
727
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [
|
|
728
|
+
/* @__PURE__ */ jsxs5("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 border border-input bg-background hover:bg-accent", onClick: () => refetch(), children: [
|
|
729
|
+
/* @__PURE__ */ jsx8(RefreshCw, { className: "w-4 h-4 mr-2" }),
|
|
730
|
+
"Aggiorna"
|
|
731
|
+
] }),
|
|
732
|
+
/* @__PURE__ */ jsxs5("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90", onClick: openCreateForm, children: [
|
|
733
|
+
/* @__PURE__ */ jsx8(Plus, { className: "w-4 h-4 mr-2" }),
|
|
734
|
+
"Nuovo Piano"
|
|
735
|
+
] })
|
|
736
|
+
] })
|
|
737
|
+
] }),
|
|
738
|
+
/* @__PURE__ */ jsx8("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: /* @__PURE__ */ jsx8("div", { className: "p-0", children: /* @__PURE__ */ jsx8(
|
|
739
|
+
PlansTable,
|
|
740
|
+
{
|
|
741
|
+
plans,
|
|
742
|
+
isLoading,
|
|
743
|
+
onEdit: openEditForm,
|
|
744
|
+
onDelete: (plan) => {
|
|
745
|
+
setSelectedPlan(plan);
|
|
746
|
+
setShowDeleteDialog(true);
|
|
747
|
+
},
|
|
748
|
+
onSync: (id) => syncMutation.mutate(id, {
|
|
749
|
+
onSuccess: () => toast.success("Piano sincronizzato con Stripe"),
|
|
750
|
+
onError: (err) => toast.error(`Errore sync: ${err.message}`)
|
|
751
|
+
}),
|
|
752
|
+
isSyncing: syncMutation.isPending
|
|
753
|
+
}
|
|
754
|
+
) }) })
|
|
755
|
+
] }),
|
|
756
|
+
/* @__PURE__ */ jsx8(
|
|
757
|
+
PlanFormDialog,
|
|
758
|
+
{
|
|
759
|
+
open: showForm,
|
|
760
|
+
onOpenChange: setShowForm,
|
|
761
|
+
formData,
|
|
762
|
+
onFormDataChange: (data2) => setFormData((prev) => ({ ...prev, ...data2 })),
|
|
763
|
+
formErrors,
|
|
764
|
+
selectedPlan,
|
|
765
|
+
onSubmit: handleSubmit,
|
|
766
|
+
isPending: createMutation.isPending || updateMutation.isPending
|
|
767
|
+
}
|
|
768
|
+
),
|
|
769
|
+
/* @__PURE__ */ jsx8(
|
|
770
|
+
DeleteConfirmDialog,
|
|
771
|
+
{
|
|
772
|
+
isOpen: showDeleteDialog,
|
|
773
|
+
onOpenChange: setShowDeleteDialog,
|
|
774
|
+
title: "Eliminare Piano?",
|
|
775
|
+
description: /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
776
|
+
"Stai per eliminare il piano ",
|
|
777
|
+
/* @__PURE__ */ jsx8("strong", { children: selectedPlan?.name }),
|
|
778
|
+
" (",
|
|
779
|
+
/* @__PURE__ */ jsx8("code", { children: selectedPlan?.slug }),
|
|
780
|
+
"). Questa azione non pu\xF2 essere annullata."
|
|
781
|
+
] }),
|
|
782
|
+
onConfirm: handleDelete,
|
|
783
|
+
isDeleting: deleteMutation.isPending,
|
|
784
|
+
confirmLabel: "Elimina Piano"
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
] });
|
|
788
|
+
return Wrapper ? /* @__PURE__ */ jsx8(Wrapper, { children: content }) : content;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/pages/ProductsPage.tsx
|
|
792
|
+
import { toast as toast2 } from "sonner";
|
|
793
|
+
import { Plus as Plus2, RefreshCw as RefreshCw2, Pencil as Pencil2, Trash2 as Trash22, Upload as Upload2, Package, Check as Check2, X as X2 } from "lucide-react";
|
|
794
|
+
import { useQuery as useQuery2 } from "@tanstack/react-query";
|
|
795
|
+
import {
|
|
796
|
+
useAdminProducts,
|
|
797
|
+
useCreateProduct,
|
|
798
|
+
useUpdateProduct,
|
|
799
|
+
useDeleteProduct,
|
|
800
|
+
useSyncProductToStripe,
|
|
801
|
+
useDialogState,
|
|
802
|
+
formatCurrencyFromCents
|
|
803
|
+
} from "@growflowstudio/billing-admin-core";
|
|
804
|
+
|
|
805
|
+
// src/components/products/ProductFormDialog.tsx
|
|
806
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
807
|
+
import { Loader2 as Loader23, Link as LinkIcon } from "lucide-react";
|
|
808
|
+
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
809
|
+
var defaultProductFormData = {
|
|
810
|
+
name: "",
|
|
811
|
+
description: "",
|
|
812
|
+
price_cents: "",
|
|
813
|
+
currency: "eur",
|
|
814
|
+
recurring_interval: "",
|
|
815
|
+
category: "feature",
|
|
816
|
+
is_active: true,
|
|
817
|
+
sync_stripe: true,
|
|
818
|
+
feature_slug: "",
|
|
819
|
+
extra_data: "{}"
|
|
820
|
+
};
|
|
821
|
+
function ProductFormDialog({
|
|
822
|
+
isOpen,
|
|
823
|
+
onOpenChange,
|
|
824
|
+
mode,
|
|
825
|
+
product,
|
|
826
|
+
features = [],
|
|
827
|
+
onSubmit,
|
|
828
|
+
isSubmitting
|
|
829
|
+
}) {
|
|
830
|
+
const [formData, setFormData] = useState3(defaultProductFormData);
|
|
831
|
+
const [formErrors, setFormErrors] = useState3({});
|
|
832
|
+
useEffect2(() => {
|
|
833
|
+
if (isOpen) {
|
|
834
|
+
if (mode === "edit" && product) {
|
|
835
|
+
const featureSlug = product.extra_data?.feature_slug || "";
|
|
836
|
+
setFormData({
|
|
837
|
+
name: product.name,
|
|
838
|
+
description: product.description || "",
|
|
839
|
+
price_cents: (product.price_cents / 100).toString(),
|
|
840
|
+
currency: product.currency,
|
|
841
|
+
recurring_interval: product.recurring_interval || "",
|
|
842
|
+
category: product.category || "feature",
|
|
843
|
+
is_active: product.is_active,
|
|
844
|
+
sync_stripe: false,
|
|
845
|
+
feature_slug: featureSlug,
|
|
846
|
+
extra_data: product.extra_data ? JSON.stringify(product.extra_data, null, 2) : "{}"
|
|
847
|
+
});
|
|
848
|
+
} else {
|
|
849
|
+
setFormData(defaultProductFormData);
|
|
850
|
+
}
|
|
851
|
+
setFormErrors({});
|
|
852
|
+
}
|
|
853
|
+
}, [isOpen, mode, product]);
|
|
854
|
+
useEffect2(() => {
|
|
855
|
+
if (formData.feature_slug) {
|
|
856
|
+
setFormData((prev) => {
|
|
857
|
+
try {
|
|
858
|
+
const parsedData = JSON.parse(prev.extra_data);
|
|
859
|
+
const updated = { ...parsedData, feature_slug: formData.feature_slug };
|
|
860
|
+
return { ...prev, extra_data: JSON.stringify(updated, null, 2) };
|
|
861
|
+
} catch {
|
|
862
|
+
return { ...prev, extra_data: JSON.stringify({ feature_slug: formData.feature_slug }, null, 2) };
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}, [formData.feature_slug]);
|
|
867
|
+
const validateForm = () => {
|
|
868
|
+
const errors = {};
|
|
869
|
+
if (!formData.name.trim()) errors.name = "Il nome \xE8 obbligatorio";
|
|
870
|
+
if (!formData.price_cents || parseFloat(formData.price_cents) < 0) errors.price_cents = "Il prezzo deve essere un numero positivo";
|
|
871
|
+
try {
|
|
872
|
+
JSON.parse(formData.extra_data);
|
|
873
|
+
} catch {
|
|
874
|
+
errors.extra_data = "JSON non valido per extra_data";
|
|
875
|
+
}
|
|
876
|
+
setFormErrors(errors);
|
|
877
|
+
return Object.keys(errors).length === 0;
|
|
878
|
+
};
|
|
879
|
+
const handleSubmit = () => {
|
|
880
|
+
if (validateForm()) onSubmit(formData);
|
|
881
|
+
};
|
|
882
|
+
if (!isOpen) return null;
|
|
883
|
+
return /* @__PURE__ */ jsxs6("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
884
|
+
/* @__PURE__ */ jsx9("div", { className: "fixed inset-0 bg-black/80", onClick: () => onOpenChange(false) }),
|
|
885
|
+
/* @__PURE__ */ jsxs6("div", { className: "relative z-50 w-full max-w-xl max-h-[90vh] overflow-y-auto rounded-lg border bg-background p-6 shadow-lg", children: [
|
|
886
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex flex-col space-y-1.5 pb-4", children: [
|
|
887
|
+
/* @__PURE__ */ jsx9("h2", { className: "text-lg font-semibold", children: mode === "create" ? "Nuovo Prodotto" : "Modifica Prodotto" }),
|
|
888
|
+
/* @__PURE__ */ jsx9("p", { className: "text-sm text-muted-foreground", children: mode === "create" ? "Crea un nuovo prodotto / feature acquistabile" : "Modifica le impostazioni del prodotto selezionato" })
|
|
889
|
+
] }),
|
|
890
|
+
/* @__PURE__ */ jsxs6("div", { className: "grid gap-4 py-4", children: [
|
|
891
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
892
|
+
/* @__PURE__ */ jsx9("label", { className: "text-sm font-medium leading-none", children: "Nome *" }),
|
|
893
|
+
/* @__PURE__ */ jsx9("input", { className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.name, onChange: (e) => setFormData({ ...formData, name: e.target.value }), placeholder: "es. Ottimizza Dati Prodotti" }),
|
|
894
|
+
formErrors.name && /* @__PURE__ */ jsx9("p", { className: "text-xs text-destructive", children: formErrors.name })
|
|
895
|
+
] }),
|
|
896
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
897
|
+
/* @__PURE__ */ jsx9("label", { className: "text-sm font-medium leading-none", children: "Descrizione" }),
|
|
898
|
+
/* @__PURE__ */ jsx9("textarea", { className: "flex min-h-[60px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.description, onChange: (e) => setFormData({ ...formData, description: e.target.value }), rows: 2 })
|
|
899
|
+
] }),
|
|
900
|
+
/* @__PURE__ */ jsxs6("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
901
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
902
|
+
/* @__PURE__ */ jsx9("label", { className: "text-sm font-medium leading-none", children: "Prezzo" }),
|
|
903
|
+
/* @__PURE__ */ jsx9("input", { type: "number", step: "0.01", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.price_cents, onChange: (e) => setFormData({ ...formData, price_cents: e.target.value }), placeholder: "49.00" }),
|
|
904
|
+
formErrors.price_cents && /* @__PURE__ */ jsx9("p", { className: "text-xs text-destructive", children: formErrors.price_cents })
|
|
905
|
+
] }),
|
|
906
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
907
|
+
/* @__PURE__ */ jsx9("label", { className: "text-sm font-medium leading-none", children: "Valuta" }),
|
|
908
|
+
/* @__PURE__ */ jsxs6("select", { className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.currency, onChange: (e) => setFormData({ ...formData, currency: e.target.value }), children: [
|
|
909
|
+
/* @__PURE__ */ jsx9("option", { value: "eur", children: "EUR" }),
|
|
910
|
+
/* @__PURE__ */ jsx9("option", { value: "usd", children: "USD" }),
|
|
911
|
+
/* @__PURE__ */ jsx9("option", { value: "gbp", children: "GBP" })
|
|
912
|
+
] })
|
|
913
|
+
] })
|
|
914
|
+
] }),
|
|
915
|
+
/* @__PURE__ */ jsxs6("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
916
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
917
|
+
/* @__PURE__ */ jsx9("label", { className: "text-sm font-medium leading-none", children: "Ricorrenza" }),
|
|
918
|
+
/* @__PURE__ */ jsxs6("select", { className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.recurring_interval || "one_time", onChange: (e) => setFormData({ ...formData, recurring_interval: e.target.value === "one_time" ? "" : e.target.value }), children: [
|
|
919
|
+
/* @__PURE__ */ jsx9("option", { value: "one_time", children: "Una tantum" }),
|
|
920
|
+
/* @__PURE__ */ jsx9("option", { value: "month", children: "Mensile" }),
|
|
921
|
+
/* @__PURE__ */ jsx9("option", { value: "year", children: "Annuale" })
|
|
922
|
+
] })
|
|
923
|
+
] }),
|
|
924
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
925
|
+
/* @__PURE__ */ jsx9("label", { className: "text-sm font-medium leading-none", children: "Categoria" }),
|
|
926
|
+
/* @__PURE__ */ jsxs6("select", { className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.category, onChange: (e) => setFormData({ ...formData, category: e.target.value }), children: [
|
|
927
|
+
/* @__PURE__ */ jsx9("option", { value: "feature", children: "Feature" }),
|
|
928
|
+
/* @__PURE__ */ jsx9("option", { value: "addon", children: "Add-on" }),
|
|
929
|
+
/* @__PURE__ */ jsx9("option", { value: "service", children: "Servizio" }),
|
|
930
|
+
/* @__PURE__ */ jsx9("option", { value: "credits", children: "Crediti" })
|
|
931
|
+
] })
|
|
932
|
+
] })
|
|
933
|
+
] }),
|
|
934
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
|
|
935
|
+
/* @__PURE__ */ jsx9(ToggleRow, { label: "Prodotto Attivo", checked: formData.is_active, onChange: (v) => setFormData({ ...formData, is_active: v }) }),
|
|
936
|
+
mode === "create" && /* @__PURE__ */ jsx9(ToggleRow, { label: "Sincronizza su Stripe", checked: formData.sync_stripe, onChange: (v) => setFormData({ ...formData, sync_stripe: v }) })
|
|
937
|
+
] }),
|
|
938
|
+
features.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
939
|
+
/* @__PURE__ */ jsx9("label", { className: "text-sm font-medium leading-none", children: "Collega a Feature" }),
|
|
940
|
+
/* @__PURE__ */ jsxs6(
|
|
941
|
+
"select",
|
|
942
|
+
{
|
|
943
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
944
|
+
value: formData.feature_slug || "none",
|
|
945
|
+
onChange: (e) => setFormData({ ...formData, feature_slug: e.target.value === "none" ? "" : e.target.value }),
|
|
946
|
+
children: [
|
|
947
|
+
/* @__PURE__ */ jsx9("option", { value: "none", children: "Nessuna feature" }),
|
|
948
|
+
features.map((f) => /* @__PURE__ */ jsxs6("option", { value: f.slug, children: [
|
|
949
|
+
f.name,
|
|
950
|
+
" (",
|
|
951
|
+
f.slug,
|
|
952
|
+
")"
|
|
953
|
+
] }, f.slug))
|
|
954
|
+
]
|
|
955
|
+
}
|
|
956
|
+
),
|
|
957
|
+
formData.feature_slug && /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 p-2 bg-blue-50 border border-blue-200 rounded text-sm", children: [
|
|
958
|
+
/* @__PURE__ */ jsx9(LinkIcon, { className: "w-4 h-4 text-blue-600" }),
|
|
959
|
+
/* @__PURE__ */ jsxs6("span", { className: "text-blue-900", children: [
|
|
960
|
+
"Collegato a: ",
|
|
961
|
+
/* @__PURE__ */ jsx9("strong", { children: formData.feature_slug })
|
|
962
|
+
] })
|
|
963
|
+
] })
|
|
964
|
+
] }),
|
|
965
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
966
|
+
/* @__PURE__ */ jsx9("label", { className: "text-sm font-medium leading-none", children: "Extra Data (JSON)" }),
|
|
967
|
+
/* @__PURE__ */ jsx9("textarea", { className: "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: formData.extra_data, onChange: (e) => setFormData({ ...formData, extra_data: e.target.value }), rows: 3 }),
|
|
968
|
+
formErrors.extra_data && /* @__PURE__ */ jsx9("p", { className: "text-xs text-destructive", children: formErrors.extra_data })
|
|
969
|
+
] })
|
|
970
|
+
] }),
|
|
971
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4", children: [
|
|
972
|
+
/* @__PURE__ */ jsx9("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent mt-2 sm:mt-0", onClick: () => onOpenChange(false), children: "Annulla" }),
|
|
973
|
+
/* @__PURE__ */ jsxs6("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50", onClick: handleSubmit, disabled: isSubmitting, children: [
|
|
974
|
+
isSubmitting && /* @__PURE__ */ jsx9(Loader23, { className: "w-4 h-4 animate-spin mr-2" }),
|
|
975
|
+
mode === "create" ? "Crea Prodotto" : "Salva Modifiche"
|
|
976
|
+
] })
|
|
977
|
+
] })
|
|
978
|
+
] })
|
|
979
|
+
] });
|
|
980
|
+
}
|
|
981
|
+
function ToggleRow({ label, checked, onChange }) {
|
|
982
|
+
return /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
|
|
983
|
+
/* @__PURE__ */ jsx9(
|
|
984
|
+
"button",
|
|
985
|
+
{
|
|
986
|
+
type: "button",
|
|
987
|
+
role: "switch",
|
|
988
|
+
"aria-checked": checked,
|
|
989
|
+
onClick: () => onChange(!checked),
|
|
990
|
+
className: `inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors ${checked ? "bg-primary" : "bg-input"}`,
|
|
991
|
+
children: /* @__PURE__ */ jsx9("span", { className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg transition-transform ${checked ? "translate-x-4" : "translate-x-0"}` })
|
|
992
|
+
}
|
|
993
|
+
),
|
|
994
|
+
/* @__PURE__ */ jsx9("span", { className: "text-sm font-medium leading-none", children: label })
|
|
995
|
+
] });
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/pages/ProductsPage.tsx
|
|
999
|
+
import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1000
|
+
function ProductsPage({ wrapper: Wrapper, header }) {
|
|
1001
|
+
const config = useBillingAdminConfig();
|
|
1002
|
+
const featuresConfig = config.products?.features;
|
|
1003
|
+
const createDialog = useDialogState();
|
|
1004
|
+
const editDialog = useDialogState();
|
|
1005
|
+
const deleteDialog = useDialogState();
|
|
1006
|
+
const { data, isLoading, refetch } = useAdminProducts({ is_active: true });
|
|
1007
|
+
const createMutation = useCreateProduct();
|
|
1008
|
+
const updateMutation = useUpdateProduct();
|
|
1009
|
+
const deleteMutation = useDeleteProduct();
|
|
1010
|
+
const syncMutation = useSyncProductToStripe();
|
|
1011
|
+
const { data: featuresData } = useQuery2({
|
|
1012
|
+
queryKey: ["billing-admin-product-features"],
|
|
1013
|
+
queryFn: () => featuresConfig?.fetchFn?.() ?? Promise.resolve({ features: [] }),
|
|
1014
|
+
enabled: !!featuresConfig?.enabled && !!featuresConfig?.fetchFn
|
|
1015
|
+
});
|
|
1016
|
+
const products = data?.items || [];
|
|
1017
|
+
const features = featuresData?.features || [];
|
|
1018
|
+
const handleCreateSubmit = (formData) => {
|
|
1019
|
+
const priceCents = Math.round(parseFloat(formData.price_cents) * 100);
|
|
1020
|
+
const createData = {
|
|
1021
|
+
name: formData.name,
|
|
1022
|
+
description: formData.description || void 0,
|
|
1023
|
+
price_cents: priceCents,
|
|
1024
|
+
currency: formData.currency,
|
|
1025
|
+
recurring_interval: formData.recurring_interval || void 0,
|
|
1026
|
+
category: formData.category || void 0,
|
|
1027
|
+
is_active: formData.is_active,
|
|
1028
|
+
sync_stripe: formData.sync_stripe,
|
|
1029
|
+
extra_data: JSON.parse(formData.extra_data)
|
|
1030
|
+
};
|
|
1031
|
+
createMutation.mutate(createData, {
|
|
1032
|
+
onSuccess: () => {
|
|
1033
|
+
toast2.success("Prodotto creato");
|
|
1034
|
+
createDialog.close();
|
|
1035
|
+
},
|
|
1036
|
+
onError: (err) => toast2.error(`Errore: ${err.message}`)
|
|
1037
|
+
});
|
|
1038
|
+
};
|
|
1039
|
+
const handleEditSubmit = (formData) => {
|
|
1040
|
+
if (!editDialog.data) return;
|
|
1041
|
+
const priceCents = Math.round(parseFloat(formData.price_cents) * 100);
|
|
1042
|
+
const updateData = {
|
|
1043
|
+
name: formData.name,
|
|
1044
|
+
description: formData.description || void 0,
|
|
1045
|
+
price_cents: priceCents,
|
|
1046
|
+
currency: formData.currency,
|
|
1047
|
+
recurring_interval: formData.recurring_interval || void 0,
|
|
1048
|
+
category: formData.category || void 0,
|
|
1049
|
+
is_active: formData.is_active,
|
|
1050
|
+
extra_data: JSON.parse(formData.extra_data)
|
|
1051
|
+
};
|
|
1052
|
+
updateMutation.mutate({ id: editDialog.data.id, data: updateData }, {
|
|
1053
|
+
onSuccess: () => {
|
|
1054
|
+
toast2.success("Prodotto aggiornato");
|
|
1055
|
+
editDialog.close();
|
|
1056
|
+
},
|
|
1057
|
+
onError: (err) => toast2.error(`Errore: ${err.message}`)
|
|
1058
|
+
});
|
|
1059
|
+
};
|
|
1060
|
+
const handleDeleteConfirm = () => {
|
|
1061
|
+
if (deleteDialog.data) {
|
|
1062
|
+
deleteMutation.mutate(deleteDialog.data.id, {
|
|
1063
|
+
onSuccess: () => {
|
|
1064
|
+
toast2.success("Prodotto disattivato");
|
|
1065
|
+
deleteDialog.close();
|
|
1066
|
+
},
|
|
1067
|
+
onError: (err) => toast2.error(`Errore: ${err.message}`)
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
const content = /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
1072
|
+
header,
|
|
1073
|
+
/* @__PURE__ */ jsxs7("div", { className: "space-y-4", children: [
|
|
1074
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
|
|
1075
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1076
|
+
/* @__PURE__ */ jsx10(Package, { className: "w-4 h-4" }),
|
|
1077
|
+
/* @__PURE__ */ jsxs7("span", { children: [
|
|
1078
|
+
products.length,
|
|
1079
|
+
" prodotti configurati"
|
|
1080
|
+
] })
|
|
1081
|
+
] }),
|
|
1082
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex gap-2", children: [
|
|
1083
|
+
/* @__PURE__ */ jsxs7("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 border border-input bg-background hover:bg-accent", onClick: () => refetch(), children: [
|
|
1084
|
+
/* @__PURE__ */ jsx10(RefreshCw2, { className: "w-4 h-4 mr-2" }),
|
|
1085
|
+
"Aggiorna"
|
|
1086
|
+
] }),
|
|
1087
|
+
/* @__PURE__ */ jsxs7("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90", onClick: () => createDialog.open(), children: [
|
|
1088
|
+
/* @__PURE__ */ jsx10(Plus2, { className: "w-4 h-4 mr-2" }),
|
|
1089
|
+
"Nuovo Prodotto"
|
|
1090
|
+
] })
|
|
1091
|
+
] })
|
|
1092
|
+
] }),
|
|
1093
|
+
/* @__PURE__ */ jsx10("div", { className: "rounded-lg border bg-card shadow-sm", children: /* @__PURE__ */ jsx10("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsxs7("table", { className: "w-full caption-bottom text-sm", children: [
|
|
1094
|
+
/* @__PURE__ */ jsx10("thead", { className: "[&_tr]:border-b", children: /* @__PURE__ */ jsxs7("tr", { className: "border-b hover:bg-muted/50", children: [
|
|
1095
|
+
/* @__PURE__ */ jsx10("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Nome" }),
|
|
1096
|
+
/* @__PURE__ */ jsx10("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Categoria" }),
|
|
1097
|
+
/* @__PURE__ */ jsx10("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Prezzo" }),
|
|
1098
|
+
/* @__PURE__ */ jsx10("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Ricorrenza" }),
|
|
1099
|
+
/* @__PURE__ */ jsx10("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Attivo" }),
|
|
1100
|
+
/* @__PURE__ */ jsx10("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Stripe" }),
|
|
1101
|
+
/* @__PURE__ */ jsx10("th", { className: "h-12 px-4 text-right font-medium text-muted-foreground", children: "Azioni" })
|
|
1102
|
+
] }) }),
|
|
1103
|
+
/* @__PURE__ */ jsx10("tbody", { children: isLoading ? /* @__PURE__ */ jsx10(SkeletonRows, { rows: 3, columns: 7 }) : products.length === 0 ? /* @__PURE__ */ jsx10("tr", { children: /* @__PURE__ */ jsx10("td", { colSpan: 7, className: "p-4 text-center py-8 text-muted-foreground", children: "Nessun prodotto configurato" }) }) : products.map((product) => {
|
|
1104
|
+
const hasFeature = !!product.extra_data?.feature_slug;
|
|
1105
|
+
return /* @__PURE__ */ jsxs7("tr", { className: "border-b hover:bg-muted/50", children: [
|
|
1106
|
+
/* @__PURE__ */ jsx10("td", { className: "p-4", children: /* @__PURE__ */ jsxs7("div", { children: [
|
|
1107
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
|
|
1108
|
+
/* @__PURE__ */ jsx10("span", { className: "font-medium", children: product.name }),
|
|
1109
|
+
hasFeature && /* @__PURE__ */ jsx10(StatusBadge, { label: "Feature", colorClass: "bg-blue-50 text-blue-700 border-blue-200" })
|
|
1110
|
+
] }),
|
|
1111
|
+
product.description && /* @__PURE__ */ jsx10("div", { className: "text-sm text-muted-foreground truncate max-w-xs", children: product.description })
|
|
1112
|
+
] }) }),
|
|
1113
|
+
/* @__PURE__ */ jsx10("td", { className: "p-4", children: /* @__PURE__ */ jsx10(StatusBadge, { label: product.category || "feature", colorClass: "border" }) }),
|
|
1114
|
+
/* @__PURE__ */ jsx10("td", { className: "p-4", children: formatCurrencyFromCents(product.price_cents, product.currency) }),
|
|
1115
|
+
/* @__PURE__ */ jsx10("td", { className: "p-4", children: product.recurring_interval ? /* @__PURE__ */ jsx10(StatusBadge, { label: product.recurring_interval === "month" ? "Mensile" : product.recurring_interval === "year" ? "Annuale" : product.recurring_interval, colorClass: "bg-secondary text-secondary-foreground border-transparent" }) : /* @__PURE__ */ jsx10("span", { className: "text-muted-foreground", children: "Una tantum" }) }),
|
|
1116
|
+
/* @__PURE__ */ jsx10("td", { className: "p-4", children: product.is_active ? /* @__PURE__ */ jsx10(Check2, { className: "w-4 h-4 text-green-600" }) : /* @__PURE__ */ jsx10(X2, { className: "w-4 h-4 text-muted-foreground" }) }),
|
|
1117
|
+
/* @__PURE__ */ jsx10("td", { className: "p-4", children: product.stripe_product_id ? /* @__PURE__ */ jsx10(StatusBadge, { label: "Synced", colorClass: "bg-green-50 text-green-700 border-green-200" }) : /* @__PURE__ */ jsx10("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 hover:bg-accent", onClick: () => syncMutation.mutate(product.id, { onSuccess: () => toast2.success("Sincronizzato"), onError: (err) => toast2.error(`Errore: ${err.message}`) }), disabled: syncMutation.isPending, children: /* @__PURE__ */ jsx10(Upload2, { className: "w-4 h-4" }) }) }),
|
|
1118
|
+
/* @__PURE__ */ jsx10("td", { className: "p-4 text-right", children: /* @__PURE__ */ jsxs7("div", { className: "flex justify-end gap-1", children: [
|
|
1119
|
+
/* @__PURE__ */ jsx10("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 hover:bg-accent", onClick: () => editDialog.open(product), children: /* @__PURE__ */ jsx10(Pencil2, { className: "w-4 h-4" }) }),
|
|
1120
|
+
/* @__PURE__ */ jsx10("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 text-destructive hover:bg-accent", onClick: () => deleteDialog.open(product), children: /* @__PURE__ */ jsx10(Trash22, { className: "w-4 h-4" }) })
|
|
1121
|
+
] }) })
|
|
1122
|
+
] }, product.id);
|
|
1123
|
+
}) })
|
|
1124
|
+
] }) }) })
|
|
1125
|
+
] }),
|
|
1126
|
+
/* @__PURE__ */ jsx10(ProductFormDialog, { isOpen: createDialog.isOpen, onOpenChange: (open) => open ? createDialog.open() : createDialog.close(), mode: "create", features, onSubmit: handleCreateSubmit, isSubmitting: createMutation.isPending }),
|
|
1127
|
+
/* @__PURE__ */ jsx10(ProductFormDialog, { isOpen: editDialog.isOpen, onOpenChange: (open) => open ? editDialog.open(editDialog.data) : editDialog.close(), mode: "edit", product: editDialog.data, features, onSubmit: handleEditSubmit, isSubmitting: updateMutation.isPending }),
|
|
1128
|
+
/* @__PURE__ */ jsx10(DeleteConfirmDialog, { isOpen: deleteDialog.isOpen, onOpenChange: (open) => !open && deleteDialog.close(), title: "Disattivare Prodotto?", description: /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
1129
|
+
"Stai per disattivare il prodotto ",
|
|
1130
|
+
/* @__PURE__ */ jsx10("strong", { children: deleteDialog.data?.name }),
|
|
1131
|
+
". Il prodotto non sar\xE0 pi\xF9 acquistabile."
|
|
1132
|
+
] }), onConfirm: handleDeleteConfirm, isDeleting: deleteMutation.isPending, confirmLabel: "Disattiva Prodotto" })
|
|
1133
|
+
] });
|
|
1134
|
+
return Wrapper ? /* @__PURE__ */ jsx10(Wrapper, { children: content }) : content;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/pages/SubscriptionsPage.tsx
|
|
1138
|
+
import { useState as useState4 } from "react";
|
|
1139
|
+
import { toast as toast3 } from "sonner";
|
|
1140
|
+
import { Eye, XCircle, CreditCard, Calendar, User } from "lucide-react";
|
|
1141
|
+
import {
|
|
1142
|
+
useAdminSubscriptions,
|
|
1143
|
+
useCancelSubscription,
|
|
1144
|
+
formatDate,
|
|
1145
|
+
subscriptionStatusColors,
|
|
1146
|
+
subscriptionStatusLabels
|
|
1147
|
+
} from "@growflowstudio/billing-admin-core";
|
|
1148
|
+
|
|
1149
|
+
// src/components/shared/FilterBar.tsx
|
|
1150
|
+
import { Search, RefreshCw as RefreshCw3 } from "lucide-react";
|
|
1151
|
+
import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1152
|
+
function FilterBar({
|
|
1153
|
+
searchPlaceholder = "Cerca...",
|
|
1154
|
+
searchValue,
|
|
1155
|
+
onSearchChange,
|
|
1156
|
+
statusFilter,
|
|
1157
|
+
onStatusFilterChange,
|
|
1158
|
+
statusOptions,
|
|
1159
|
+
onRefresh
|
|
1160
|
+
}) {
|
|
1161
|
+
return /* @__PURE__ */ jsx11("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: /* @__PURE__ */ jsx11("div", { className: "p-6 pt-6", children: /* @__PURE__ */ jsxs8("div", { className: "flex flex-col sm:flex-row gap-4", children: [
|
|
1162
|
+
/* @__PURE__ */ jsxs8("div", { className: "relative flex-1", children: [
|
|
1163
|
+
/* @__PURE__ */ jsx11(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" }),
|
|
1164
|
+
/* @__PURE__ */ jsx11(
|
|
1165
|
+
"input",
|
|
1166
|
+
{
|
|
1167
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 pl-9",
|
|
1168
|
+
placeholder: searchPlaceholder,
|
|
1169
|
+
value: searchValue,
|
|
1170
|
+
onChange: (e) => onSearchChange(e.target.value)
|
|
1171
|
+
}
|
|
1172
|
+
)
|
|
1173
|
+
] }),
|
|
1174
|
+
statusOptions && onStatusFilterChange && /* @__PURE__ */ jsx11(
|
|
1175
|
+
"select",
|
|
1176
|
+
{
|
|
1177
|
+
className: "flex h-10 w-[180px] rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
1178
|
+
value: statusFilter || "all",
|
|
1179
|
+
onChange: (e) => onStatusFilterChange(e.target.value),
|
|
1180
|
+
children: statusOptions.map((opt) => /* @__PURE__ */ jsx11("option", { value: opt.value, children: opt.label }, opt.value))
|
|
1181
|
+
}
|
|
1182
|
+
),
|
|
1183
|
+
/* @__PURE__ */ jsxs8(
|
|
1184
|
+
"button",
|
|
1185
|
+
{
|
|
1186
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
1187
|
+
onClick: onRefresh,
|
|
1188
|
+
children: [
|
|
1189
|
+
/* @__PURE__ */ jsx11(RefreshCw3, { className: "w-4 h-4 mr-2" }),
|
|
1190
|
+
"Aggiorna"
|
|
1191
|
+
]
|
|
1192
|
+
}
|
|
1193
|
+
)
|
|
1194
|
+
] }) }) });
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/pages/SubscriptionsPage.tsx
|
|
1198
|
+
import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1199
|
+
var statusFilterOptions = [
|
|
1200
|
+
{ value: "all", label: "Tutti gli stati" },
|
|
1201
|
+
{ value: "active", label: "Attivi" },
|
|
1202
|
+
{ value: "trialing", label: "In Prova" },
|
|
1203
|
+
{ value: "past_due", label: "Scaduti" },
|
|
1204
|
+
{ value: "canceled", label: "Cancellati" },
|
|
1205
|
+
{ value: "unpaid", label: "Non Pagati" }
|
|
1206
|
+
];
|
|
1207
|
+
function SubscriptionsPage({ wrapper: Wrapper, header }) {
|
|
1208
|
+
const [search, setSearch] = useState4("");
|
|
1209
|
+
const [statusFilter, setStatusFilter] = useState4("all");
|
|
1210
|
+
const [selectedSub, setSelectedSub] = useState4(null);
|
|
1211
|
+
const [showDetails, setShowDetails] = useState4(false);
|
|
1212
|
+
const [showCancelDialog, setShowCancelDialog] = useState4(false);
|
|
1213
|
+
const [cancelAtPeriodEnd, setCancelAtPeriodEnd] = useState4(true);
|
|
1214
|
+
const { data, isLoading, refetch } = useAdminSubscriptions({
|
|
1215
|
+
search: search || void 0,
|
|
1216
|
+
status: statusFilter !== "all" ? statusFilter : void 0,
|
|
1217
|
+
limit: 100
|
|
1218
|
+
});
|
|
1219
|
+
const cancelMutation = useCancelSubscription();
|
|
1220
|
+
const subscriptions = data?.items || [];
|
|
1221
|
+
const total = data?.total || 0;
|
|
1222
|
+
const handleCancel = () => {
|
|
1223
|
+
if (selectedSub) {
|
|
1224
|
+
cancelMutation.mutate({ id: selectedSub.id, atPeriodEnd: cancelAtPeriodEnd }, {
|
|
1225
|
+
onSuccess: () => {
|
|
1226
|
+
toast3.success("Abbonamento cancellato");
|
|
1227
|
+
setShowCancelDialog(false);
|
|
1228
|
+
setSelectedSub(null);
|
|
1229
|
+
},
|
|
1230
|
+
onError: (err) => toast3.error(`Errore: ${err.message}`)
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
const content = /* @__PURE__ */ jsxs9(Fragment4, { children: [
|
|
1235
|
+
header,
|
|
1236
|
+
/* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
|
|
1237
|
+
/* @__PURE__ */ jsx12(
|
|
1238
|
+
FilterBar,
|
|
1239
|
+
{
|
|
1240
|
+
searchPlaceholder: "Cerca per email o ID esterno...",
|
|
1241
|
+
searchValue: search,
|
|
1242
|
+
onSearchChange: setSearch,
|
|
1243
|
+
statusFilter,
|
|
1244
|
+
onStatusFilterChange: setStatusFilter,
|
|
1245
|
+
statusOptions: statusFilterOptions,
|
|
1246
|
+
onRefresh: refetch
|
|
1247
|
+
}
|
|
1248
|
+
),
|
|
1249
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1250
|
+
/* @__PURE__ */ jsx12(CreditCard, { className: "w-4 h-4" }),
|
|
1251
|
+
/* @__PURE__ */ jsxs9("span", { children: [
|
|
1252
|
+
total,
|
|
1253
|
+
" abbonament",
|
|
1254
|
+
total === 1 ? "o" : "i",
|
|
1255
|
+
" trovati"
|
|
1256
|
+
] })
|
|
1257
|
+
] }),
|
|
1258
|
+
/* @__PURE__ */ jsx12("div", { className: "rounded-lg border bg-card shadow-sm", children: /* @__PURE__ */ jsx12("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsxs9("table", { className: "w-full caption-bottom text-sm", children: [
|
|
1259
|
+
/* @__PURE__ */ jsx12("thead", { className: "[&_tr]:border-b", children: /* @__PURE__ */ jsxs9("tr", { className: "border-b hover:bg-muted/50", children: [
|
|
1260
|
+
/* @__PURE__ */ jsx12("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Subscriber" }),
|
|
1261
|
+
/* @__PURE__ */ jsx12("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Piano" }),
|
|
1262
|
+
/* @__PURE__ */ jsx12("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Stato" }),
|
|
1263
|
+
/* @__PURE__ */ jsx12("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Ciclo" }),
|
|
1264
|
+
/* @__PURE__ */ jsx12("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Periodo" }),
|
|
1265
|
+
/* @__PURE__ */ jsx12("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Trial End" }),
|
|
1266
|
+
/* @__PURE__ */ jsx12("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Creato" }),
|
|
1267
|
+
/* @__PURE__ */ jsx12("th", { className: "h-12 px-4 text-right font-medium text-muted-foreground", children: "Azioni" })
|
|
1268
|
+
] }) }),
|
|
1269
|
+
/* @__PURE__ */ jsx12("tbody", { children: isLoading ? /* @__PURE__ */ jsx12(SkeletonRows, { rows: 5, columns: 8 }) : subscriptions.length === 0 ? /* @__PURE__ */ jsx12("tr", { children: /* @__PURE__ */ jsx12("td", { colSpan: 8, className: "p-4 text-center py-8 text-muted-foreground", children: "Nessun abbonamento trovato" }) }) : subscriptions.map((sub) => /* @__PURE__ */ jsxs9("tr", { className: "border-b hover:bg-muted/50", children: [
|
|
1270
|
+
/* @__PURE__ */ jsx12("td", { className: "p-4", children: /* @__PURE__ */ jsxs9("div", { className: "flex flex-col", children: [
|
|
1271
|
+
/* @__PURE__ */ jsx12("span", { className: "font-medium", children: sub.subscriber_email }),
|
|
1272
|
+
/* @__PURE__ */ jsx12("span", { className: "text-xs text-muted-foreground", children: sub.subscriber_external_id })
|
|
1273
|
+
] }) }),
|
|
1274
|
+
/* @__PURE__ */ jsxs9("td", { className: "p-4", children: [
|
|
1275
|
+
/* @__PURE__ */ jsx12("span", { className: "font-medium", children: sub.plan_name }),
|
|
1276
|
+
/* @__PURE__ */ jsxs9("span", { className: "text-xs text-muted-foreground ml-1", children: [
|
|
1277
|
+
"(",
|
|
1278
|
+
sub.plan_slug,
|
|
1279
|
+
")"
|
|
1280
|
+
] })
|
|
1281
|
+
] }),
|
|
1282
|
+
/* @__PURE__ */ jsx12("td", { className: "p-4", children: /* @__PURE__ */ jsx12(StatusBadge, { label: subscriptionStatusLabels[sub.status], colorClass: subscriptionStatusColors[sub.status] }) }),
|
|
1283
|
+
/* @__PURE__ */ jsx12("td", { className: "p-4 capitalize", children: sub.billing_cycle }),
|
|
1284
|
+
/* @__PURE__ */ jsxs9("td", { className: "p-4 text-sm", children: [
|
|
1285
|
+
formatDate(sub.current_period_start),
|
|
1286
|
+
" - ",
|
|
1287
|
+
formatDate(sub.current_period_end)
|
|
1288
|
+
] }),
|
|
1289
|
+
/* @__PURE__ */ jsx12("td", { className: "p-4", children: formatDate(sub.trial_end) }),
|
|
1290
|
+
/* @__PURE__ */ jsx12("td", { className: "p-4", children: formatDate(sub.created_at) }),
|
|
1291
|
+
/* @__PURE__ */ jsx12("td", { className: "p-4 text-right", children: /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-1", children: [
|
|
1292
|
+
/* @__PURE__ */ jsx12("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 hover:bg-accent", onClick: () => {
|
|
1293
|
+
setSelectedSub(sub);
|
|
1294
|
+
setShowDetails(true);
|
|
1295
|
+
}, children: /* @__PURE__ */ jsx12(Eye, { className: "w-4 h-4" }) }),
|
|
1296
|
+
sub.status !== "canceled" && /* @__PURE__ */ jsx12("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 text-destructive hover:bg-accent", onClick: () => {
|
|
1297
|
+
setSelectedSub(sub);
|
|
1298
|
+
setShowCancelDialog(true);
|
|
1299
|
+
}, children: /* @__PURE__ */ jsx12(XCircle, { className: "w-4 h-4" }) })
|
|
1300
|
+
] }) })
|
|
1301
|
+
] }, sub.id)) })
|
|
1302
|
+
] }) }) })
|
|
1303
|
+
] }),
|
|
1304
|
+
showDetails && selectedSub && /* @__PURE__ */ jsxs9("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1305
|
+
/* @__PURE__ */ jsx12("div", { className: "fixed inset-0 bg-black/80", onClick: () => setShowDetails(false) }),
|
|
1306
|
+
/* @__PURE__ */ jsxs9("div", { className: "relative z-50 w-full max-w-lg rounded-lg border bg-background p-6 shadow-lg", children: [
|
|
1307
|
+
/* @__PURE__ */ jsx12("h2", { className: "text-lg font-semibold mb-1", children: "Dettagli Abbonamento" }),
|
|
1308
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground mb-4", children: "Informazioni complete" }),
|
|
1309
|
+
/* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
|
|
1310
|
+
/* @__PURE__ */ jsxs9("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
1311
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1312
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Subscriber" }),
|
|
1313
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 mt-1", children: [
|
|
1314
|
+
/* @__PURE__ */ jsx12(User, { className: "w-4 h-4 text-muted-foreground" }),
|
|
1315
|
+
/* @__PURE__ */ jsx12("span", { className: "font-medium", children: selectedSub.subscriber_email })
|
|
1316
|
+
] }),
|
|
1317
|
+
/* @__PURE__ */ jsxs9("p", { className: "text-xs text-muted-foreground mt-1", children: [
|
|
1318
|
+
"ID: ",
|
|
1319
|
+
selectedSub.subscriber_external_id
|
|
1320
|
+
] })
|
|
1321
|
+
] }),
|
|
1322
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1323
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Piano" }),
|
|
1324
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 mt-1", children: [
|
|
1325
|
+
/* @__PURE__ */ jsx12(CreditCard, { className: "w-4 h-4 text-muted-foreground" }),
|
|
1326
|
+
/* @__PURE__ */ jsx12("span", { className: "font-medium", children: selectedSub.plan_name })
|
|
1327
|
+
] })
|
|
1328
|
+
] })
|
|
1329
|
+
] }),
|
|
1330
|
+
/* @__PURE__ */ jsxs9("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
1331
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1332
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Stato" }),
|
|
1333
|
+
/* @__PURE__ */ jsx12(StatusBadge, { label: subscriptionStatusLabels[selectedSub.status], colorClass: subscriptionStatusColors[selectedSub.status], className: "mt-1" })
|
|
1334
|
+
] }),
|
|
1335
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1336
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Ciclo" }),
|
|
1337
|
+
/* @__PURE__ */ jsx12("p", { className: "font-medium capitalize mt-1", children: selectedSub.billing_cycle })
|
|
1338
|
+
] })
|
|
1339
|
+
] }),
|
|
1340
|
+
/* @__PURE__ */ jsxs9("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
1341
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1342
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Periodo" }),
|
|
1343
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 mt-1", children: [
|
|
1344
|
+
/* @__PURE__ */ jsx12(Calendar, { className: "w-4 h-4 text-muted-foreground" }),
|
|
1345
|
+
/* @__PURE__ */ jsxs9("span", { children: [
|
|
1346
|
+
formatDate(selectedSub.current_period_start),
|
|
1347
|
+
" - ",
|
|
1348
|
+
formatDate(selectedSub.current_period_end)
|
|
1349
|
+
] })
|
|
1350
|
+
] })
|
|
1351
|
+
] }),
|
|
1352
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1353
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Fine Trial" }),
|
|
1354
|
+
/* @__PURE__ */ jsx12("p", { className: "mt-1", children: formatDate(selectedSub.trial_end) })
|
|
1355
|
+
] })
|
|
1356
|
+
] }),
|
|
1357
|
+
selectedSub.canceled_at && /* @__PURE__ */ jsxs9("div", { className: "p-3 bg-destructive/10 rounded-lg", children: [
|
|
1358
|
+
/* @__PURE__ */ jsxs9("p", { className: "text-sm text-destructive font-medium", children: [
|
|
1359
|
+
"Cancellato il ",
|
|
1360
|
+
formatDate(selectedSub.canceled_at)
|
|
1361
|
+
] }),
|
|
1362
|
+
selectedSub.cancel_at && /* @__PURE__ */ jsxs9("p", { className: "text-xs text-destructive/80 mt-1", children: [
|
|
1363
|
+
"Termina il ",
|
|
1364
|
+
formatDate(selectedSub.cancel_at)
|
|
1365
|
+
] })
|
|
1366
|
+
] }),
|
|
1367
|
+
selectedSub.stripe_subscription_id && /* @__PURE__ */ jsxs9("div", { children: [
|
|
1368
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground", children: "Stripe ID" }),
|
|
1369
|
+
/* @__PURE__ */ jsx12("code", { className: "text-xs bg-muted px-2 py-1 rounded mt-1 inline-block", children: selectedSub.stripe_subscription_id })
|
|
1370
|
+
] })
|
|
1371
|
+
] }),
|
|
1372
|
+
/* @__PURE__ */ jsx12("div", { className: "flex justify-end pt-4", children: /* @__PURE__ */ jsx12("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent", onClick: () => setShowDetails(false), children: "Chiudi" }) })
|
|
1373
|
+
] })
|
|
1374
|
+
] }),
|
|
1375
|
+
showCancelDialog && selectedSub && /* @__PURE__ */ jsxs9("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1376
|
+
/* @__PURE__ */ jsx12("div", { className: "fixed inset-0 bg-black/80", onClick: () => setShowCancelDialog(false) }),
|
|
1377
|
+
/* @__PURE__ */ jsxs9("div", { className: "relative z-50 w-full max-w-lg rounded-lg border bg-background p-6 shadow-lg", children: [
|
|
1378
|
+
/* @__PURE__ */ jsx12("h2", { className: "text-lg font-semibold", children: "Cancellare Abbonamento?" }),
|
|
1379
|
+
/* @__PURE__ */ jsxs9("p", { className: "text-sm text-muted-foreground mt-1", children: [
|
|
1380
|
+
"Stai per cancellare l'abbonamento di ",
|
|
1381
|
+
/* @__PURE__ */ jsx12("strong", { children: selectedSub.subscriber_email }),
|
|
1382
|
+
"."
|
|
1383
|
+
] }),
|
|
1384
|
+
/* @__PURE__ */ jsxs9("div", { className: "py-4", children: [
|
|
1385
|
+
/* @__PURE__ */ jsxs9("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
1386
|
+
/* @__PURE__ */ jsx12("input", { type: "checkbox", checked: cancelAtPeriodEnd, onChange: (e) => setCancelAtPeriodEnd(e.target.checked), className: "rounded border-input" }),
|
|
1387
|
+
/* @__PURE__ */ jsx12("span", { className: "text-sm", children: "Cancella alla fine del periodo corrente (consigliato)" })
|
|
1388
|
+
] }),
|
|
1389
|
+
/* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground mt-2", children: cancelAtPeriodEnd ? "L'abbonamento rimarr\xE0 attivo fino alla fine del periodo." : "L'abbonamento verr\xE0 cancellato immediatamente." })
|
|
1390
|
+
] }),
|
|
1391
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2", children: [
|
|
1392
|
+
/* @__PURE__ */ jsx12("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent", onClick: () => setShowCancelDialog(false), children: "Annulla" }),
|
|
1393
|
+
/* @__PURE__ */ jsx12("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-destructive text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50", onClick: handleCancel, disabled: cancelMutation.isPending, children: cancelMutation.isPending ? "Cancellazione..." : "Conferma Cancellazione" })
|
|
1394
|
+
] })
|
|
1395
|
+
] })
|
|
1396
|
+
] })
|
|
1397
|
+
] });
|
|
1398
|
+
return Wrapper ? /* @__PURE__ */ jsx12(Wrapper, { children: content }) : content;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/pages/PaymentsPage.tsx
|
|
1402
|
+
import { useState as useState5 } from "react";
|
|
1403
|
+
import { toast as toast4 } from "sonner";
|
|
1404
|
+
import { RotateCcw, Receipt } from "lucide-react";
|
|
1405
|
+
import {
|
|
1406
|
+
useAdminPayments,
|
|
1407
|
+
useRefundPayment,
|
|
1408
|
+
formatCurrencyFromCents as formatCurrencyFromCents2,
|
|
1409
|
+
formatTimestamp,
|
|
1410
|
+
truncateId,
|
|
1411
|
+
paymentStatusColors,
|
|
1412
|
+
paymentStatusLabels
|
|
1413
|
+
} from "@growflowstudio/billing-admin-core";
|
|
1414
|
+
|
|
1415
|
+
// src/components/shared/CursorPagination.tsx
|
|
1416
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
1417
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1418
|
+
function CursorPagination({
|
|
1419
|
+
cursor,
|
|
1420
|
+
hasMore,
|
|
1421
|
+
items,
|
|
1422
|
+
onFirstPage,
|
|
1423
|
+
onNextPage
|
|
1424
|
+
}) {
|
|
1425
|
+
return /* @__PURE__ */ jsxs10("div", { className: "flex justify-between items-center", children: [
|
|
1426
|
+
/* @__PURE__ */ jsxs10(
|
|
1427
|
+
"button",
|
|
1428
|
+
{
|
|
1429
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none",
|
|
1430
|
+
onClick: onFirstPage,
|
|
1431
|
+
disabled: !cursor,
|
|
1432
|
+
children: [
|
|
1433
|
+
/* @__PURE__ */ jsx13(ChevronLeft, { className: "w-4 h-4 mr-1" }),
|
|
1434
|
+
"Prima Pagina"
|
|
1435
|
+
]
|
|
1436
|
+
}
|
|
1437
|
+
),
|
|
1438
|
+
/* @__PURE__ */ jsxs10(
|
|
1439
|
+
"button",
|
|
1440
|
+
{
|
|
1441
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none",
|
|
1442
|
+
onClick: () => {
|
|
1443
|
+
if (items.length > 0) {
|
|
1444
|
+
onNextPage(items[items.length - 1].id);
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
disabled: !hasMore,
|
|
1448
|
+
children: [
|
|
1449
|
+
"Prossima",
|
|
1450
|
+
/* @__PURE__ */ jsx13(ChevronRight, { className: "w-4 h-4 ml-1" })
|
|
1451
|
+
]
|
|
1452
|
+
}
|
|
1453
|
+
)
|
|
1454
|
+
] });
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/pages/PaymentsPage.tsx
|
|
1458
|
+
import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1459
|
+
var statusFilterOptions2 = [
|
|
1460
|
+
{ value: "all", label: "Tutti gli stati" },
|
|
1461
|
+
{ value: "succeeded", label: "Completati" },
|
|
1462
|
+
{ value: "pending", label: "In Attesa" },
|
|
1463
|
+
{ value: "failed", label: "Falliti" },
|
|
1464
|
+
{ value: "canceled", label: "Annullati" }
|
|
1465
|
+
];
|
|
1466
|
+
function PaymentsPage({ wrapper: Wrapper, header }) {
|
|
1467
|
+
const [search, setSearch] = useState5("");
|
|
1468
|
+
const [statusFilter, setStatusFilter] = useState5("all");
|
|
1469
|
+
const [cursor, setCursor] = useState5();
|
|
1470
|
+
const [selectedPayment, setSelectedPayment] = useState5(null);
|
|
1471
|
+
const [showRefundDialog, setShowRefundDialog] = useState5(false);
|
|
1472
|
+
const [refundAmount, setRefundAmount] = useState5("");
|
|
1473
|
+
const [refundReason, setRefundReason] = useState5("requested_by_customer");
|
|
1474
|
+
const { data, isLoading, refetch } = useAdminPayments({
|
|
1475
|
+
search: search || void 0,
|
|
1476
|
+
status: statusFilter !== "all" ? statusFilter : void 0,
|
|
1477
|
+
limit: 25,
|
|
1478
|
+
starting_after: cursor
|
|
1479
|
+
});
|
|
1480
|
+
const refundMutation = useRefundPayment();
|
|
1481
|
+
const payments = data?.data || [];
|
|
1482
|
+
const hasMore = data?.has_more || false;
|
|
1483
|
+
const handleRefund = () => {
|
|
1484
|
+
if (!selectedPayment) return;
|
|
1485
|
+
const amountCents = refundAmount ? Math.round(parseFloat(refundAmount) * 100) : void 0;
|
|
1486
|
+
refundMutation.mutate({ id: selectedPayment.id, amount: amountCents, reason: refundReason }, {
|
|
1487
|
+
onSuccess: () => {
|
|
1488
|
+
toast4.success("Rimborso effettuato");
|
|
1489
|
+
setShowRefundDialog(false);
|
|
1490
|
+
setSelectedPayment(null);
|
|
1491
|
+
setRefundAmount("");
|
|
1492
|
+
},
|
|
1493
|
+
onError: (err) => toast4.error(`Errore rimborso: ${err.message}`)
|
|
1494
|
+
});
|
|
1495
|
+
};
|
|
1496
|
+
const openRefundDialog = (payment) => {
|
|
1497
|
+
setSelectedPayment(payment);
|
|
1498
|
+
setRefundAmount((payment.amount / 100).toFixed(2));
|
|
1499
|
+
setShowRefundDialog(true);
|
|
1500
|
+
};
|
|
1501
|
+
const content = /* @__PURE__ */ jsxs11(Fragment5, { children: [
|
|
1502
|
+
header,
|
|
1503
|
+
/* @__PURE__ */ jsxs11("div", { className: "space-y-4", children: [
|
|
1504
|
+
/* @__PURE__ */ jsx14(
|
|
1505
|
+
FilterBar,
|
|
1506
|
+
{
|
|
1507
|
+
searchPlaceholder: "Cerca per email cliente...",
|
|
1508
|
+
searchValue: search,
|
|
1509
|
+
onSearchChange: (v) => {
|
|
1510
|
+
setSearch(v);
|
|
1511
|
+
setCursor(void 0);
|
|
1512
|
+
},
|
|
1513
|
+
statusFilter,
|
|
1514
|
+
onStatusFilterChange: (v) => {
|
|
1515
|
+
setStatusFilter(v);
|
|
1516
|
+
setCursor(void 0);
|
|
1517
|
+
},
|
|
1518
|
+
statusOptions: statusFilterOptions2,
|
|
1519
|
+
onRefresh: refetch
|
|
1520
|
+
}
|
|
1521
|
+
),
|
|
1522
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1523
|
+
/* @__PURE__ */ jsx14(Receipt, { className: "w-4 h-4" }),
|
|
1524
|
+
/* @__PURE__ */ jsxs11("span", { children: [
|
|
1525
|
+
payments.length,
|
|
1526
|
+
" pagamenti visualizzati"
|
|
1527
|
+
] })
|
|
1528
|
+
] }),
|
|
1529
|
+
/* @__PURE__ */ jsx14("div", { className: "rounded-lg border bg-card shadow-sm", children: /* @__PURE__ */ jsx14("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsxs11("table", { className: "w-full caption-bottom text-sm", children: [
|
|
1530
|
+
/* @__PURE__ */ jsx14("thead", { className: "[&_tr]:border-b", children: /* @__PURE__ */ jsxs11("tr", { className: "border-b hover:bg-muted/50", children: [
|
|
1531
|
+
/* @__PURE__ */ jsx14("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "ID Pagamento" }),
|
|
1532
|
+
/* @__PURE__ */ jsx14("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Importo" }),
|
|
1533
|
+
/* @__PURE__ */ jsx14("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Stato" }),
|
|
1534
|
+
/* @__PURE__ */ jsx14("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Cliente" }),
|
|
1535
|
+
/* @__PURE__ */ jsx14("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Descrizione" }),
|
|
1536
|
+
/* @__PURE__ */ jsx14("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Data" }),
|
|
1537
|
+
/* @__PURE__ */ jsx14("th", { className: "h-12 px-4 text-right font-medium text-muted-foreground", children: "Azioni" })
|
|
1538
|
+
] }) }),
|
|
1539
|
+
/* @__PURE__ */ jsx14("tbody", { children: isLoading ? /* @__PURE__ */ jsx14(SkeletonRows, { rows: 5, columns: 7 }) : payments.length === 0 ? /* @__PURE__ */ jsx14("tr", { children: /* @__PURE__ */ jsx14("td", { colSpan: 7, className: "p-4 text-center py-8 text-muted-foreground", children: "Nessun pagamento trovato" }) }) : payments.map((payment) => /* @__PURE__ */ jsxs11("tr", { className: "border-b hover:bg-muted/50", children: [
|
|
1540
|
+
/* @__PURE__ */ jsx14("td", { className: "p-4", children: /* @__PURE__ */ jsx14("code", { className: "text-xs bg-muted px-2 py-0.5 rounded", children: truncateId(payment.id) }) }),
|
|
1541
|
+
/* @__PURE__ */ jsx14("td", { className: "p-4 font-medium", children: formatCurrencyFromCents2(payment.amount, payment.currency) }),
|
|
1542
|
+
/* @__PURE__ */ jsx14("td", { className: "p-4", children: /* @__PURE__ */ jsx14(StatusBadge, { label: paymentStatusLabels[payment.status], colorClass: paymentStatusColors[payment.status] }) }),
|
|
1543
|
+
/* @__PURE__ */ jsx14("td", { className: "p-4", children: payment.customer_email || /* @__PURE__ */ jsx14("span", { className: "text-muted-foreground", children: "-" }) }),
|
|
1544
|
+
/* @__PURE__ */ jsx14("td", { className: "p-4 max-w-[200px] truncate", children: payment.description || /* @__PURE__ */ jsx14("span", { className: "text-muted-foreground", children: "-" }) }),
|
|
1545
|
+
/* @__PURE__ */ jsx14("td", { className: "p-4", children: formatTimestamp(payment.created, true) }),
|
|
1546
|
+
/* @__PURE__ */ jsx14("td", { className: "p-4 text-right", children: payment.status === "succeeded" && /* @__PURE__ */ jsxs11("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 hover:bg-accent", onClick: () => openRefundDialog(payment), children: [
|
|
1547
|
+
/* @__PURE__ */ jsx14(RotateCcw, { className: "w-4 h-4 mr-1" }),
|
|
1548
|
+
"Rimborsa"
|
|
1549
|
+
] }) })
|
|
1550
|
+
] }, payment.id)) })
|
|
1551
|
+
] }) }) }),
|
|
1552
|
+
/* @__PURE__ */ jsx14(CursorPagination, { cursor, hasMore, items: payments, onFirstPage: () => setCursor(void 0), onNextPage: setCursor })
|
|
1553
|
+
] }),
|
|
1554
|
+
showRefundDialog && selectedPayment && /* @__PURE__ */ jsxs11("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1555
|
+
/* @__PURE__ */ jsx14("div", { className: "fixed inset-0 bg-black/80", onClick: () => setShowRefundDialog(false) }),
|
|
1556
|
+
/* @__PURE__ */ jsxs11("div", { className: "relative z-50 w-full max-w-md rounded-lg border bg-background p-6 shadow-lg", children: [
|
|
1557
|
+
/* @__PURE__ */ jsx14("h2", { className: "text-lg font-semibold", children: "Rimborso Pagamento" }),
|
|
1558
|
+
/* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground mt-1", children: "Effettua un rimborso per il pagamento selezionato" }),
|
|
1559
|
+
/* @__PURE__ */ jsxs11("div", { className: "space-y-4 py-4", children: [
|
|
1560
|
+
/* @__PURE__ */ jsxs11("div", { className: "p-3 bg-muted rounded-lg", children: [
|
|
1561
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex justify-between items-center", children: [
|
|
1562
|
+
/* @__PURE__ */ jsx14("span", { className: "text-sm text-muted-foreground", children: "Pagamento originale" }),
|
|
1563
|
+
/* @__PURE__ */ jsx14("span", { className: "font-medium", children: formatCurrencyFromCents2(selectedPayment.amount, selectedPayment.currency) })
|
|
1564
|
+
] }),
|
|
1565
|
+
selectedPayment.customer_email && /* @__PURE__ */ jsxs11("p", { className: "text-sm text-muted-foreground mt-1", children: [
|
|
1566
|
+
"Cliente: ",
|
|
1567
|
+
selectedPayment.customer_email
|
|
1568
|
+
] })
|
|
1569
|
+
] }),
|
|
1570
|
+
/* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
|
|
1571
|
+
/* @__PURE__ */ jsx14("label", { className: "text-sm font-medium leading-none", children: "Importo Rimborso" }),
|
|
1572
|
+
/* @__PURE__ */ jsx14("input", { type: "number", step: "0.01", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "Lascia vuoto per rimborso totale" })
|
|
1573
|
+
] }),
|
|
1574
|
+
/* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
|
|
1575
|
+
/* @__PURE__ */ jsx14("label", { className: "text-sm font-medium leading-none", children: "Motivo" }),
|
|
1576
|
+
/* @__PURE__ */ jsxs11("select", { className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", value: refundReason, onChange: (e) => setRefundReason(e.target.value), children: [
|
|
1577
|
+
/* @__PURE__ */ jsx14("option", { value: "requested_by_customer", children: "Richiesto dal cliente" }),
|
|
1578
|
+
/* @__PURE__ */ jsx14("option", { value: "duplicate", children: "Pagamento duplicato" }),
|
|
1579
|
+
/* @__PURE__ */ jsx14("option", { value: "fraudulent", children: "Fraudolento" })
|
|
1580
|
+
] })
|
|
1581
|
+
] })
|
|
1582
|
+
] }),
|
|
1583
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex justify-end gap-2", children: [
|
|
1584
|
+
/* @__PURE__ */ jsx14("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent", onClick: () => setShowRefundDialog(false), children: "Annulla" }),
|
|
1585
|
+
/* @__PURE__ */ jsx14("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-destructive text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50", onClick: handleRefund, disabled: refundMutation.isPending, children: refundMutation.isPending ? "Rimborso in corso..." : "Conferma Rimborso" })
|
|
1586
|
+
] })
|
|
1587
|
+
] })
|
|
1588
|
+
] })
|
|
1589
|
+
] });
|
|
1590
|
+
return Wrapper ? /* @__PURE__ */ jsx14(Wrapper, { children: content }) : content;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/pages/InvoicesPage.tsx
|
|
1594
|
+
import { useState as useState6 } from "react";
|
|
1595
|
+
import { toast as toast5 } from "sonner";
|
|
1596
|
+
import { Eye as Eye2, Send, XCircle as XCircle2, Download, FileText } from "lucide-react";
|
|
1597
|
+
import {
|
|
1598
|
+
useAdminInvoices,
|
|
1599
|
+
useSendInvoice,
|
|
1600
|
+
useVoidInvoice,
|
|
1601
|
+
useDialogState as useDialogState2,
|
|
1602
|
+
formatCurrencyFromCents as formatCurrencyFromCents4,
|
|
1603
|
+
formatTimestamp as formatTimestamp3,
|
|
1604
|
+
invoiceStatusColors as invoiceStatusColors2,
|
|
1605
|
+
invoiceStatusLabels as invoiceStatusLabels2
|
|
1606
|
+
} from "@growflowstudio/billing-admin-core";
|
|
1607
|
+
|
|
1608
|
+
// src/components/invoices/InvoiceDetailsDialog.tsx
|
|
1609
|
+
import { ExternalLink } from "lucide-react";
|
|
1610
|
+
import { formatCurrencyFromCents as formatCurrencyFromCents3, formatTimestamp as formatTimestamp2, invoiceStatusColors, invoiceStatusLabels } from "@growflowstudio/billing-admin-core";
|
|
1611
|
+
import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1612
|
+
function InvoiceDetailsDialog({ isOpen, onOpenChange, invoice }) {
|
|
1613
|
+
if (!isOpen || !invoice) return null;
|
|
1614
|
+
return /* @__PURE__ */ jsxs12("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1615
|
+
/* @__PURE__ */ jsx15("div", { className: "fixed inset-0 bg-black/80", onClick: () => onOpenChange(false) }),
|
|
1616
|
+
/* @__PURE__ */ jsxs12("div", { className: "relative z-50 w-full max-w-lg rounded-lg border bg-background p-6 shadow-lg", children: [
|
|
1617
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex flex-col space-y-1.5 pb-4", children: [
|
|
1618
|
+
/* @__PURE__ */ jsx15("h2", { className: "text-lg font-semibold", children: "Dettagli Fattura" }),
|
|
1619
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: invoice.number || "Fattura senza numero" })
|
|
1620
|
+
] }),
|
|
1621
|
+
/* @__PURE__ */ jsxs12("div", { className: "space-y-4", children: [
|
|
1622
|
+
/* @__PURE__ */ jsxs12("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
1623
|
+
/* @__PURE__ */ jsxs12("div", { children: [
|
|
1624
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Stato" }),
|
|
1625
|
+
/* @__PURE__ */ jsx15(StatusBadge, { label: invoiceStatusLabels[invoice.status], colorClass: invoiceStatusColors[invoice.status], className: "mt-1" })
|
|
1626
|
+
] }),
|
|
1627
|
+
/* @__PURE__ */ jsxs12("div", { children: [
|
|
1628
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Data Creazione" }),
|
|
1629
|
+
/* @__PURE__ */ jsx15("p", { className: "font-medium mt-1", children: formatTimestamp2(invoice.created) })
|
|
1630
|
+
] })
|
|
1631
|
+
] }),
|
|
1632
|
+
/* @__PURE__ */ jsxs12("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
1633
|
+
/* @__PURE__ */ jsxs12("div", { children: [
|
|
1634
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Importo Dovuto" }),
|
|
1635
|
+
/* @__PURE__ */ jsx15("p", { className: "font-medium mt-1", children: formatCurrencyFromCents3(invoice.amount_due, invoice.currency) })
|
|
1636
|
+
] }),
|
|
1637
|
+
/* @__PURE__ */ jsxs12("div", { children: [
|
|
1638
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Importo Pagato" }),
|
|
1639
|
+
/* @__PURE__ */ jsx15("p", { className: "font-medium mt-1", children: formatCurrencyFromCents3(invoice.amount_paid, invoice.currency) })
|
|
1640
|
+
] })
|
|
1641
|
+
] }),
|
|
1642
|
+
invoice.amount_remaining > 0 && /* @__PURE__ */ jsx15("div", { className: "p-3 bg-amber-50 dark:bg-amber-900/20 rounded-lg", children: /* @__PURE__ */ jsxs12("p", { className: "text-sm text-amber-700 dark:text-amber-400", children: [
|
|
1643
|
+
"Importo rimanente: ",
|
|
1644
|
+
/* @__PURE__ */ jsx15("strong", { children: formatCurrencyFromCents3(invoice.amount_remaining, invoice.currency) })
|
|
1645
|
+
] }) }),
|
|
1646
|
+
/* @__PURE__ */ jsxs12("div", { children: [
|
|
1647
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Cliente" }),
|
|
1648
|
+
/* @__PURE__ */ jsx15("p", { className: "font-medium mt-1", children: invoice.customer_name || "Non specificato" }),
|
|
1649
|
+
invoice.customer_email && /* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: invoice.customer_email })
|
|
1650
|
+
] }),
|
|
1651
|
+
invoice.lines && invoice.lines.length > 0 && /* @__PURE__ */ jsxs12("div", { children: [
|
|
1652
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground mb-2", children: "Voci Fattura" }),
|
|
1653
|
+
/* @__PURE__ */ jsx15("div", { className: "border rounded-lg divide-y", children: invoice.lines.map((line) => /* @__PURE__ */ jsxs12("div", { className: "p-3 flex justify-between items-center", children: [
|
|
1654
|
+
/* @__PURE__ */ jsxs12("div", { children: [
|
|
1655
|
+
/* @__PURE__ */ jsx15("p", { className: "text-sm font-medium", children: line.description }),
|
|
1656
|
+
/* @__PURE__ */ jsxs12("p", { className: "text-xs text-muted-foreground", children: [
|
|
1657
|
+
"Qt\xE0: ",
|
|
1658
|
+
line.quantity
|
|
1659
|
+
] })
|
|
1660
|
+
] }),
|
|
1661
|
+
/* @__PURE__ */ jsx15("p", { className: "font-medium", children: formatCurrencyFromCents3(line.amount, line.currency) })
|
|
1662
|
+
] }, line.id)) })
|
|
1663
|
+
] }),
|
|
1664
|
+
invoice.hosted_invoice_url && /* @__PURE__ */ jsxs12(
|
|
1665
|
+
"button",
|
|
1666
|
+
{
|
|
1667
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent w-full",
|
|
1668
|
+
onClick: () => window.open(invoice.hosted_invoice_url, "_blank"),
|
|
1669
|
+
children: [
|
|
1670
|
+
/* @__PURE__ */ jsx15(ExternalLink, { className: "w-4 h-4 mr-2" }),
|
|
1671
|
+
"Apri su Stripe"
|
|
1672
|
+
]
|
|
1673
|
+
}
|
|
1674
|
+
)
|
|
1675
|
+
] }),
|
|
1676
|
+
/* @__PURE__ */ jsx15("div", { className: "flex justify-end pt-4", children: /* @__PURE__ */ jsx15("button", { className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent", onClick: () => onOpenChange(false), children: "Chiudi" }) })
|
|
1677
|
+
] })
|
|
1678
|
+
] });
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/pages/InvoicesPage.tsx
|
|
1682
|
+
import { Fragment as Fragment6, jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1683
|
+
var statusFilterOptions3 = [
|
|
1684
|
+
{ value: "all", label: "Tutti gli stati" },
|
|
1685
|
+
{ value: "draft", label: "Bozze" },
|
|
1686
|
+
{ value: "open", label: "Aperte" },
|
|
1687
|
+
{ value: "paid", label: "Pagate" },
|
|
1688
|
+
{ value: "void", label: "Annullate" },
|
|
1689
|
+
{ value: "uncollectible", label: "Non Riscuotibili" }
|
|
1690
|
+
];
|
|
1691
|
+
function InvoicesPage({ wrapper: Wrapper, header }) {
|
|
1692
|
+
const [search, setSearch] = useState6("");
|
|
1693
|
+
const [statusFilter, setStatusFilter] = useState6("all");
|
|
1694
|
+
const [cursor, setCursor] = useState6();
|
|
1695
|
+
const detailsDialog = useDialogState2();
|
|
1696
|
+
const voidDialog = useDialogState2();
|
|
1697
|
+
const { data, isLoading, refetch } = useAdminInvoices({
|
|
1698
|
+
search: search || void 0,
|
|
1699
|
+
status: statusFilter !== "all" ? statusFilter : void 0,
|
|
1700
|
+
limit: 25,
|
|
1701
|
+
starting_after: cursor
|
|
1702
|
+
});
|
|
1703
|
+
const sendMutation = useSendInvoice();
|
|
1704
|
+
const voidMutation = useVoidInvoice();
|
|
1705
|
+
const invoices = data?.data || [];
|
|
1706
|
+
const hasMore = data?.has_more || false;
|
|
1707
|
+
const handleVoidConfirm = () => {
|
|
1708
|
+
if (voidDialog.data) {
|
|
1709
|
+
voidMutation.mutate(voidDialog.data.id, {
|
|
1710
|
+
onSuccess: () => {
|
|
1711
|
+
toast5.success("Fattura annullata");
|
|
1712
|
+
voidDialog.close();
|
|
1713
|
+
},
|
|
1714
|
+
onError: (err) => toast5.error(`Errore: ${err.message}`)
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
const openInvoicePdf = (invoice) => {
|
|
1719
|
+
if (invoice.invoice_pdf) window.open(invoice.invoice_pdf, "_blank");
|
|
1720
|
+
else if (invoice.hosted_invoice_url) window.open(invoice.hosted_invoice_url, "_blank");
|
|
1721
|
+
};
|
|
1722
|
+
const content = /* @__PURE__ */ jsxs13(Fragment6, { children: [
|
|
1723
|
+
header,
|
|
1724
|
+
/* @__PURE__ */ jsxs13("div", { className: "space-y-4", children: [
|
|
1725
|
+
/* @__PURE__ */ jsx16(
|
|
1726
|
+
FilterBar,
|
|
1727
|
+
{
|
|
1728
|
+
searchPlaceholder: "Cerca per email o numero fattura...",
|
|
1729
|
+
searchValue: search,
|
|
1730
|
+
onSearchChange: (v) => {
|
|
1731
|
+
setSearch(v);
|
|
1732
|
+
setCursor(void 0);
|
|
1733
|
+
},
|
|
1734
|
+
statusFilter,
|
|
1735
|
+
onStatusFilterChange: (v) => {
|
|
1736
|
+
setStatusFilter(v);
|
|
1737
|
+
setCursor(void 0);
|
|
1738
|
+
},
|
|
1739
|
+
statusOptions: statusFilterOptions3,
|
|
1740
|
+
onRefresh: refetch
|
|
1741
|
+
}
|
|
1742
|
+
),
|
|
1743
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1744
|
+
/* @__PURE__ */ jsx16(FileText, { className: "w-4 h-4" }),
|
|
1745
|
+
/* @__PURE__ */ jsxs13("span", { children: [
|
|
1746
|
+
invoices.length,
|
|
1747
|
+
" fatture visualizzate"
|
|
1748
|
+
] })
|
|
1749
|
+
] }),
|
|
1750
|
+
/* @__PURE__ */ jsx16("div", { className: "rounded-lg border bg-card shadow-sm", children: /* @__PURE__ */ jsx16("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsxs13("table", { className: "w-full caption-bottom text-sm", children: [
|
|
1751
|
+
/* @__PURE__ */ jsx16("thead", { className: "[&_tr]:border-b", children: /* @__PURE__ */ jsxs13("tr", { className: "border-b hover:bg-muted/50", children: [
|
|
1752
|
+
/* @__PURE__ */ jsx16("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Numero" }),
|
|
1753
|
+
/* @__PURE__ */ jsx16("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Stato" }),
|
|
1754
|
+
/* @__PURE__ */ jsx16("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Importo Dovuto" }),
|
|
1755
|
+
/* @__PURE__ */ jsx16("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Importo Pagato" }),
|
|
1756
|
+
/* @__PURE__ */ jsx16("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Cliente" }),
|
|
1757
|
+
/* @__PURE__ */ jsx16("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Data" }),
|
|
1758
|
+
/* @__PURE__ */ jsx16("th", { className: "h-12 px-4 text-left font-medium text-muted-foreground", children: "Scadenza" }),
|
|
1759
|
+
/* @__PURE__ */ jsx16("th", { className: "h-12 px-4 text-right font-medium text-muted-foreground", children: "Azioni" })
|
|
1760
|
+
] }) }),
|
|
1761
|
+
/* @__PURE__ */ jsx16("tbody", { children: isLoading ? /* @__PURE__ */ jsx16(SkeletonRows, { rows: 5, columns: 8 }) : invoices.length === 0 ? /* @__PURE__ */ jsx16("tr", { children: /* @__PURE__ */ jsx16("td", { colSpan: 8, className: "p-4 text-center py-8 text-muted-foreground", children: "Nessuna fattura trovata" }) }) : invoices.map((inv) => /* @__PURE__ */ jsxs13("tr", { className: "border-b hover:bg-muted/50", children: [
|
|
1762
|
+
/* @__PURE__ */ jsx16("td", { className: "p-4 font-medium", children: inv.number || /* @__PURE__ */ jsx16("span", { className: "text-muted-foreground", children: "-" }) }),
|
|
1763
|
+
/* @__PURE__ */ jsx16("td", { className: "p-4", children: /* @__PURE__ */ jsx16(StatusBadge, { label: invoiceStatusLabels2[inv.status], colorClass: invoiceStatusColors2[inv.status] }) }),
|
|
1764
|
+
/* @__PURE__ */ jsx16("td", { className: "p-4 font-medium", children: formatCurrencyFromCents4(inv.amount_due, inv.currency) }),
|
|
1765
|
+
/* @__PURE__ */ jsx16("td", { className: "p-4", children: formatCurrencyFromCents4(inv.amount_paid, inv.currency) }),
|
|
1766
|
+
/* @__PURE__ */ jsx16("td", { className: "p-4", children: /* @__PURE__ */ jsxs13("div", { className: "flex flex-col", children: [
|
|
1767
|
+
/* @__PURE__ */ jsx16("span", { children: inv.customer_name || "-" }),
|
|
1768
|
+
inv.customer_email && /* @__PURE__ */ jsx16("span", { className: "text-xs text-muted-foreground", children: inv.customer_email })
|
|
1769
|
+
] }) }),
|
|
1770
|
+
/* @__PURE__ */ jsx16("td", { className: "p-4", children: formatTimestamp3(inv.created) }),
|
|
1771
|
+
/* @__PURE__ */ jsx16("td", { className: "p-4", children: formatTimestamp3(inv.due_date) }),
|
|
1772
|
+
/* @__PURE__ */ jsx16("td", { className: "p-4 text-right", children: /* @__PURE__ */ jsxs13("div", { className: "flex justify-end gap-1", children: [
|
|
1773
|
+
/* @__PURE__ */ jsx16("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 hover:bg-accent", onClick: () => detailsDialog.open(inv), children: /* @__PURE__ */ jsx16(Eye2, { className: "w-4 h-4" }) }),
|
|
1774
|
+
(inv.invoice_pdf || inv.hosted_invoice_url) && /* @__PURE__ */ jsx16("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 hover:bg-accent", onClick: () => openInvoicePdf(inv), children: /* @__PURE__ */ jsx16(Download, { className: "w-4 h-4" }) }),
|
|
1775
|
+
inv.status === "open" && /* @__PURE__ */ jsx16("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 hover:bg-accent", onClick: () => sendMutation.mutate(inv.id, { onSuccess: () => toast5.success("Fattura inviata"), onError: (err) => toast5.error(`Errore: ${err.message}`) }), disabled: sendMutation.isPending, children: /* @__PURE__ */ jsx16(Send, { className: "w-4 h-4" }) }),
|
|
1776
|
+
(inv.status === "draft" || inv.status === "open") && /* @__PURE__ */ jsx16("button", { className: "inline-flex items-center justify-center rounded-md h-9 w-9 text-destructive hover:bg-accent", onClick: () => voidDialog.open(inv), children: /* @__PURE__ */ jsx16(XCircle2, { className: "w-4 h-4" }) })
|
|
1777
|
+
] }) })
|
|
1778
|
+
] }, inv.id)) })
|
|
1779
|
+
] }) }) }),
|
|
1780
|
+
/* @__PURE__ */ jsx16(CursorPagination, { cursor, hasMore, items: invoices, onFirstPage: () => setCursor(void 0), onNextPage: setCursor })
|
|
1781
|
+
] }),
|
|
1782
|
+
/* @__PURE__ */ jsx16(InvoiceDetailsDialog, { isOpen: detailsDialog.isOpen, onOpenChange: (open) => !open && detailsDialog.close(), invoice: detailsDialog.data }),
|
|
1783
|
+
/* @__PURE__ */ jsx16(
|
|
1784
|
+
DeleteConfirmDialog,
|
|
1785
|
+
{
|
|
1786
|
+
isOpen: voidDialog.isOpen,
|
|
1787
|
+
onOpenChange: (open) => !open && voidDialog.close(),
|
|
1788
|
+
title: "Annullare Fattura?",
|
|
1789
|
+
description: /* @__PURE__ */ jsxs13(Fragment6, { children: [
|
|
1790
|
+
"Stai per annullare la fattura ",
|
|
1791
|
+
/* @__PURE__ */ jsx16("strong", { children: voidDialog.data?.number || voidDialog.data?.id }),
|
|
1792
|
+
". Una fattura annullata non pu\xF2 essere riattivata."
|
|
1793
|
+
] }),
|
|
1794
|
+
onConfirm: handleVoidConfirm,
|
|
1795
|
+
isDeleting: voidMutation.isPending,
|
|
1796
|
+
confirmLabel: "Conferma Annullamento"
|
|
1797
|
+
}
|
|
1798
|
+
)
|
|
1799
|
+
] });
|
|
1800
|
+
return Wrapper ? /* @__PURE__ */ jsx16(Wrapper, { children: content }) : content;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// src/pages/FeaturesPage.tsx
|
|
1804
|
+
import { useState as useState7 } from "react";
|
|
1805
|
+
import { toast as toast6 } from "sonner";
|
|
1806
|
+
import { Plus as Plus3, Pencil as Pencil3, Trash2 as Trash23, Package as Package2, AlertCircle } from "lucide-react";
|
|
1807
|
+
import {
|
|
1808
|
+
useAdminFeatures,
|
|
1809
|
+
useCreateFeature,
|
|
1810
|
+
useUpdateFeature,
|
|
1811
|
+
useDeleteFeature
|
|
1812
|
+
} from "@growflowstudio/billing-admin-core";
|
|
1813
|
+
import { Fragment as Fragment7, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1814
|
+
function FeaturesPage({ wrapper: Wrapper, header }) {
|
|
1815
|
+
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState7(false);
|
|
1816
|
+
const [editingFeature, setEditingFeature] = useState7(null);
|
|
1817
|
+
const [deletingFeature, setDeletingFeature] = useState7(null);
|
|
1818
|
+
const { data, isLoading } = useAdminFeatures();
|
|
1819
|
+
const createMutation = useCreateFeature();
|
|
1820
|
+
const updateMutation = useUpdateFeature();
|
|
1821
|
+
const deleteMutation = useDeleteFeature();
|
|
1822
|
+
const features = data?.features || [];
|
|
1823
|
+
const handleCreateFeature = (e) => {
|
|
1824
|
+
e.preventDefault();
|
|
1825
|
+
const formData = new FormData(e.currentTarget);
|
|
1826
|
+
const createData = {
|
|
1827
|
+
slug: formData.get("slug"),
|
|
1828
|
+
name: formData.get("name"),
|
|
1829
|
+
description: formData.get("description") || void 0,
|
|
1830
|
+
icon: formData.get("icon") || void 0,
|
|
1831
|
+
category: formData.get("category") || void 0,
|
|
1832
|
+
is_available: formData.get("is_available") === "on",
|
|
1833
|
+
billing_product_id: formData.get("billing_product_id") || void 0
|
|
1834
|
+
};
|
|
1835
|
+
createMutation.mutate(createData, {
|
|
1836
|
+
onSuccess: () => {
|
|
1837
|
+
toast6.success("Feature creata con successo");
|
|
1838
|
+
setIsCreateDialogOpen(false);
|
|
1839
|
+
},
|
|
1840
|
+
onError: (err) => toast6.error(`Errore: ${err.message}`)
|
|
1841
|
+
});
|
|
1842
|
+
};
|
|
1843
|
+
const handleUpdateFeature = (e) => {
|
|
1844
|
+
e.preventDefault();
|
|
1845
|
+
if (!editingFeature) return;
|
|
1846
|
+
const formData = new FormData(e.currentTarget);
|
|
1847
|
+
const updateData = {
|
|
1848
|
+
name: formData.get("name"),
|
|
1849
|
+
description: formData.get("description") || void 0,
|
|
1850
|
+
icon: formData.get("icon") || void 0,
|
|
1851
|
+
category: formData.get("category") || void 0,
|
|
1852
|
+
is_available: formData.get("is_available") === "on",
|
|
1853
|
+
billing_product_id: formData.get("billing_product_id") || void 0
|
|
1854
|
+
};
|
|
1855
|
+
updateMutation.mutate({ id: editingFeature.id, data: updateData }, {
|
|
1856
|
+
onSuccess: () => {
|
|
1857
|
+
toast6.success("Feature aggiornata con successo");
|
|
1858
|
+
setEditingFeature(null);
|
|
1859
|
+
},
|
|
1860
|
+
onError: (err) => toast6.error(`Errore: ${err.message}`)
|
|
1861
|
+
});
|
|
1862
|
+
};
|
|
1863
|
+
const handleDeleteConfirm = () => {
|
|
1864
|
+
if (deletingFeature) {
|
|
1865
|
+
deleteMutation.mutate(deletingFeature.id, {
|
|
1866
|
+
onSuccess: () => {
|
|
1867
|
+
toast6.success("Feature eliminata con successo");
|
|
1868
|
+
setDeletingFeature(null);
|
|
1869
|
+
},
|
|
1870
|
+
onError: (err) => toast6.error(`Errore: ${err.message}`)
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
const content = /* @__PURE__ */ jsxs14(Fragment7, { children: [
|
|
1875
|
+
header,
|
|
1876
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-4", children: [
|
|
1877
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between", children: [
|
|
1878
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1879
|
+
/* @__PURE__ */ jsx17(Package2, { className: "w-4 h-4" }),
|
|
1880
|
+
/* @__PURE__ */ jsxs14("span", { children: [
|
|
1881
|
+
features.length,
|
|
1882
|
+
" feature",
|
|
1883
|
+
features.length !== 1 ? "s" : "",
|
|
1884
|
+
" configurate"
|
|
1885
|
+
] })
|
|
1886
|
+
] }),
|
|
1887
|
+
/* @__PURE__ */ jsxs14(
|
|
1888
|
+
"button",
|
|
1889
|
+
{
|
|
1890
|
+
className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90",
|
|
1891
|
+
onClick: () => setIsCreateDialogOpen(true),
|
|
1892
|
+
children: [
|
|
1893
|
+
/* @__PURE__ */ jsx17(Plus3, { className: "w-4 h-4 mr-2" }),
|
|
1894
|
+
"Nuova Feature"
|
|
1895
|
+
]
|
|
1896
|
+
}
|
|
1897
|
+
)
|
|
1898
|
+
] }),
|
|
1899
|
+
/* @__PURE__ */ jsxs14("div", { className: "rounded-lg border bg-card shadow-sm", children: [
|
|
1900
|
+
/* @__PURE__ */ jsxs14("div", { className: "p-4 border-b", children: [
|
|
1901
|
+
/* @__PURE__ */ jsxs14("h3", { className: "text-base font-semibold flex items-center gap-2", children: [
|
|
1902
|
+
/* @__PURE__ */ jsx17(Package2, { className: "w-5 h-5" }),
|
|
1903
|
+
"Features Disponibili"
|
|
1904
|
+
] }),
|
|
1905
|
+
/* @__PURE__ */ jsx17("p", { className: "text-sm text-muted-foreground mt-1", children: "Le features possono essere collegate a prodotti billing o attivate manualmente." })
|
|
1906
|
+
] }),
|
|
1907
|
+
/* @__PURE__ */ jsxs14("div", { className: "p-4", children: [
|
|
1908
|
+
isLoading ? /* @__PURE__ */ jsx17("div", { className: "space-y-3", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx17("div", { className: "h-20 bg-muted animate-pulse rounded-lg" }, i)) }) : features.length === 0 ? /* @__PURE__ */ jsxs14("div", { className: "text-center py-12 text-muted-foreground", children: [
|
|
1909
|
+
/* @__PURE__ */ jsx17(Package2, { className: "mx-auto h-12 w-12 mb-4 opacity-50" }),
|
|
1910
|
+
/* @__PURE__ */ jsx17("p", { children: "Nessuna feature configurata" }),
|
|
1911
|
+
/* @__PURE__ */ jsx17(
|
|
1912
|
+
"button",
|
|
1913
|
+
{
|
|
1914
|
+
className: "text-sm text-primary hover:underline mt-2",
|
|
1915
|
+
onClick: () => setIsCreateDialogOpen(true),
|
|
1916
|
+
children: "Crea la prima feature"
|
|
1917
|
+
}
|
|
1918
|
+
)
|
|
1919
|
+
] }) : /* @__PURE__ */ jsx17("div", { className: "space-y-3", children: features.map((feature) => /* @__PURE__ */ jsx17(
|
|
1920
|
+
"div",
|
|
1921
|
+
{
|
|
1922
|
+
className: "border rounded-lg p-4 hover:bg-muted/50 transition-colors",
|
|
1923
|
+
children: /* @__PURE__ */ jsxs14("div", { className: "flex items-start justify-between", children: [
|
|
1924
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex-1", children: [
|
|
1925
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2 mb-2", children: [
|
|
1926
|
+
/* @__PURE__ */ jsx17("span", { className: "font-semibold", children: feature.name }),
|
|
1927
|
+
/* @__PURE__ */ jsx17("code", { className: "text-xs bg-muted px-2 py-0.5 rounded", children: feature.slug }),
|
|
1928
|
+
!feature.is_available && /* @__PURE__ */ jsx17(StatusBadge, { label: "Disattivata", colorClass: "bg-secondary text-secondary-foreground border-transparent" }),
|
|
1929
|
+
feature.billing_product_id && /* @__PURE__ */ jsx17(StatusBadge, { label: "Con Billing", colorClass: "border" })
|
|
1930
|
+
] }),
|
|
1931
|
+
feature.description && /* @__PURE__ */ jsx17("p", { className: "text-sm text-muted-foreground mb-2", children: feature.description }),
|
|
1932
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-4 text-xs text-muted-foreground", children: [
|
|
1933
|
+
feature.category && /* @__PURE__ */ jsxs14("span", { children: [
|
|
1934
|
+
"Categoria: ",
|
|
1935
|
+
feature.category
|
|
1936
|
+
] }),
|
|
1937
|
+
feature.icon && /* @__PURE__ */ jsxs14("span", { children: [
|
|
1938
|
+
"Icona: ",
|
|
1939
|
+
feature.icon
|
|
1940
|
+
] })
|
|
1941
|
+
] })
|
|
1942
|
+
] }),
|
|
1943
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1", children: [
|
|
1944
|
+
/* @__PURE__ */ jsx17(
|
|
1945
|
+
"button",
|
|
1946
|
+
{
|
|
1947
|
+
className: "inline-flex items-center justify-center rounded-md h-9 w-9 hover:bg-accent",
|
|
1948
|
+
onClick: () => setEditingFeature(feature),
|
|
1949
|
+
children: /* @__PURE__ */ jsx17(Pencil3, { className: "w-4 h-4" })
|
|
1950
|
+
}
|
|
1951
|
+
),
|
|
1952
|
+
/* @__PURE__ */ jsx17(
|
|
1953
|
+
"button",
|
|
1954
|
+
{
|
|
1955
|
+
className: "inline-flex items-center justify-center rounded-md h-9 w-9 text-destructive hover:bg-accent",
|
|
1956
|
+
onClick: () => setDeletingFeature(feature),
|
|
1957
|
+
children: /* @__PURE__ */ jsx17(Trash23, { className: "w-4 h-4" })
|
|
1958
|
+
}
|
|
1959
|
+
)
|
|
1960
|
+
] })
|
|
1961
|
+
] })
|
|
1962
|
+
},
|
|
1963
|
+
feature.id
|
|
1964
|
+
)) }),
|
|
1965
|
+
/* @__PURE__ */ jsxs14("div", { className: "mt-6 flex gap-3 items-start rounded-lg border p-3 text-sm", children: [
|
|
1966
|
+
/* @__PURE__ */ jsx17(AlertCircle, { className: "w-4 h-4 mt-0.5 text-muted-foreground shrink-0" }),
|
|
1967
|
+
/* @__PURE__ */ jsxs14("p", { className: "text-muted-foreground text-xs", children: [
|
|
1968
|
+
/* @__PURE__ */ jsx17("strong", { children: "Nota:" }),
|
|
1969
|
+
" Features con un billing_product_id vengono attivate automaticamente quando il cliente sottoscrive il piano corrispondente."
|
|
1970
|
+
] })
|
|
1971
|
+
] })
|
|
1972
|
+
] })
|
|
1973
|
+
] })
|
|
1974
|
+
] }),
|
|
1975
|
+
isCreateDialogOpen && /* @__PURE__ */ jsxs14("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1976
|
+
/* @__PURE__ */ jsx17("div", { className: "fixed inset-0 bg-black/80", onClick: () => setIsCreateDialogOpen(false) }),
|
|
1977
|
+
/* @__PURE__ */ jsx17("div", { className: "relative z-50 w-full max-w-md rounded-lg border bg-background p-6 shadow-lg", children: /* @__PURE__ */ jsxs14("form", { onSubmit: handleCreateFeature, children: [
|
|
1978
|
+
/* @__PURE__ */ jsx17("h2", { className: "text-lg font-semibold", children: "Crea Nuova Feature" }),
|
|
1979
|
+
/* @__PURE__ */ jsx17("p", { className: "text-sm text-muted-foreground mt-1", children: "Configura una nuova feature disponibile." }),
|
|
1980
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-4 py-4", children: [
|
|
1981
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
1982
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "slug", children: "Slug *" }),
|
|
1983
|
+
/* @__PURE__ */ jsx17("input", { id: "slug", name: "slug", required: true, placeholder: "ottimizza-dati-prodotti", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" }),
|
|
1984
|
+
/* @__PURE__ */ jsx17("p", { className: "text-xs text-muted-foreground", children: "Identificatore univoco (lowercase, trattini)" })
|
|
1985
|
+
] }),
|
|
1986
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
1987
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "name", children: "Nome *" }),
|
|
1988
|
+
/* @__PURE__ */ jsx17("input", { id: "name", name: "name", required: true, placeholder: "Ottimizza Dati Prodotti", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
1989
|
+
] }),
|
|
1990
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
1991
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "description", children: "Descrizione" }),
|
|
1992
|
+
/* @__PURE__ */ jsx17("textarea", { id: "description", name: "description", rows: 3, placeholder: "Descrizione della feature...", className: "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
1993
|
+
] }),
|
|
1994
|
+
/* @__PURE__ */ jsxs14("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
1995
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
1996
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "icon", children: "Icona Lucide" }),
|
|
1997
|
+
/* @__PURE__ */ jsx17("input", { id: "icon", name: "icon", placeholder: "Package", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
1998
|
+
] }),
|
|
1999
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
2000
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "category", children: "Categoria" }),
|
|
2001
|
+
/* @__PURE__ */ jsx17("input", { id: "category", name: "category", placeholder: "ai, limits, integration...", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
2002
|
+
] })
|
|
2003
|
+
] }),
|
|
2004
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
2005
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "billing_product_id", children: "Billing Product ID (opzionale)" }),
|
|
2006
|
+
/* @__PURE__ */ jsx17("input", { id: "billing_product_id", name: "billing_product_id", placeholder: "UUID del prodotto billing", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" }),
|
|
2007
|
+
/* @__PURE__ */ jsx17("p", { className: "text-xs text-muted-foreground", children: "Se collegato a un prodotto, la feature viene attivata automaticamente" })
|
|
2008
|
+
] }),
|
|
2009
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
|
|
2010
|
+
/* @__PURE__ */ jsx17("input", { type: "checkbox", id: "is_available", name: "is_available", defaultChecked: true, className: "rounded border-input" }),
|
|
2011
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "is_available", children: "Feature disponibile" })
|
|
2012
|
+
] })
|
|
2013
|
+
] }),
|
|
2014
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex justify-end gap-2", children: [
|
|
2015
|
+
/* @__PURE__ */ jsx17("button", { type: "button", className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent", onClick: () => setIsCreateDialogOpen(false), children: "Annulla" }),
|
|
2016
|
+
/* @__PURE__ */ jsx17("button", { type: "submit", className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50", disabled: createMutation.isPending, children: createMutation.isPending ? "Creazione..." : "Crea Feature" })
|
|
2017
|
+
] })
|
|
2018
|
+
] }) })
|
|
2019
|
+
] }),
|
|
2020
|
+
editingFeature && /* @__PURE__ */ jsxs14("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
2021
|
+
/* @__PURE__ */ jsx17("div", { className: "fixed inset-0 bg-black/80", onClick: () => setEditingFeature(null) }),
|
|
2022
|
+
/* @__PURE__ */ jsx17("div", { className: "relative z-50 w-full max-w-md rounded-lg border bg-background p-6 shadow-lg", children: /* @__PURE__ */ jsxs14("form", { onSubmit: handleUpdateFeature, children: [
|
|
2023
|
+
/* @__PURE__ */ jsx17("h2", { className: "text-lg font-semibold", children: "Modifica Feature" }),
|
|
2024
|
+
/* @__PURE__ */ jsxs14("p", { className: "text-sm text-muted-foreground mt-1", children: [
|
|
2025
|
+
'Aggiorna i dettagli della feature "',
|
|
2026
|
+
editingFeature.name,
|
|
2027
|
+
'".'
|
|
2028
|
+
] }),
|
|
2029
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-4 py-4", children: [
|
|
2030
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
2031
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", children: "Slug (non modificabile)" }),
|
|
2032
|
+
/* @__PURE__ */ jsx17("input", { value: editingFeature.slug, disabled: true, className: "flex h-10 w-full rounded-md border border-input bg-muted px-3 py-2 text-sm" })
|
|
2033
|
+
] }),
|
|
2034
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
2035
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "edit-name", children: "Nome *" }),
|
|
2036
|
+
/* @__PURE__ */ jsx17("input", { id: "edit-name", name: "name", required: true, defaultValue: editingFeature.name, className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
2037
|
+
] }),
|
|
2038
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
2039
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "edit-description", children: "Descrizione" }),
|
|
2040
|
+
/* @__PURE__ */ jsx17("textarea", { id: "edit-description", name: "description", rows: 3, defaultValue: editingFeature.description || "", className: "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
2041
|
+
] }),
|
|
2042
|
+
/* @__PURE__ */ jsxs14("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
2043
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
2044
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "edit-icon", children: "Icona Lucide" }),
|
|
2045
|
+
/* @__PURE__ */ jsx17("input", { id: "edit-icon", name: "icon", defaultValue: editingFeature.icon || "", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
2046
|
+
] }),
|
|
2047
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
2048
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "edit-category", children: "Categoria" }),
|
|
2049
|
+
/* @__PURE__ */ jsx17("input", { id: "edit-category", name: "category", defaultValue: editingFeature.category || "", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
2050
|
+
] })
|
|
2051
|
+
] }),
|
|
2052
|
+
/* @__PURE__ */ jsxs14("div", { className: "space-y-2", children: [
|
|
2053
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "edit-billing_product_id", children: "Billing Product ID" }),
|
|
2054
|
+
/* @__PURE__ */ jsx17("input", { id: "edit-billing_product_id", name: "billing_product_id", defaultValue: editingFeature.billing_product_id || "", className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" })
|
|
2055
|
+
] }),
|
|
2056
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
|
|
2057
|
+
/* @__PURE__ */ jsx17("input", { type: "checkbox", id: "edit-is_available", name: "is_available", defaultChecked: editingFeature.is_available, className: "rounded border-input" }),
|
|
2058
|
+
/* @__PURE__ */ jsx17("label", { className: "text-sm font-medium leading-none", htmlFor: "edit-is_available", children: "Feature disponibile" })
|
|
2059
|
+
] })
|
|
2060
|
+
] }),
|
|
2061
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex justify-end gap-2", children: [
|
|
2062
|
+
/* @__PURE__ */ jsx17("button", { type: "button", className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 border border-input bg-background hover:bg-accent", onClick: () => setEditingFeature(null), children: "Annulla" }),
|
|
2063
|
+
/* @__PURE__ */ jsx17("button", { type: "submit", className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50", disabled: updateMutation.isPending, children: updateMutation.isPending ? "Salvataggio..." : "Salva Modifiche" })
|
|
2064
|
+
] })
|
|
2065
|
+
] }) })
|
|
2066
|
+
] }),
|
|
2067
|
+
/* @__PURE__ */ jsx17(
|
|
2068
|
+
DeleteConfirmDialog,
|
|
2069
|
+
{
|
|
2070
|
+
isOpen: !!deletingFeature,
|
|
2071
|
+
onOpenChange: (open) => !open && setDeletingFeature(null),
|
|
2072
|
+
title: "Eliminare Feature?",
|
|
2073
|
+
description: /* @__PURE__ */ jsxs14(Fragment7, { children: [
|
|
2074
|
+
"Stai per eliminare la feature ",
|
|
2075
|
+
/* @__PURE__ */ jsx17("strong", { children: deletingFeature?.name }),
|
|
2076
|
+
" (",
|
|
2077
|
+
/* @__PURE__ */ jsx17("code", { children: deletingFeature?.slug }),
|
|
2078
|
+
"). Questa azione non pu\xF2 essere annullata."
|
|
2079
|
+
] }),
|
|
2080
|
+
onConfirm: handleDeleteConfirm,
|
|
2081
|
+
isDeleting: deleteMutation.isPending,
|
|
2082
|
+
confirmLabel: "Elimina Feature"
|
|
2083
|
+
}
|
|
2084
|
+
)
|
|
2085
|
+
] });
|
|
2086
|
+
return Wrapper ? /* @__PURE__ */ jsx17(Wrapper, { children: content }) : content;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/nav/billingNavItems.ts
|
|
2090
|
+
var ALL_NAV_ITEMS = [
|
|
2091
|
+
{ key: "plans", label: "Piani", icon: "LayoutGrid", href: "/admin/plans" },
|
|
2092
|
+
{ key: "subscriptions", label: "Abbonamenti", icon: "CreditCard", href: "/admin/subscriptions" },
|
|
2093
|
+
{ key: "products", label: "Prodotti", icon: "Package", href: "/admin/products" },
|
|
2094
|
+
{ key: "features", label: "Features", icon: "Puzzle", href: "/admin/features" },
|
|
2095
|
+
{ key: "payments", label: "Pagamenti", icon: "Receipt", href: "/admin/payments" },
|
|
2096
|
+
{ key: "invoices", label: "Fatture", icon: "FileText", href: "/admin/invoices" }
|
|
2097
|
+
];
|
|
2098
|
+
function getBillingNavItems(modules, basePath) {
|
|
2099
|
+
const enabled = {
|
|
2100
|
+
plans: true,
|
|
2101
|
+
subscriptions: true,
|
|
2102
|
+
products: true,
|
|
2103
|
+
payments: true,
|
|
2104
|
+
invoices: true,
|
|
2105
|
+
features: true,
|
|
2106
|
+
...modules
|
|
2107
|
+
};
|
|
2108
|
+
return ALL_NAV_ITEMS.filter((item) => enabled[item.key] !== false).map((item) => ({
|
|
2109
|
+
...item,
|
|
2110
|
+
href: basePath ? `${basePath}${item.href}` : item.href
|
|
2111
|
+
}));
|
|
2112
|
+
}
|
|
2113
|
+
export {
|
|
2114
|
+
BillingAdminProvider,
|
|
2115
|
+
CursorPagination,
|
|
2116
|
+
DeleteConfirmDialog,
|
|
2117
|
+
FeaturesPage,
|
|
2118
|
+
FilterBar,
|
|
2119
|
+
InvoiceDetailsDialog,
|
|
2120
|
+
InvoicesPage,
|
|
2121
|
+
LimitInput,
|
|
2122
|
+
PLAN_COLORS,
|
|
2123
|
+
PaymentsPage,
|
|
2124
|
+
PlanFormDialog,
|
|
2125
|
+
PlansPage,
|
|
2126
|
+
PlansTable,
|
|
2127
|
+
ProductFormDialog,
|
|
2128
|
+
ProductsPage,
|
|
2129
|
+
SkeletonRows,
|
|
2130
|
+
StatusBadge,
|
|
2131
|
+
SubscriptionsPage,
|
|
2132
|
+
createDefaultPlanFormData,
|
|
2133
|
+
defaultProductFormData,
|
|
2134
|
+
getBillingNavItems,
|
|
2135
|
+
getPlanColorClass,
|
|
2136
|
+
useBillingAdminConfig,
|
|
2137
|
+
useEnabledModules
|
|
2138
|
+
};
|
|
2139
|
+
//# sourceMappingURL=index.mjs.map
|