@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,424 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Page,
5
+ PageHeader,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
9
+ import { Badge } from '@/components/ui/badge';
10
+ import { Button } from '@/components/ui/button';
11
+ import {
12
+ Form,
13
+ FormControl,
14
+ FormField,
15
+ FormItem,
16
+ FormLabel,
17
+ FormMessage,
18
+ } from '@/components/ui/form';
19
+ import { Input } from '@/components/ui/input';
20
+ import { InputMoney } from '@/components/ui/input-money';
21
+ import {
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from '@/components/ui/select';
28
+ import {
29
+ Sheet,
30
+ SheetContent,
31
+ SheetDescription,
32
+ SheetHeader,
33
+ SheetTitle,
34
+ } from '@/components/ui/sheet';
35
+ import {
36
+ Table,
37
+ TableBody,
38
+ TableCell,
39
+ TableHead,
40
+ TableHeader,
41
+ TableRow,
42
+ } from '@/components/ui/table';
43
+ import { Textarea } from '@/components/ui/textarea';
44
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
45
+ import { zodResolver } from '@hookform/resolvers/zod';
46
+ import { Plus } from 'lucide-react';
47
+ import { useTranslations } from 'next-intl';
48
+ import { useState } from 'react';
49
+ import { useForm } from 'react-hook-form';
50
+ import { z } from 'zod';
51
+
52
+ type BillingOrder = {
53
+ id: number;
54
+ status: string;
55
+ currency: string;
56
+ total_cents: number;
57
+ source: string | null;
58
+ created_at: string;
59
+ };
60
+
61
+ type ListResponse<T> = {
62
+ data?: T[];
63
+ total?: number;
64
+ };
65
+
66
+ const schema = z.object({
67
+ contact_id: z.coerce.number().optional(),
68
+ status: z.enum(['pending', 'paid', 'canceled', 'expired']),
69
+ currency: z.string().default('BRL'),
70
+ subtotal_cents: z.coerce.number().min(0),
71
+ total_cents: z.coerce.number().min(0),
72
+ source: z.string().optional(),
73
+ notes: z.string().optional(),
74
+ });
75
+
76
+ type OrderFormValues = z.infer<typeof schema>;
77
+
78
+ const formatCurrency = (cents: number, currency = 'BRL') =>
79
+ new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(
80
+ cents / 100
81
+ );
82
+
83
+ const badgeClass = (value: string) => {
84
+ if (['active', 'paid', 'authorized', 'approved'].includes(value)) {
85
+ return 'bg-green-100 text-green-800';
86
+ }
87
+ if (['pending', 'open', 'draft'].includes(value)) {
88
+ return 'bg-yellow-100 text-yellow-800';
89
+ }
90
+ if (['failed', 'canceled', 'void', 'expired'].includes(value)) {
91
+ return 'bg-red-100 text-red-800';
92
+ }
93
+ if (['paused', 'inactive'].includes(value)) {
94
+ return 'bg-gray-100 text-gray-800';
95
+ }
96
+ return 'bg-slate-100 text-slate-800';
97
+ };
98
+
99
+ export default function BillingOrdersPage() {
100
+ const t = useTranslations('BillingOrdersPage');
101
+ const { request, currentLocaleCode, showToastHandler } = useApp();
102
+ const [search, setSearch] = useState('');
103
+ const [page, setPage] = useState(1);
104
+ const [pageSize, setPageSize] = useState(12);
105
+ const [sheetOpen, setSheetOpen] = useState(false);
106
+
107
+ const form = useForm<OrderFormValues>({
108
+ resolver: zodResolver(schema),
109
+ defaultValues: {
110
+ contact_id: undefined,
111
+ status: 'pending',
112
+ currency: 'BRL',
113
+ subtotal_cents: 0,
114
+ total_cents: 0,
115
+ source: '',
116
+ notes: '',
117
+ },
118
+ });
119
+
120
+ const { data, refetch } = useQuery<{ items: BillingOrder[]; total: number }>({
121
+ queryKey: ['billing-orders', currentLocaleCode, page, pageSize, search],
122
+ queryFn: async () => {
123
+ const response = await request<ListResponse<BillingOrder>>({
124
+ url: '/billing/orders',
125
+ method: 'GET',
126
+ params: {
127
+ page,
128
+ pageSize,
129
+ search: search.trim() || undefined,
130
+ },
131
+ });
132
+
133
+ const payload = response.data as
134
+ | ListResponse<BillingOrder>
135
+ | BillingOrder[];
136
+ if (Array.isArray(payload)) {
137
+ return { items: payload, total: payload.length };
138
+ }
139
+
140
+ return {
141
+ items: payload.data ?? [],
142
+ total: payload.total ?? payload.data?.length ?? 0,
143
+ };
144
+ },
145
+ placeholderData: (old) => old,
146
+ });
147
+
148
+ const items = data?.items ?? [];
149
+ const totalItems = data?.total ?? 0;
150
+
151
+ const onSubmit = async (values: OrderFormValues) => {
152
+ try {
153
+ await request({
154
+ url: '/billing/orders',
155
+ method: 'POST',
156
+ data: {
157
+ contact_id: values.contact_id,
158
+ status: values.status,
159
+ currency: values.currency,
160
+ subtotal_cents: values.subtotal_cents,
161
+ total_cents: values.total_cents,
162
+ source: values.source?.trim() || undefined,
163
+ notes: values.notes?.trim() || undefined,
164
+ },
165
+ });
166
+
167
+ showToastHandler?.('success', t('messages.createSuccess'));
168
+ setSheetOpen(false);
169
+ form.reset();
170
+ await refetch();
171
+ } catch {
172
+ showToastHandler?.('error', t('messages.createError'));
173
+ }
174
+ };
175
+
176
+ return (
177
+ <Page>
178
+ <PageHeader
179
+ title={t('title')}
180
+ description={t('description')}
181
+ breadcrumbs={[
182
+ { label: t('breadcrumbs.home'), href: '/' },
183
+ { label: t('breadcrumbs.billing'), href: '/billing' },
184
+ { label: t('breadcrumbs.orders') },
185
+ ]}
186
+ actions={[
187
+ {
188
+ label: t('actions.create'),
189
+ onClick: () => setSheetOpen(true),
190
+ icon: <Plus className="size-4" />,
191
+ },
192
+ ]}
193
+ />
194
+
195
+ <SearchBar
196
+ searchQuery={search}
197
+ onSearchChange={(value) => {
198
+ setSearch(value);
199
+ setPage(1);
200
+ }}
201
+ onSearch={() => {
202
+ setPage(1);
203
+ void refetch();
204
+ }}
205
+ placeholder={t('filters.searchPlaceholder')}
206
+ />
207
+
208
+ <div className="overflow-x-auto rounded-md border">
209
+ <Table>
210
+ <TableHeader>
211
+ <TableRow>
212
+ <TableHead>{t('table.columns.id')}</TableHead>
213
+ <TableHead>{t('table.columns.status')}</TableHead>
214
+ <TableHead>{t('table.columns.currency')}</TableHead>
215
+ <TableHead>{t('table.columns.total')}</TableHead>
216
+ <TableHead>{t('table.columns.source')}</TableHead>
217
+ <TableHead>{t('table.columns.createdAt')}</TableHead>
218
+ </TableRow>
219
+ </TableHeader>
220
+ <TableBody>
221
+ {items.length === 0 && (
222
+ <TableRow>
223
+ <TableCell
224
+ colSpan={6}
225
+ className="text-center text-muted-foreground"
226
+ >
227
+ {t('table.empty')}
228
+ </TableCell>
229
+ </TableRow>
230
+ )}
231
+ {items.map((item) => (
232
+ <TableRow key={item.id}>
233
+ <TableCell className="font-medium">#{item.id}</TableCell>
234
+ <TableCell>
235
+ <Badge className={badgeClass(item.status)}>
236
+ {item.status}
237
+ </Badge>
238
+ </TableCell>
239
+ <TableCell>{item.currency}</TableCell>
240
+ <TableCell>
241
+ {formatCurrency(item.total_cents, item.currency)}
242
+ </TableCell>
243
+ <TableCell>{item.source ?? '-'}</TableCell>
244
+ <TableCell>
245
+ {new Intl.DateTimeFormat('pt-BR', {
246
+ dateStyle: 'short',
247
+ }).format(new Date(item.created_at))}
248
+ </TableCell>
249
+ </TableRow>
250
+ ))}
251
+ </TableBody>
252
+ </Table>
253
+ </div>
254
+
255
+ <PaginationFooter
256
+ currentPage={page}
257
+ pageSize={pageSize}
258
+ totalItems={totalItems}
259
+ onPageChange={setPage}
260
+ onPageSizeChange={(nextSize) => {
261
+ setPageSize(nextSize);
262
+ setPage(1);
263
+ }}
264
+ />
265
+
266
+ <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
267
+ <SheetContent className="w-full sm:max-w-xl">
268
+ <SheetHeader>
269
+ <SheetTitle>{t('sheet.createTitle')}</SheetTitle>
270
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
271
+ </SheetHeader>
272
+
273
+ <Form {...form}>
274
+ <form
275
+ className="space-y-4 p-4"
276
+ onSubmit={form.handleSubmit(onSubmit)}
277
+ >
278
+ <div className="grid grid-cols-2 gap-4">
279
+ <FormField
280
+ control={form.control}
281
+ name="contact_id"
282
+ render={({ field }) => (
283
+ <FormItem>
284
+ <FormLabel>{t('form.contactId')}</FormLabel>
285
+ <FormControl>
286
+ <Input
287
+ type="number"
288
+ min={1}
289
+ value={field.value ?? ''}
290
+ onChange={(event) =>
291
+ field.onChange(
292
+ event.target.value
293
+ ? Number(event.target.value)
294
+ : undefined
295
+ )
296
+ }
297
+ />
298
+ </FormControl>
299
+ <FormMessage />
300
+ </FormItem>
301
+ )}
302
+ />
303
+ <FormField
304
+ control={form.control}
305
+ name="status"
306
+ render={({ field }) => (
307
+ <FormItem>
308
+ <FormLabel>{t('form.status')}</FormLabel>
309
+ <Select
310
+ value={field.value}
311
+ onValueChange={field.onChange}
312
+ >
313
+ <FormControl>
314
+ <SelectTrigger className="w-full">
315
+ <SelectValue />
316
+ </SelectTrigger>
317
+ </FormControl>
318
+ <SelectContent>
319
+ <SelectItem value="pending">pending</SelectItem>
320
+ <SelectItem value="paid">paid</SelectItem>
321
+ <SelectItem value="canceled">canceled</SelectItem>
322
+ <SelectItem value="expired">expired</SelectItem>
323
+ </SelectContent>
324
+ </Select>
325
+ <FormMessage />
326
+ </FormItem>
327
+ )}
328
+ />
329
+ </div>
330
+
331
+ <div className="grid grid-cols-3 gap-4">
332
+ <FormField
333
+ control={form.control}
334
+ name="currency"
335
+ render={({ field }) => (
336
+ <FormItem>
337
+ <FormLabel>{t('form.currency')}</FormLabel>
338
+ <FormControl>
339
+ <Input {...field} value={field.value ?? 'BRL'} />
340
+ </FormControl>
341
+ <FormMessage />
342
+ </FormItem>
343
+ )}
344
+ />
345
+ <FormField
346
+ control={form.control}
347
+ name="subtotal_cents"
348
+ render={({ field }) => (
349
+ <FormItem>
350
+ <FormLabel>{t('form.subtotal')}</FormLabel>
351
+ <FormControl>
352
+ <InputMoney
353
+ value={(field.value ?? 0) / 100}
354
+ onValueChange={(value) =>
355
+ field.onChange(Math.round((value ?? 0) * 100))
356
+ }
357
+ />
358
+ </FormControl>
359
+ <FormMessage />
360
+ </FormItem>
361
+ )}
362
+ />
363
+ <FormField
364
+ control={form.control}
365
+ name="total_cents"
366
+ render={({ field }) => (
367
+ <FormItem>
368
+ <FormLabel>{t('form.total')}</FormLabel>
369
+ <FormControl>
370
+ <InputMoney
371
+ value={(field.value ?? 0) / 100}
372
+ onValueChange={(value) =>
373
+ field.onChange(Math.round((value ?? 0) * 100))
374
+ }
375
+ />
376
+ </FormControl>
377
+ <FormMessage />
378
+ </FormItem>
379
+ )}
380
+ />
381
+ </div>
382
+
383
+ <FormField
384
+ control={form.control}
385
+ name="source"
386
+ render={({ field }) => (
387
+ <FormItem>
388
+ <FormLabel>{t('form.source')}</FormLabel>
389
+ <FormControl>
390
+ <Input {...field} value={field.value ?? ''} />
391
+ </FormControl>
392
+ <FormMessage />
393
+ </FormItem>
394
+ )}
395
+ />
396
+
397
+ <FormField
398
+ control={form.control}
399
+ name="notes"
400
+ render={({ field }) => (
401
+ <FormItem>
402
+ <FormLabel>{t('form.notes')}</FormLabel>
403
+ <FormControl>
404
+ <Textarea {...field} value={field.value ?? ''} />
405
+ </FormControl>
406
+ <FormMessage />
407
+ </FormItem>
408
+ )}
409
+ />
410
+
411
+ <Button
412
+ type="submit"
413
+ className="w-full"
414
+ disabled={form.formState.isSubmitting}
415
+ >
416
+ {t('actions.save')}
417
+ </Button>
418
+ </form>
419
+ </Form>
420
+ </SheetContent>
421
+ </Sheet>
422
+ </Page>
423
+ );
424
+ }
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Page,
5
+ PageHeader,
6
+ StatsCards,
7
+ type StatCardConfig,
8
+ } from '@/components/entity-list';
9
+ import {
10
+ Card,
11
+ CardContent,
12
+ CardDescription,
13
+ CardHeader,
14
+ CardTitle,
15
+ } from '@/components/ui/card';
16
+ import {
17
+ Table,
18
+ TableBody,
19
+ TableCell,
20
+ TableHead,
21
+ TableHeader,
22
+ TableRow,
23
+ } from '@/components/ui/table';
24
+ import {
25
+ AlertTriangle,
26
+ CreditCard,
27
+ FileWarning,
28
+ RefreshCw,
29
+ } from 'lucide-react';
30
+ import { useTranslations } from 'next-intl';
31
+ import {
32
+ CartesianGrid,
33
+ Legend,
34
+ Line,
35
+ LineChart,
36
+ ResponsiveContainer,
37
+ Tooltip,
38
+ XAxis,
39
+ YAxis,
40
+ } from 'recharts';
41
+ import {
42
+ MOCK_DASHBOARD_STATS,
43
+ MOCK_PAYMENTS,
44
+ MOCK_REVENUE_CHART,
45
+ } from './_lib/billing-mocks';
46
+
47
+ const formatCurrency = (cents: number, currency = 'BRL') =>
48
+ new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(
49
+ cents / 100
50
+ );
51
+
52
+ const formatDate = (value: string | null | undefined) => {
53
+ if (!value) {
54
+ return '-';
55
+ }
56
+
57
+ return new Intl.DateTimeFormat('pt-BR', {
58
+ dateStyle: 'short',
59
+ timeStyle: 'short',
60
+ }).format(new Date(value));
61
+ };
62
+
63
+ export default function BillingDashboardPage() {
64
+ const t = useTranslations('BillingDashboardPage');
65
+
66
+ const stats: StatCardConfig[] = [
67
+ {
68
+ title: t('stats.mrr'),
69
+ value: formatCurrency(MOCK_DASHBOARD_STATS.mrr),
70
+ icon: <RefreshCw className="size-5" />,
71
+ iconBgColor: 'bg-blue-50',
72
+ iconColor: 'text-blue-600',
73
+ },
74
+ {
75
+ title: t('stats.activeSubscriptions'),
76
+ value: MOCK_DASHBOARD_STATS.activeSubscriptions,
77
+ icon: <CreditCard className="size-5" />,
78
+ iconBgColor: 'bg-green-50',
79
+ iconColor: 'text-green-600',
80
+ },
81
+ {
82
+ title: t('stats.failedPayments'),
83
+ value: MOCK_DASHBOARD_STATS.failedPayments,
84
+ icon: <AlertTriangle className="size-5" />,
85
+ iconBgColor: 'bg-yellow-50',
86
+ iconColor: 'text-yellow-600',
87
+ },
88
+ {
89
+ title: t('stats.overdueInvoices'),
90
+ value: MOCK_DASHBOARD_STATS.overdueInvoices,
91
+ icon: <FileWarning className="size-5" />,
92
+ iconBgColor: 'bg-red-50',
93
+ iconColor: 'text-red-600',
94
+ },
95
+ ];
96
+
97
+ return (
98
+ <Page>
99
+ <PageHeader
100
+ title={t('title')}
101
+ description={t('description')}
102
+ breadcrumbs={[
103
+ { label: t('breadcrumbs.home'), href: '/' },
104
+ { label: t('breadcrumbs.billing'), href: '/billing' },
105
+ { label: t('breadcrumbs.dashboard') },
106
+ ]}
107
+ />
108
+
109
+ <StatsCards stats={stats} />
110
+
111
+ <Card>
112
+ <CardHeader>
113
+ <CardTitle>{t('revenueTrend.title')}</CardTitle>
114
+ <CardDescription>{t('revenueTrend.description')}</CardDescription>
115
+ </CardHeader>
116
+ <CardContent>
117
+ <ResponsiveContainer width="100%" height={320}>
118
+ <LineChart data={MOCK_REVENUE_CHART}>
119
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
120
+ <XAxis dataKey="month" tickLine={false} axisLine={false} />
121
+ <YAxis
122
+ tickLine={false}
123
+ axisLine={false}
124
+ tickFormatter={(value) => `R$ ${(value / 100000).toFixed(0)}k`}
125
+ />
126
+ <Tooltip
127
+ formatter={(value: number) => formatCurrency(value)}
128
+ contentStyle={{
129
+ backgroundColor: 'hsl(var(--background))',
130
+ border: '1px solid hsl(var(--border))',
131
+ borderRadius: '8px',
132
+ }}
133
+ />
134
+ <Legend />
135
+ <Line
136
+ type="monotone"
137
+ dataKey="revenue_cents"
138
+ name={t('revenueTrend.legend')}
139
+ stroke="#2563eb"
140
+ strokeWidth={3}
141
+ dot={{ r: 4, fill: '#2563eb' }}
142
+ />
143
+ </LineChart>
144
+ </ResponsiveContainer>
145
+ </CardContent>
146
+ </Card>
147
+
148
+ <Card>
149
+ <CardHeader>
150
+ <CardTitle>{t('recentPayments.title')}</CardTitle>
151
+ <CardDescription>{t('recentPayments.description')}</CardDescription>
152
+ </CardHeader>
153
+ <CardContent>
154
+ <div className="overflow-x-auto rounded-md border">
155
+ <Table>
156
+ <TableHeader>
157
+ <TableRow>
158
+ <TableHead>{t('recentPayments.columns.provider')}</TableHead>
159
+ <TableHead>{t('recentPayments.columns.method')}</TableHead>
160
+ <TableHead>{t('recentPayments.columns.status')}</TableHead>
161
+ <TableHead>{t('recentPayments.columns.amount')}</TableHead>
162
+ <TableHead>{t('recentPayments.columns.paidAt')}</TableHead>
163
+ </TableRow>
164
+ </TableHeader>
165
+ <TableBody>
166
+ {MOCK_PAYMENTS.slice(0, 10).map((payment) => (
167
+ <TableRow key={payment.id}>
168
+ <TableCell className="font-medium">
169
+ {payment.provider}
170
+ </TableCell>
171
+ <TableCell>{payment.method_type}</TableCell>
172
+ <TableCell>{payment.status}</TableCell>
173
+ <TableCell>
174
+ {formatCurrency(payment.amount_cents, payment.currency)}
175
+ </TableCell>
176
+ <TableCell>{formatDate(payment.paid_at)}</TableCell>
177
+ </TableRow>
178
+ ))}
179
+ </TableBody>
180
+ </Table>
181
+ </div>
182
+ </CardContent>
183
+ </Card>
184
+ </Page>
185
+ );
186
+ }