@hed-hog/billing 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/dist/adapters/billing-gateway-registry.d.ts +8 -0
  2. package/dist/adapters/billing-gateway-registry.d.ts.map +1 -0
  3. package/dist/adapters/billing-gateway-registry.js +33 -0
  4. package/dist/adapters/billing-gateway-registry.js.map +1 -0
  5. package/dist/adapters/mercadopago.adapter.d.ts +44 -0
  6. package/dist/adapters/mercadopago.adapter.d.ts.map +1 -0
  7. package/dist/adapters/mercadopago.adapter.js +68 -0
  8. package/dist/adapters/mercadopago.adapter.js.map +1 -0
  9. package/dist/adapters/pagarme.adapter.d.ts +44 -0
  10. package/dist/adapters/pagarme.adapter.d.ts.map +1 -0
  11. package/dist/adapters/pagarme.adapter.js +68 -0
  12. package/dist/adapters/pagarme.adapter.js.map +1 -0
  13. package/dist/adapters/payment-gateway.adapter.d.ts +15 -0
  14. package/dist/adapters/payment-gateway.adapter.d.ts.map +1 -0
  15. package/dist/adapters/payment-gateway.adapter.js +3 -0
  16. package/dist/adapters/payment-gateway.adapter.js.map +1 -0
  17. package/dist/adapters/stripe.adapter.d.ts +44 -0
  18. package/dist/adapters/stripe.adapter.d.ts.map +1 -0
  19. package/dist/adapters/stripe.adapter.js +69 -0
  20. package/dist/adapters/stripe.adapter.js.map +1 -0
  21. package/dist/billing-contracts.controller.d.ts +22 -0
  22. package/dist/billing-contracts.controller.d.ts.map +1 -0
  23. package/dist/billing-contracts.controller.js +84 -0
  24. package/dist/billing-contracts.controller.js.map +1 -0
  25. package/dist/billing-coupons.controller.d.ts +22 -0
  26. package/dist/billing-coupons.controller.d.ts.map +1 -0
  27. package/dist/billing-coupons.controller.js +84 -0
  28. package/dist/billing-coupons.controller.js.map +1 -0
  29. package/dist/billing-dashboard.controller.d.ts +13 -0
  30. package/dist/billing-dashboard.controller.d.ts.map +1 -0
  31. package/dist/billing-dashboard.controller.js +36 -0
  32. package/dist/billing-dashboard.controller.js.map +1 -0
  33. package/dist/billing-entitlements.controller.d.ts +19 -0
  34. package/dist/billing-entitlements.controller.d.ts.map +1 -0
  35. package/dist/billing-entitlements.controller.js +62 -0
  36. package/dist/billing-entitlements.controller.js.map +1 -0
  37. package/dist/billing-gateways.controller.d.ts +8 -0
  38. package/dist/billing-gateways.controller.d.ts.map +1 -0
  39. package/dist/billing-gateways.controller.js +50 -0
  40. package/dist/billing-gateways.controller.js.map +1 -0
  41. package/dist/billing-invoices.controller.d.ts +18 -0
  42. package/dist/billing-invoices.controller.d.ts.map +1 -0
  43. package/dist/billing-invoices.controller.js +61 -0
  44. package/dist/billing-invoices.controller.js.map +1 -0
  45. package/dist/billing-offers.controller.d.ts +22 -0
  46. package/dist/billing-offers.controller.d.ts.map +1 -0
  47. package/dist/billing-offers.controller.js +84 -0
  48. package/dist/billing-offers.controller.js.map +1 -0
  49. package/dist/billing-orders.controller.d.ts +19 -0
  50. package/dist/billing-orders.controller.d.ts.map +1 -0
  51. package/dist/billing-orders.controller.js +62 -0
  52. package/dist/billing-orders.controller.js.map +1 -0
  53. package/dist/billing-payments.controller.d.ts +17 -0
  54. package/dist/billing-payments.controller.d.ts.map +1 -0
  55. package/dist/billing-payments.controller.js +51 -0
  56. package/dist/billing-payments.controller.js.map +1 -0
  57. package/dist/billing-prices.controller.d.ts +22 -0
  58. package/dist/billing-prices.controller.d.ts.map +1 -0
  59. package/dist/billing-prices.controller.js +84 -0
  60. package/dist/billing-prices.controller.js.map +1 -0
  61. package/dist/billing-products.controller.d.ts +22 -0
  62. package/dist/billing-products.controller.d.ts.map +1 -0
  63. package/dist/billing-products.controller.js +84 -0
  64. package/dist/billing-products.controller.js.map +1 -0
  65. package/dist/billing-refunds.controller.d.ts +16 -0
  66. package/dist/billing-refunds.controller.d.ts.map +1 -0
  67. package/dist/billing-refunds.controller.js +41 -0
  68. package/dist/billing-refunds.controller.js.map +1 -0
  69. package/dist/billing-subscriptions.controller.d.ts +22 -0
  70. package/dist/billing-subscriptions.controller.d.ts.map +1 -0
  71. package/dist/billing-subscriptions.controller.js +92 -0
  72. package/dist/billing-subscriptions.controller.js.map +1 -0
  73. package/dist/billing-webhooks.controller.d.ts +16 -0
  74. package/dist/billing-webhooks.controller.d.ts.map +1 -0
  75. package/dist/billing-webhooks.controller.js +41 -0
  76. package/dist/billing-webhooks.controller.js.map +1 -0
  77. package/dist/billing.module.d.ts +3 -0
  78. package/dist/billing.module.d.ts.map +1 -0
  79. package/dist/billing.module.js +61 -0
  80. package/dist/billing.module.js.map +1 -0
  81. package/dist/billing.service.d.ts +169 -0
  82. package/dist/billing.service.d.ts.map +1 -0
  83. package/dist/billing.service.js +290 -0
  84. package/dist/billing.service.js.map +1 -0
  85. package/dist/dto/create-contract.dto.d.ts +11 -0
  86. package/dist/dto/create-contract.dto.d.ts.map +1 -0
  87. package/dist/dto/create-contract.dto.js +52 -0
  88. package/dist/dto/create-contract.dto.js.map +1 -0
  89. package/dist/dto/create-coupon.dto.d.ts +12 -0
  90. package/dist/dto/create-coupon.dto.d.ts.map +1 -0
  91. package/dist/dto/create-coupon.dto.js +60 -0
  92. package/dist/dto/create-coupon.dto.js.map +1 -0
  93. package/dist/dto/create-entitlement.dto.d.ts +12 -0
  94. package/dist/dto/create-entitlement.dto.d.ts.map +1 -0
  95. package/dist/dto/create-entitlement.dto.js +54 -0
  96. package/dist/dto/create-entitlement.dto.js.map +1 -0
  97. package/dist/dto/create-offer.dto.d.ts +7 -0
  98. package/dist/dto/create-offer.dto.d.ts.map +1 -0
  99. package/dist/dto/create-offer.dto.js +35 -0
  100. package/dist/dto/create-offer.dto.js.map +1 -0
  101. package/dist/dto/create-order.dto.d.ts +12 -0
  102. package/dist/dto/create-order.dto.d.ts.map +1 -0
  103. package/dist/dto/create-order.dto.js +65 -0
  104. package/dist/dto/create-order.dto.js.map +1 -0
  105. package/dist/dto/create-price.dto.d.ts +15 -0
  106. package/dist/dto/create-price.dto.d.ts.map +1 -0
  107. package/dist/dto/create-price.dto.js +76 -0
  108. package/dist/dto/create-price.dto.js.map +1 -0
  109. package/dist/dto/create-product.dto.d.ts +9 -0
  110. package/dist/dto/create-product.dto.d.ts.map +1 -0
  111. package/dist/dto/create-product.dto.js +45 -0
  112. package/dist/dto/create-product.dto.js.map +1 -0
  113. package/dist/dto/create-subscription.dto.d.ts +14 -0
  114. package/dist/dto/create-subscription.dto.d.ts.map +1 -0
  115. package/dist/dto/create-subscription.dto.js +67 -0
  116. package/dist/dto/create-subscription.dto.js.map +1 -0
  117. package/dist/dto/update-contract.dto.d.ts +6 -0
  118. package/dist/dto/update-contract.dto.d.ts.map +1 -0
  119. package/dist/dto/update-contract.dto.js +9 -0
  120. package/dist/dto/update-contract.dto.js.map +1 -0
  121. package/dist/dto/update-coupon.dto.d.ts +6 -0
  122. package/dist/dto/update-coupon.dto.d.ts.map +1 -0
  123. package/dist/dto/update-coupon.dto.js +9 -0
  124. package/dist/dto/update-coupon.dto.js.map +1 -0
  125. package/dist/dto/update-offer.dto.d.ts +6 -0
  126. package/dist/dto/update-offer.dto.d.ts.map +1 -0
  127. package/dist/dto/update-offer.dto.js +9 -0
  128. package/dist/dto/update-offer.dto.js.map +1 -0
  129. package/dist/dto/update-order.dto.d.ts +6 -0
  130. package/dist/dto/update-order.dto.d.ts.map +1 -0
  131. package/dist/dto/update-order.dto.js +9 -0
  132. package/dist/dto/update-order.dto.js.map +1 -0
  133. package/dist/dto/update-price.dto.d.ts +6 -0
  134. package/dist/dto/update-price.dto.d.ts.map +1 -0
  135. package/dist/dto/update-price.dto.js +9 -0
  136. package/dist/dto/update-price.dto.js.map +1 -0
  137. package/dist/dto/update-product.dto.d.ts +6 -0
  138. package/dist/dto/update-product.dto.d.ts.map +1 -0
  139. package/dist/dto/update-product.dto.js +9 -0
  140. package/dist/dto/update-product.dto.js.map +1 -0
  141. package/dist/dto/update-subscription.dto.d.ts +6 -0
  142. package/dist/dto/update-subscription.dto.d.ts.map +1 -0
  143. package/dist/dto/update-subscription.dto.js +9 -0
  144. package/dist/dto/update-subscription.dto.js.map +1 -0
  145. package/dist/index.d.ts +32 -0
  146. package/dist/index.d.ts.map +1 -0
  147. package/dist/index.js +48 -0
  148. package/dist/index.js.map +1 -0
  149. package/hedhog/data/menu.yaml +284 -0
  150. package/hedhog/data/role.yaml +7 -0
  151. package/hedhog/data/route.yaml +422 -0
  152. package/hedhog/frontend/app/_lib/billing-mocks.ts.ejs +270 -0
  153. package/hedhog/frontend/app/contracts/page.tsx.ejs +562 -0
  154. package/hedhog/frontend/app/coupons/page.tsx.ejs +669 -0
  155. package/hedhog/frontend/app/entitlements/page.tsx.ejs +526 -0
  156. package/hedhog/frontend/app/gateways/page.tsx.ejs +308 -0
  157. package/hedhog/frontend/app/invoices/page.tsx.ejs +179 -0
  158. package/hedhog/frontend/app/offers/page.tsx.ejs +483 -0
  159. package/hedhog/frontend/app/orders/page.tsx.ejs +424 -0
  160. package/hedhog/frontend/app/page.tsx.ejs +186 -0
  161. package/hedhog/frontend/app/payments/page.tsx.ejs +187 -0
  162. package/hedhog/frontend/app/prices/page.tsx.ejs +704 -0
  163. package/hedhog/frontend/app/products/page.tsx.ejs +568 -0
  164. package/hedhog/frontend/app/refunds/page.tsx.ejs +174 -0
  165. package/hedhog/frontend/app/reports/page.tsx.ejs +177 -0
  166. package/hedhog/frontend/app/subscriptions/page.tsx.ejs +283 -0
  167. package/hedhog/frontend/app/webhooks/page.tsx.ejs +172 -0
  168. package/hedhog/frontend/messages/en.json +551 -0
  169. package/hedhog/frontend/messages/pt.json +563 -0
  170. package/hedhog/table/billing_contract.yaml +37 -0
  171. package/hedhog/table/billing_contract_seat.yaml +28 -0
  172. package/hedhog/table/billing_coupon.yaml +42 -0
  173. package/hedhog/table/billing_entitlement.yaml +41 -0
  174. package/hedhog/table/billing_entitlement_event.yaml +20 -0
  175. package/hedhog/table/billing_invoice.yaml +60 -0
  176. package/hedhog/table/billing_invoice_item.yaml +25 -0
  177. package/hedhog/table/billing_offer.yaml +22 -0
  178. package/hedhog/table/billing_offer_price.yaml +23 -0
  179. package/hedhog/table/billing_order.yaml +50 -0
  180. package/hedhog/table/billing_order_item.yaml +35 -0
  181. package/hedhog/table/billing_payment.yaml +66 -0
  182. package/hedhog/table/billing_payment_method.yaml +49 -0
  183. package/hedhog/table/billing_payment_provider.yaml +21 -0
  184. package/hedhog/table/billing_price.yaml +53 -0
  185. package/hedhog/table/billing_product.yaml +31 -0
  186. package/hedhog/table/billing_product_item.yaml +24 -0
  187. package/hedhog/table/billing_provider_event.yaml +31 -0
  188. package/hedhog/table/billing_refund.yaml +41 -0
  189. package/hedhog/table/billing_seat_allocation.yaml +42 -0
  190. package/hedhog/table/billing_subscription.yaml +66 -0
  191. package/hedhog/table/billing_subscription_event.yaml +20 -0
  192. package/hedhog/table/billing_subscription_item.yaml +29 -0
  193. package/package.json +37 -0
  194. package/src/adapters/billing-gateway-registry.ts +23 -0
  195. package/src/adapters/mercadopago.adapter.ts +66 -0
  196. package/src/adapters/pagarme.adapter.ts +66 -0
  197. package/src/adapters/payment-gateway.adapter.ts +14 -0
  198. package/src/adapters/stripe.adapter.ts +67 -0
  199. package/src/billing-contracts.controller.ts +46 -0
  200. package/src/billing-coupons.controller.ts +46 -0
  201. package/src/billing-dashboard.controller.ts +14 -0
  202. package/src/billing-entitlements.controller.ts +34 -0
  203. package/src/billing-gateways.controller.ts +19 -0
  204. package/src/billing-invoices.controller.ts +32 -0
  205. package/src/billing-offers.controller.ts +46 -0
  206. package/src/billing-orders.controller.ts +33 -0
  207. package/src/billing-payments.controller.ts +20 -0
  208. package/src/billing-prices.controller.ts +46 -0
  209. package/src/billing-products.controller.ts +46 -0
  210. package/src/billing-refunds.controller.ts +15 -0
  211. package/src/billing-subscriptions.controller.ts +49 -0
  212. package/src/billing-webhooks.controller.ts +15 -0
  213. package/src/billing.module.ts +48 -0
  214. package/src/billing.service.ts +391 -0
  215. package/src/dto/create-contract.dto.ts +30 -0
  216. package/src/dto/create-coupon.dto.ts +37 -0
  217. package/src/dto/create-entitlement.dto.ts +31 -0
  218. package/src/dto/create-offer.dto.ts +17 -0
  219. package/src/dto/create-order.dto.ts +42 -0
  220. package/src/dto/create-price.dto.ts +50 -0
  221. package/src/dto/create-product.dto.ts +25 -0
  222. package/src/dto/create-subscription.dto.ts +42 -0
  223. package/src/dto/update-contract.dto.ts +4 -0
  224. package/src/dto/update-coupon.dto.ts +4 -0
  225. package/src/dto/update-offer.dto.ts +4 -0
  226. package/src/dto/update-order.dto.ts +4 -0
  227. package/src/dto/update-price.dto.ts +4 -0
  228. package/src/dto/update-product.dto.ts +4 -0
  229. package/src/dto/update-subscription.dto.ts +4 -0
  230. package/src/index.ts +32 -0
  231. package/src/language/en.json +8 -0
  232. package/src/language/pt.json +8 -0
@@ -0,0 +1,568 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Page,
5
+ PageHeader,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
9
+ import {
10
+ AlertDialog,
11
+ AlertDialogAction,
12
+ AlertDialogCancel,
13
+ AlertDialogContent,
14
+ AlertDialogDescription,
15
+ AlertDialogFooter,
16
+ AlertDialogHeader,
17
+ AlertDialogTitle,
18
+ } from '@/components/ui/alert-dialog';
19
+ import { Badge } from '@/components/ui/badge';
20
+ import { Button } from '@/components/ui/button';
21
+ import {
22
+ Form,
23
+ FormControl,
24
+ FormField,
25
+ FormItem,
26
+ FormLabel,
27
+ FormMessage,
28
+ } from '@/components/ui/form';
29
+ import { Input } from '@/components/ui/input';
30
+ import {
31
+ Select,
32
+ SelectContent,
33
+ SelectItem,
34
+ SelectTrigger,
35
+ SelectValue,
36
+ } from '@/components/ui/select';
37
+ import {
38
+ Sheet,
39
+ SheetContent,
40
+ SheetDescription,
41
+ SheetHeader,
42
+ SheetTitle,
43
+ } from '@/components/ui/sheet';
44
+ import { Switch } from '@/components/ui/switch';
45
+ import {
46
+ Table,
47
+ TableBody,
48
+ TableCell,
49
+ TableHead,
50
+ TableHeader,
51
+ TableRow,
52
+ } from '@/components/ui/table';
53
+ import { Textarea } from '@/components/ui/textarea';
54
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
55
+ import { zodResolver } from '@hookform/resolvers/zod';
56
+ import { Pencil, Plus, Trash2 } from 'lucide-react';
57
+ import { useTranslations } from 'next-intl';
58
+ import { useEffect, useMemo, useState } from 'react';
59
+ import { useForm } from 'react-hook-form';
60
+ import { z } from 'zod';
61
+
62
+ type Product = {
63
+ id: number;
64
+ name: string;
65
+ code: string | null;
66
+ description: string | null;
67
+ product_type: string;
68
+ is_active: boolean;
69
+ created_at: string;
70
+ };
71
+
72
+ type ProductsResponse = {
73
+ data?: Product[];
74
+ total?: number;
75
+ };
76
+
77
+ const productSchema = z.object({
78
+ name: z.string().trim().min(1),
79
+ code: z.string().optional(),
80
+ description: z.string().optional(),
81
+ product_type: z.enum(['digital', 'service', 'physical', 'bundle']),
82
+ is_active: z.boolean(),
83
+ });
84
+
85
+ type ProductFormValues = z.infer<typeof productSchema>;
86
+
87
+ const mapProductTypeToApi = (value: ProductFormValues['product_type']) => {
88
+ if (value === 'digital') {
89
+ return 'saas';
90
+ }
91
+
92
+ if (value === 'bundle') {
93
+ return 'addon';
94
+ }
95
+
96
+ return value;
97
+ };
98
+
99
+ const mapProductTypeFromApi = (
100
+ value: string
101
+ ): ProductFormValues['product_type'] => {
102
+ if (value === 'saas') {
103
+ return 'digital';
104
+ }
105
+
106
+ if (value === 'addon') {
107
+ return 'bundle';
108
+ }
109
+
110
+ if (value === 'service' || value === 'physical') {
111
+ return value;
112
+ }
113
+
114
+ return 'digital';
115
+ };
116
+
117
+ const statusBadgeClass = (value: string) => {
118
+ if (['active', 'paid', 'authorized', 'approved'].includes(value)) {
119
+ return 'bg-green-100 text-green-800';
120
+ }
121
+
122
+ if (['pending', 'open', 'draft'].includes(value)) {
123
+ return 'bg-yellow-100 text-yellow-800';
124
+ }
125
+
126
+ if (['failed', 'canceled', 'void'].includes(value)) {
127
+ return 'bg-red-100 text-red-800';
128
+ }
129
+
130
+ if (['paused', 'inactive'].includes(value)) {
131
+ return 'bg-gray-100 text-gray-800';
132
+ }
133
+
134
+ return 'bg-slate-100 text-slate-800';
135
+ };
136
+
137
+ export default function BillingProductsPage() {
138
+ const t = useTranslations('BillingProductsPage');
139
+ const { request, showToastHandler, currentLocaleCode } = useApp();
140
+ const [search, setSearch] = useState('');
141
+ const [statusFilter, setStatusFilter] = useState('all');
142
+ const [page, setPage] = useState(1);
143
+ const [pageSize, setPageSize] = useState(12);
144
+ const [sheetOpen, setSheetOpen] = useState(false);
145
+ const [editingProduct, setEditingProduct] = useState<Product | null>(null);
146
+ const [deleteId, setDeleteId] = useState<number | null>(null);
147
+
148
+ const form = useForm<ProductFormValues>({
149
+ resolver: zodResolver(productSchema),
150
+ defaultValues: {
151
+ name: '',
152
+ code: '',
153
+ description: '',
154
+ product_type: 'digital',
155
+ is_active: true,
156
+ },
157
+ });
158
+
159
+ useEffect(() => {
160
+ if (!sheetOpen) {
161
+ return;
162
+ }
163
+
164
+ if (editingProduct) {
165
+ form.reset({
166
+ name: editingProduct.name,
167
+ code: editingProduct.code ?? '',
168
+ description: editingProduct.description ?? '',
169
+ product_type: mapProductTypeFromApi(editingProduct.product_type),
170
+ is_active: Boolean(editingProduct.is_active),
171
+ });
172
+ return;
173
+ }
174
+
175
+ form.reset({
176
+ name: '',
177
+ code: '',
178
+ description: '',
179
+ product_type: 'digital',
180
+ is_active: true,
181
+ });
182
+ }, [editingProduct, form, sheetOpen]);
183
+
184
+ const { data, refetch } = useQuery<{ items: Product[]; total: number }>({
185
+ queryKey: [
186
+ 'billing-products',
187
+ currentLocaleCode,
188
+ search,
189
+ statusFilter,
190
+ page,
191
+ pageSize,
192
+ ],
193
+ queryFn: async () => {
194
+ const response = await request<ProductsResponse>({
195
+ url: '/billing/products',
196
+ method: 'GET',
197
+ params: {
198
+ page,
199
+ pageSize,
200
+ search: search.trim() || undefined,
201
+ status: statusFilter !== 'all' ? statusFilter : undefined,
202
+ },
203
+ });
204
+
205
+ const payload = (response.data ?? {}) as ProductsResponse | Product[];
206
+
207
+ if (Array.isArray(payload)) {
208
+ return { items: payload, total: payload.length };
209
+ }
210
+
211
+ return {
212
+ items: payload.data ?? [],
213
+ total: payload.total ?? payload.data?.length ?? 0,
214
+ };
215
+ },
216
+ placeholderData: (old) => old,
217
+ });
218
+
219
+ const items = data?.items ?? [];
220
+ const totalItems = data?.total ?? 0;
221
+
222
+ const stats = useMemo(
223
+ () => ({
224
+ active: items.filter((item) => item.is_active).length,
225
+ inactive: items.filter((item) => !item.is_active).length,
226
+ }),
227
+ [items]
228
+ );
229
+
230
+ const onSubmit = async (values: ProductFormValues) => {
231
+ try {
232
+ const payload = {
233
+ name: values.name,
234
+ code: values.code?.trim() || undefined,
235
+ description: values.description?.trim() || undefined,
236
+ product_type: mapProductTypeToApi(values.product_type),
237
+ is_active: values.is_active,
238
+ };
239
+
240
+ if (editingProduct) {
241
+ await request({
242
+ url: `/billing/products/${editingProduct.id}`,
243
+ method: 'PATCH',
244
+ data: payload,
245
+ });
246
+ } else {
247
+ await request({
248
+ url: '/billing/products',
249
+ method: 'POST',
250
+ data: payload,
251
+ });
252
+ }
253
+
254
+ showToastHandler?.('success', t('messages.saveSuccess'));
255
+ setSheetOpen(false);
256
+ setEditingProduct(null);
257
+ form.reset();
258
+ await refetch();
259
+ } catch {
260
+ showToastHandler?.('error', t('messages.saveError'));
261
+ }
262
+ };
263
+
264
+ const onDelete = async () => {
265
+ if (!deleteId) {
266
+ return;
267
+ }
268
+
269
+ try {
270
+ await request({
271
+ url: `/billing/products/${deleteId}`,
272
+ method: 'DELETE',
273
+ });
274
+
275
+ showToastHandler?.('success', t('messages.deleteSuccess'));
276
+ setDeleteId(null);
277
+ await refetch();
278
+ } catch {
279
+ showToastHandler?.('error', t('messages.deleteError'));
280
+ }
281
+ };
282
+
283
+ return (
284
+ <Page>
285
+ <PageHeader
286
+ title={t('title')}
287
+ description={t('description')}
288
+ breadcrumbs={[
289
+ { label: t('breadcrumbs.home'), href: '/' },
290
+ { label: t('breadcrumbs.billing'), href: '/billing' },
291
+ { label: t('breadcrumbs.products') },
292
+ ]}
293
+ actions={[
294
+ {
295
+ label: t('actions.create'),
296
+ onClick: () => {
297
+ setEditingProduct(null);
298
+ setSheetOpen(true);
299
+ },
300
+ icon: <Plus className="size-4" />,
301
+ },
302
+ ]}
303
+ />
304
+
305
+ <SearchBar
306
+ searchQuery={search}
307
+ onSearchChange={(value) => {
308
+ setSearch(value);
309
+ setPage(1);
310
+ }}
311
+ onSearch={() => {
312
+ setPage(1);
313
+ void refetch();
314
+ }}
315
+ placeholder={t('filters.searchPlaceholder')}
316
+ filters={{
317
+ value: statusFilter,
318
+ options: [
319
+ { label: t('filters.statusAll'), value: 'all' },
320
+ { label: t('filters.statusActive'), value: 'active' },
321
+ { label: t('filters.statusInactive'), value: 'inactive' },
322
+ ],
323
+ onChange: (value) => {
324
+ setStatusFilter(value);
325
+ setPage(1);
326
+ },
327
+ placeholder: t('filters.statusLabel'),
328
+ }}
329
+ />
330
+
331
+ <div className="overflow-x-auto rounded-md border">
332
+ <Table>
333
+ <TableHeader>
334
+ <TableRow>
335
+ <TableHead>{t('table.columns.name')}</TableHead>
336
+ <TableHead>{t('table.columns.code')}</TableHead>
337
+ <TableHead>{t('table.columns.productType')}</TableHead>
338
+ <TableHead>{t('table.columns.isActive')}</TableHead>
339
+ <TableHead>{t('table.columns.createdAt')}</TableHead>
340
+ <TableHead className="w-[120px] text-right">
341
+ {t('table.columns.actions')}
342
+ </TableHead>
343
+ </TableRow>
344
+ </TableHeader>
345
+ <TableBody>
346
+ {items.length === 0 && (
347
+ <TableRow>
348
+ <TableCell
349
+ colSpan={6}
350
+ className="text-center text-muted-foreground"
351
+ >
352
+ {t('table.empty')}
353
+ </TableCell>
354
+ </TableRow>
355
+ )}
356
+ {items.map((item) => (
357
+ <TableRow key={item.id}>
358
+ <TableCell className="font-medium">{item.name}</TableCell>
359
+ <TableCell>{item.code ?? '-'}</TableCell>
360
+ <TableCell>
361
+ <Badge className={statusBadgeClass('draft')}>
362
+ {mapProductTypeFromApi(item.product_type)}
363
+ </Badge>
364
+ </TableCell>
365
+ <TableCell>
366
+ <Badge
367
+ className={statusBadgeClass(
368
+ item.is_active ? 'active' : 'inactive'
369
+ )}
370
+ >
371
+ {item.is_active ? t('status.active') : t('status.inactive')}
372
+ </Badge>
373
+ </TableCell>
374
+ <TableCell>
375
+ {new Intl.DateTimeFormat('pt-BR', {
376
+ dateStyle: 'short',
377
+ }).format(new Date(item.created_at))}
378
+ </TableCell>
379
+ <TableCell>
380
+ <div className="flex justify-end gap-2">
381
+ <Button
382
+ variant="outline"
383
+ size="icon"
384
+ onClick={() => {
385
+ setEditingProduct(item);
386
+ setSheetOpen(true);
387
+ }}
388
+ >
389
+ <Pencil className="size-4" />
390
+ </Button>
391
+ <Button
392
+ variant="destructive"
393
+ size="icon"
394
+ onClick={() => setDeleteId(item.id)}
395
+ >
396
+ <Trash2 className="size-4" />
397
+ </Button>
398
+ </div>
399
+ </TableCell>
400
+ </TableRow>
401
+ ))}
402
+ </TableBody>
403
+ </Table>
404
+ </div>
405
+
406
+ <PaginationFooter
407
+ currentPage={page}
408
+ pageSize={pageSize}
409
+ totalItems={totalItems}
410
+ onPageChange={setPage}
411
+ onPageSizeChange={(nextSize) => {
412
+ setPageSize(nextSize);
413
+ setPage(1);
414
+ }}
415
+ selectedCount={stats.active + stats.inactive}
416
+ />
417
+
418
+ <Sheet
419
+ open={sheetOpen}
420
+ onOpenChange={(open) => {
421
+ setSheetOpen(open);
422
+ if (!open) {
423
+ setEditingProduct(null);
424
+ }
425
+ }}
426
+ >
427
+ <SheetContent className="w-full sm:max-w-xl">
428
+ <SheetHeader>
429
+ <SheetTitle>
430
+ {editingProduct ? t('sheet.editTitle') : t('sheet.createTitle')}
431
+ </SheetTitle>
432
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
433
+ </SheetHeader>
434
+ <Form {...form}>
435
+ <form
436
+ onSubmit={form.handleSubmit(onSubmit)}
437
+ className="space-y-4 p-4"
438
+ >
439
+ <FormField
440
+ control={form.control}
441
+ name="name"
442
+ render={({ field }) => (
443
+ <FormItem>
444
+ <FormLabel>{t('form.name')}</FormLabel>
445
+ <FormControl>
446
+ <Input
447
+ placeholder={t('form.namePlaceholder')}
448
+ {...field}
449
+ />
450
+ </FormControl>
451
+ <FormMessage />
452
+ </FormItem>
453
+ )}
454
+ />
455
+
456
+ <FormField
457
+ control={form.control}
458
+ name="code"
459
+ render={({ field }) => (
460
+ <FormItem>
461
+ <FormLabel>{t('form.code')}</FormLabel>
462
+ <FormControl>
463
+ <Input
464
+ placeholder={t('form.codePlaceholder')}
465
+ {...field}
466
+ value={field.value ?? ''}
467
+ />
468
+ </FormControl>
469
+ <FormMessage />
470
+ </FormItem>
471
+ )}
472
+ />
473
+
474
+ <FormField
475
+ control={form.control}
476
+ name="description"
477
+ render={({ field }) => (
478
+ <FormItem>
479
+ <FormLabel>{t('form.description')}</FormLabel>
480
+ <FormControl>
481
+ <Textarea
482
+ placeholder={t('form.descriptionPlaceholder')}
483
+ {...field}
484
+ value={field.value ?? ''}
485
+ />
486
+ </FormControl>
487
+ <FormMessage />
488
+ </FormItem>
489
+ )}
490
+ />
491
+
492
+ <FormField
493
+ control={form.control}
494
+ name="product_type"
495
+ render={({ field }) => (
496
+ <FormItem>
497
+ <FormLabel>{t('form.productType')}</FormLabel>
498
+ <Select value={field.value} onValueChange={field.onChange}>
499
+ <FormControl>
500
+ <SelectTrigger className="w-full">
501
+ <SelectValue
502
+ placeholder={t('form.productTypePlaceholder')}
503
+ />
504
+ </SelectTrigger>
505
+ </FormControl>
506
+ <SelectContent>
507
+ <SelectItem value="digital">digital</SelectItem>
508
+ <SelectItem value="service">service</SelectItem>
509
+ <SelectItem value="physical">physical</SelectItem>
510
+ <SelectItem value="bundle">bundle</SelectItem>
511
+ </SelectContent>
512
+ </Select>
513
+ <FormMessage />
514
+ </FormItem>
515
+ )}
516
+ />
517
+
518
+ <FormField
519
+ control={form.control}
520
+ name="is_active"
521
+ render={({ field }) => (
522
+ <FormItem className="flex items-center justify-between rounded-md border p-3">
523
+ <FormLabel>{t('form.isActive')}</FormLabel>
524
+ <FormControl>
525
+ <Switch
526
+ checked={field.value}
527
+ onCheckedChange={field.onChange}
528
+ />
529
+ </FormControl>
530
+ <FormMessage />
531
+ </FormItem>
532
+ )}
533
+ />
534
+
535
+ <Button
536
+ type="submit"
537
+ className="w-full"
538
+ disabled={form.formState.isSubmitting}
539
+ >
540
+ {t('actions.save')}
541
+ </Button>
542
+ </form>
543
+ </Form>
544
+ </SheetContent>
545
+ </Sheet>
546
+
547
+ <AlertDialog
548
+ open={deleteId !== null}
549
+ onOpenChange={(open) => !open && setDeleteId(null)}
550
+ >
551
+ <AlertDialogContent>
552
+ <AlertDialogHeader>
553
+ <AlertDialogTitle>{t('deleteDialog.title')}</AlertDialogTitle>
554
+ <AlertDialogDescription>
555
+ {t('deleteDialog.description')}
556
+ </AlertDialogDescription>
557
+ </AlertDialogHeader>
558
+ <AlertDialogFooter>
559
+ <AlertDialogCancel>{t('deleteDialog.cancel')}</AlertDialogCancel>
560
+ <AlertDialogAction onClick={onDelete}>
561
+ {t('deleteDialog.confirm')}
562
+ </AlertDialogAction>
563
+ </AlertDialogFooter>
564
+ </AlertDialogContent>
565
+ </AlertDialog>
566
+ </Page>
567
+ );
568
+ }