@greatapps/common 1.1.6 → 1.1.8
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/dist/components/layouts/ProfilePopover.mjs +16 -5
- package/dist/components/layouts/ProfilePopover.mjs.map +1 -1
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -1
- package/dist/infra/api/client.mjs +14 -6
- package/dist/infra/api/client.mjs.map +1 -1
- package/dist/modules/auth/utils/get-user-context.mjs +24 -0
- package/dist/modules/auth/utils/get-user-context.mjs.map +1 -0
- package/dist/modules/ia-credits/actions/list-ia-credits.action.mjs +9 -0
- package/dist/modules/ia-credits/actions/list-ia-credits.action.mjs.map +1 -0
- package/dist/modules/ia-credits/hooks/ia-credits.hook.mjs +75 -0
- package/dist/modules/ia-credits/hooks/ia-credits.hook.mjs.map +1 -0
- package/dist/modules/ia-credits/services/ia-credits.service.mjs +31 -0
- package/dist/modules/ia-credits/services/ia-credits.service.mjs.map +1 -0
- package/dist/modules/ia-credits/types.mjs +25 -0
- package/dist/modules/ia-credits/types.mjs.map +1 -0
- package/dist/modules/subscriptions/actions/list-subscriptions.action.mjs +9 -0
- package/dist/modules/subscriptions/actions/list-subscriptions.action.mjs.map +1 -0
- package/dist/modules/subscriptions/hooks/list-subscriptions.hook.mjs +14 -0
- package/dist/modules/subscriptions/hooks/list-subscriptions.hook.mjs.map +1 -0
- package/dist/modules/subscriptions/services/subscriptions.service.mjs +32 -0
- package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -0
- package/dist/modules/subscriptions/types.mjs +12 -0
- package/dist/modules/subscriptions/types.mjs.map +1 -0
- package/dist/modules/users/services/user.service.mjs +1 -1
- package/dist/modules/users/services/user.service.mjs.map +1 -1
- package/dist/modules/whitelabel/services/whitelabel.service.mjs +2 -2
- package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
- package/dist/server.mjs +12 -1
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/layouts/ProfilePopover.tsx +115 -97
- package/src/index.ts +4 -0
- package/src/infra/api/client.ts +19 -8
- package/src/modules/auth/utils/get-user-context.ts +29 -0
- package/src/modules/ia-credits/actions/list-ia-credits.action.ts +11 -0
- package/src/modules/ia-credits/hooks/ia-credits.hook.ts +96 -0
- package/src/modules/ia-credits/services/ia-credits.service.ts +37 -0
- package/src/modules/ia-credits/types.ts +33 -0
- package/src/modules/subscriptions/actions/list-subscriptions.action.ts +11 -0
- package/src/modules/subscriptions/hooks/list-subscriptions.hook.ts +14 -0
- package/src/modules/subscriptions/services/subscriptions.service.ts +38 -0
- package/src/modules/subscriptions/types.ts +17 -0
- package/src/modules/users/services/user.service.ts +1 -1
- package/src/modules/whitelabel/services/whitelabel.service.ts +2 -2
- package/src/server.ts +6 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/whitelabel/services/whitelabel.service.ts"],"sourcesContent":["\r\nimport greatCache from '@greatapps/cache';\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { WhitelabelTokenApiResponse, WhitelabelTokenData } from '../schema';\r\n\r\nclass WhitelabelService {\r\n private getApiUrl(): string {\r\n const apiUrl = process.env.
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/whitelabel/services/whitelabel.service.ts"],"sourcesContent":["\r\nimport greatCache from '@greatapps/cache';\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { WhitelabelTokenApiResponse, WhitelabelTokenData } from '../schema';\r\n\r\nclass WhitelabelService {\r\n private getApiUrl(): string {\r\n const apiUrl = process.env.GAPPS_R3_API_URL;\r\n if (!apiUrl) {\r\n throw new ApiError('GAPPS_R3_API_URL not configured', 'CONFIG_ERROR', 500);\r\n }\r\n return apiUrl;\r\n }\r\n\r\n private getToken(): string {\r\n const token = process.env.WHITELABEL_TOKEN_MASTER;\r\n if (!token) {\r\n throw new ApiError('WHITELABEL_TOKEN_MASTER not configured', 'CONFIG_ERROR', 500);\r\n }\r\n return token;\r\n }\r\n\r\n async getTokenByDomain(hostname: string): Promise<WhitelabelTokenData> {\r\n hostname = 'greatapps.com.br';\r\n\r\n const apiUrl = this.getApiUrl();\r\n const whitelabelMasterToken = this.getToken();\r\n\r\n const url = `${apiUrl}/v1/pt-br/1/whitelabel/${hostname}/token`;\r\n\r\n console.log('[WhitelabelService] Fetching token for domain', { url });\r\n\r\n const cache = new greatCache({\r\n service: 'whitelabel-service',\r\n version: '1.0',\r\n domain: 'whitelabel-cache.greatapps.com.br',\r\n ambient: process.env.NODE_ENV || 'development',\r\n });\r\n\r\n const cachedData = await cache.select(`whitelabel-token-${hostname}`);\r\n\r\n if (cachedData.status == 1 && 'data' in cachedData && cachedData.data) {\r\n console.log('[WhitelabelService] Cache hit for domain', { hostname });\r\n\r\n return JSON.parse(cachedData.data) as WhitelabelTokenData;\r\n }\r\n\r\n const response = await fetch(url, {\r\n method: 'GET',\r\n headers: {\r\n authorization: whitelabelMasterToken,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n console.error('[WhitelabelService] Failed to fetch whitelabel token', { response });\r\n throw new ApiError(\r\n `Failed to fetch whitelabel token: ${response.status}`,\r\n 'FETCH_ERROR',\r\n response.status\r\n );\r\n }\r\n\r\n const result: WhitelabelTokenApiResponse = await response.json();\r\n\r\n if (result.status !== 1 || !result.data?.length) {\r\n throw new ApiError('Whitelabel token not found', 'TOKEN_NOT_FOUND', 404);\r\n }\r\n\r\n await cache.insert(`whitelabel-token-${hostname}`, JSON.stringify(result.data[0]), 604800);\r\n\r\n return result.data[0];\r\n }\r\n}\r\n\r\nexport const whitelabelService = new WhitelabelService();\r\n"],"mappings":"AACA,OAAO,gBAAgB;AACvB,SAAS,gBAAgB;AAGzB,MAAM,kBAAkB;AAAA,EACd,YAAoB;AAC1B,UAAM,SAAS,QAAQ,IAAI;AAC3B,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,SAAS,mCAAmC,gBAAgB,GAAG;AAAA,IAC3E;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,WAAmB;AACzB,UAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,SAAS,0CAA0C,gBAAgB,GAAG;AAAA,IAClF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,iBAAiB,UAAgD;AACrE,eAAW;AAEX,UAAM,SAAS,KAAK,UAAU;AAC9B,UAAM,wBAAwB,KAAK,SAAS;AAE5C,UAAM,MAAM,GAAG,MAAM,0BAA0B,QAAQ;AAEvD,YAAQ,IAAI,iDAAiD,EAAE,IAAI,CAAC;AAEpE,UAAM,QAAQ,IAAI,WAAW;AAAA,MAC3B,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS,QAAQ,IAAI,YAAY;AAAA,IACnC,CAAC;AAED,UAAM,aAAa,MAAM,MAAM,OAAO,oBAAoB,QAAQ,EAAE;AAEpE,QAAI,WAAW,UAAU,KAAK,UAAU,cAAc,WAAW,MAAM;AACrE,cAAQ,IAAI,4CAA4C,EAAE,SAAS,CAAC;AAEpE,aAAO,KAAK,MAAM,WAAW,IAAI;AAAA,IACnC;AAEA,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,wDAAwD,EAAE,SAAS,CAAC;AAClF,YAAM,IAAI;AAAA,QACR,qCAAqC,SAAS,MAAM;AAAA,QACpD;AAAA,QACA,SAAS;AAAA,MACX;AAAA,IACF;AAEA,UAAM,SAAqC,MAAM,SAAS,KAAK;AAE/D,QAAI,OAAO,WAAW,KAAK,CAAC,OAAO,MAAM,QAAQ;AAC/C,YAAM,IAAI,SAAS,8BAA8B,mBAAmB,GAAG;AAAA,IACzE;AAEA,UAAM,MAAM,OAAO,oBAAoB,QAAQ,IAAI,KAAK,UAAU,OAAO,KAAK,CAAC,CAAC,GAAG,MAAM;AAEzF,WAAO,OAAO,KAAK,CAAC;AAAA,EACtB;AACF;AAEO,MAAM,oBAAoB,IAAI,kBAAkB;","names":[]}
|
package/dist/server.mjs
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
-
import {
|
|
2
|
+
import { ApiClient, api, apiClient } from "./infra/api/client";
|
|
3
3
|
import { authService } from "./modules/auth/services/auth.service";
|
|
4
4
|
import { whitelabelService } from "./modules/whitelabel/services/whitelabel.service";
|
|
5
|
+
import { subscriptionsService } from "./modules/subscriptions/services/subscriptions.service";
|
|
6
|
+
import { iaCreditsService } from "./modules/ia-credits/services/ia-credits.service";
|
|
5
7
|
import { findWhitelabel } from "./modules/whitelabel/actions/find-whitelabel.action";
|
|
6
8
|
import { validateSessionAction } from "./modules/auth/actions/validate-session.action";
|
|
9
|
+
import { listSubscriptionsAction } from "./modules/subscriptions/actions/list-subscriptions.action";
|
|
10
|
+
import { listIaCreditsAction } from "./modules/ia-credits/actions/list-ia-credits.action";
|
|
7
11
|
import { getClientInfoFromRequest } from "./infra/utils/client-info";
|
|
12
|
+
import { getUserContext } from "./modules/auth/utils/get-user-context";
|
|
8
13
|
export {
|
|
9
14
|
ApiClient,
|
|
15
|
+
api,
|
|
10
16
|
apiClient,
|
|
11
17
|
authService,
|
|
12
18
|
findWhitelabel,
|
|
13
19
|
getClientInfoFromRequest,
|
|
20
|
+
getUserContext,
|
|
21
|
+
iaCreditsService,
|
|
22
|
+
listIaCreditsAction,
|
|
23
|
+
listSubscriptionsAction,
|
|
24
|
+
subscriptionsService,
|
|
14
25
|
validateSessionAction,
|
|
15
26
|
whitelabelService
|
|
16
27
|
};
|
package/dist/server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport {
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { ApiClient, api, apiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\nexport { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';\r\nexport { iaCreditsService } from './modules/ia-credits/services/ia-credits.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { listIaCreditsAction } from './modules/ia-credits/actions/list-ia-credits.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\nexport { getUserContext } from './modules/auth/utils/get-user-context';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,KAAK,iBAAiB;AAG1C,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,4BAA4B;AACrC,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,2BAA2B;AAGpC,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from
|
|
4
|
-
import { useRouter } from
|
|
5
|
-
import * as PopoverPrimitive from
|
|
6
|
-
import { Popover } from
|
|
7
|
-
import { Button } from
|
|
8
|
-
import { Progress } from
|
|
9
|
-
import { Separator } from
|
|
10
|
-
import { NavBarItem } from
|
|
11
|
-
import { UserAvatar } from
|
|
12
|
-
import { useAuth } from
|
|
13
|
-
import { cn } from
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
6
|
+
import { Popover } from "../ui/overlay/Popover";
|
|
7
|
+
import { Button } from "../ui/buttons/Button";
|
|
8
|
+
import { Progress } from "../ui/feedback/Progress";
|
|
9
|
+
import { Separator } from "../ui/data-display/Separator";
|
|
10
|
+
import { NavBarItem } from "./NavBarItem";
|
|
11
|
+
import { UserAvatar } from "../ui/data-display/UserAvatar";
|
|
12
|
+
import { useAuth } from "../../providers/auth.provider";
|
|
13
|
+
import { cn } from "../../infra/utils/clsx";
|
|
14
|
+
import { useIaCredits } from "../../modules/ia-credits/hooks/ia-credits.hook";
|
|
14
15
|
|
|
15
16
|
export interface ProfileMenuItem {
|
|
16
17
|
icon?: React.ReactNode;
|
|
@@ -19,11 +20,10 @@ export interface ProfileMenuItem {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface ProfilePopoverProps {
|
|
22
|
-
side?:
|
|
23
|
-
align?:
|
|
23
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
24
|
+
align?: "start" | "center" | "end";
|
|
24
25
|
contentClassName?: string;
|
|
25
26
|
onEditProfile?: () => void;
|
|
26
|
-
credits?: { used: number; total: number };
|
|
27
27
|
onCreditsClick?: () => void;
|
|
28
28
|
menuItems?: ProfileMenuItem[];
|
|
29
29
|
logoutRedirect?: string;
|
|
@@ -31,35 +31,41 @@ export interface ProfilePopoverProps {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function ProfilePopover({
|
|
34
|
-
side =
|
|
35
|
-
align =
|
|
36
|
-
contentClassName =
|
|
34
|
+
side = "top",
|
|
35
|
+
align = "start",
|
|
36
|
+
contentClassName = "absolute bottom-2 left-[26px]",
|
|
37
37
|
onEditProfile,
|
|
38
|
-
credits,
|
|
39
38
|
onCreditsClick,
|
|
40
39
|
menuItems = [],
|
|
41
|
-
logoutRedirect =
|
|
40
|
+
logoutRedirect = "/login",
|
|
42
41
|
logoutOverlay,
|
|
43
42
|
}: ProfilePopoverProps) {
|
|
44
43
|
const router = useRouter();
|
|
45
44
|
const { user, logout } = useAuth();
|
|
46
45
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
|
47
46
|
|
|
47
|
+
const { summary, subscription, isSubscriptionLoading, isCreditsLoading } =
|
|
48
|
+
useIaCredits();
|
|
49
|
+
|
|
48
50
|
const handleLogout = async () => {
|
|
49
51
|
try {
|
|
50
52
|
setIsLoggingOut(true);
|
|
51
53
|
await logout();
|
|
52
54
|
router.push(logoutRedirect);
|
|
53
55
|
} catch (error) {
|
|
54
|
-
console.error(
|
|
56
|
+
console.error("Erro ao fazer logout:", error);
|
|
55
57
|
} finally {
|
|
56
58
|
setIsLoggingOut(false);
|
|
57
59
|
}
|
|
58
60
|
};
|
|
59
61
|
|
|
62
|
+
const hasSubscription = !isSubscriptionLoading && subscription !== null;
|
|
63
|
+
const showCreditsSkeleton = hasSubscription && isCreditsLoading;
|
|
64
|
+
const showCredits =
|
|
65
|
+
hasSubscription && !isCreditsLoading && summary.totalCredits > 0;
|
|
60
66
|
const creditsPercentage =
|
|
61
|
-
|
|
62
|
-
? (
|
|
67
|
+
showCredits && summary.totalCredits > 0
|
|
68
|
+
? (summary.usedCredits / summary.totalCredits) * 100
|
|
63
69
|
: 0;
|
|
64
70
|
|
|
65
71
|
return (
|
|
@@ -78,89 +84,101 @@ function ProfilePopover({
|
|
|
78
84
|
</PopoverPrimitive.Trigger>
|
|
79
85
|
|
|
80
86
|
<PopoverPrimitive.Portal>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)}
|
|
86
|
-
side={side}
|
|
87
|
-
align={align}
|
|
88
|
-
sideOffset={8}
|
|
89
|
-
>
|
|
90
|
-
<div className="grid grid-cols-[32px_1fr_auto] items-center gap-3 p-3 w-full">
|
|
91
|
-
{user && (
|
|
92
|
-
<UserAvatar
|
|
93
|
-
key={user.id}
|
|
94
|
-
photo={user.photo}
|
|
95
|
-
name={user.name}
|
|
96
|
-
size={32}
|
|
97
|
-
/>
|
|
87
|
+
<PopoverPrimitive.Content
|
|
88
|
+
className={cn(
|
|
89
|
+
"w-64.75 p-0 bg-white rounded-2xl shadow-md border border-gray-200 z-99",
|
|
90
|
+
contentClassName,
|
|
98
91
|
)}
|
|
92
|
+
side={side}
|
|
93
|
+
align={align}
|
|
94
|
+
sideOffset={8}
|
|
95
|
+
>
|
|
96
|
+
<div className="grid grid-cols-[32px_1fr_auto] items-center gap-3 p-3 w-full">
|
|
97
|
+
{user && (
|
|
98
|
+
<UserAvatar
|
|
99
|
+
key={user.id}
|
|
100
|
+
photo={user.photo}
|
|
101
|
+
name={user.name}
|
|
102
|
+
size={32}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
107
|
+
<span className="paragraph-small-semibold text-gray-950 truncate">
|
|
108
|
+
{user?.name || ""}
|
|
109
|
+
</span>
|
|
110
|
+
<span className="paragraph-xsmall-medium text-gray-500 truncate">
|
|
111
|
+
{user?.email || ""}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
{onEditProfile && (
|
|
116
|
+
<Button
|
|
117
|
+
variant="secondary"
|
|
118
|
+
className="paragraph-small-semibold h-8! whitespace-nowrap px-3 w-fit"
|
|
119
|
+
onClick={onEditProfile}
|
|
120
|
+
>
|
|
121
|
+
Editar Perfil
|
|
122
|
+
</Button>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
119
125
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<span className="paragraph-xsmall-semibold text-white truncate">
|
|
131
|
-
Créditos usados
|
|
132
|
-
</span>
|
|
133
|
-
<span className="paragraph-xsmall-medium text-white/50">
|
|
134
|
-
{credits.used.toLocaleString('pt-BR')}/
|
|
135
|
-
{credits.total.toLocaleString('pt-BR')}
|
|
136
|
-
</span>
|
|
126
|
+
<div className="flex flex-col gap-2 border-t border-gray-200 rounded-2xl p-1.5">
|
|
127
|
+
{showCreditsSkeleton && (
|
|
128
|
+
<div className="flex flex-col bg-gray-950 rounded-lg gap-4 w-full p-4">
|
|
129
|
+
<div className="flex items-center justify-between gap-2">
|
|
130
|
+
<span className="paragraph-xsmall-semibold text-white truncate">
|
|
131
|
+
Créditos usados
|
|
132
|
+
</span>
|
|
133
|
+
<div className="h-3.5 w-16 rounded bg-white/20 animate-pulse" />
|
|
134
|
+
</div>
|
|
135
|
+
<div className="h-1.5 w-full rounded-full bg-white/20 animate-pulse" />
|
|
137
136
|
</div>
|
|
138
|
-
|
|
139
|
-
className="bg-white/10"
|
|
140
|
-
indicatorColor="bg-white"
|
|
141
|
-
value={creditsPercentage}
|
|
142
|
-
/>
|
|
143
|
-
</div>
|
|
144
|
-
)}
|
|
137
|
+
)}
|
|
145
138
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
139
|
+
{showCredits && (
|
|
140
|
+
<div
|
|
141
|
+
className={cn(
|
|
142
|
+
"flex flex-col bg-gray-950 rounded-lg gap-4 w-full p-4 transition-colors",
|
|
143
|
+
onCreditsClick && "cursor-pointer hover:bg-gray-900",
|
|
144
|
+
)}
|
|
145
|
+
onClick={onCreditsClick}
|
|
146
|
+
>
|
|
147
|
+
<div className="flex items-center justify-between gap-2">
|
|
148
|
+
<span className="paragraph-xsmall-semibold text-white truncate">
|
|
149
|
+
Créditos usados
|
|
150
|
+
</span>
|
|
151
|
+
<span className="paragraph-xsmall-medium text-white/50">
|
|
152
|
+
{summary.usedCredits.toLocaleString("pt-BR")}/
|
|
153
|
+
{summary.totalCredits.toLocaleString("pt-BR")}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
<Progress
|
|
157
|
+
className="bg-white/10"
|
|
158
|
+
indicatorColor="bg-white"
|
|
159
|
+
value={creditsPercentage}
|
|
160
|
+
/>
|
|
157
161
|
</div>
|
|
158
162
|
)}
|
|
159
|
-
|
|
160
|
-
<
|
|
163
|
+
|
|
164
|
+
<div className="flex flex-col gap-2">
|
|
165
|
+
{menuItems.length > 0 && (
|
|
166
|
+
<div className="flex flex-col">
|
|
167
|
+
{menuItems.map((item, index) => (
|
|
168
|
+
<NavBarItem
|
|
169
|
+
key={index}
|
|
170
|
+
icon={item.icon}
|
|
171
|
+
label={item.label}
|
|
172
|
+
onClick={item.onClick}
|
|
173
|
+
/>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
<Separator />
|
|
178
|
+
<NavBarItem label="Sair" onClick={handleLogout} />
|
|
179
|
+
</div>
|
|
161
180
|
</div>
|
|
162
|
-
</
|
|
163
|
-
</PopoverPrimitive.Content>
|
|
181
|
+
</PopoverPrimitive.Content>
|
|
164
182
|
</PopoverPrimitive.Portal>
|
|
165
183
|
</Popover>
|
|
166
184
|
</>
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,10 @@ export * from './providers/whitelabel.provider';
|
|
|
11
11
|
|
|
12
12
|
// Hooks
|
|
13
13
|
export { useUserQuery, useUserValidateSession, useInvalidateUser, useSetUserData, USER_QUERY_KEY } from './modules/auth/hooks/useUserQuery';
|
|
14
|
+
export { useActiveSubscription } from './modules/subscriptions/hooks/list-subscriptions.hook';
|
|
15
|
+
export { useIaCredits } from './modules/ia-credits/hooks/ia-credits.hook';
|
|
16
|
+
export type { IaCreditsSummary, IaCreditOperation } from './modules/ia-credits/types';
|
|
17
|
+
export type { Subscription, FindSubscriptionsParams } from './modules/subscriptions/types';
|
|
14
18
|
|
|
15
19
|
// Providers
|
|
16
20
|
export { QueryProvider } from './providers/query.provider';
|
package/src/infra/api/client.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { ApiError } from './types';
|
|
2
2
|
import { findWhitelabel } from '../../modules/whitelabel/actions/find-whitelabel.action';
|
|
3
3
|
|
|
4
|
-
const API_BASE_URL = process.env.R3_API_URL || 'https://r3-api.greatapps.dev.br';
|
|
5
|
-
const API_VERSION = 'v1';
|
|
6
|
-
const API_LOCALE = 'pt-br';
|
|
7
|
-
|
|
8
4
|
interface RequestConfig extends RequestInit {
|
|
9
5
|
timeout?: number;
|
|
10
6
|
}
|
|
11
7
|
|
|
12
8
|
class ApiClient {
|
|
9
|
+
private readonly baseUrl: string;
|
|
10
|
+
private readonly apiVersion = 'v1';
|
|
11
|
+
private readonly apiLocale = 'pt-br';
|
|
12
|
+
|
|
13
|
+
constructor(baseUrl: string) {
|
|
14
|
+
this.baseUrl = baseUrl;
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
private async buildUrl(endpoint: string, whiteLabelId: number): Promise<string> {
|
|
14
|
-
const url = `${
|
|
18
|
+
const url = `${this.baseUrl}/${this.apiVersion}/${this.apiLocale}/${whiteLabelId}${endpoint}`;
|
|
15
19
|
console.log('[ApiClient] buildUrl', {
|
|
16
|
-
baseURL:
|
|
20
|
+
baseURL: this.baseUrl,
|
|
17
21
|
whiteLabelId,
|
|
18
22
|
endpoint,
|
|
19
23
|
fullUrl: url,
|
|
@@ -135,5 +139,12 @@ class ApiClient {
|
|
|
135
139
|
}
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
const api = {
|
|
143
|
+
apps: new ApiClient(process.env.GAPPS_R3_API_URL || 'https://r3-api.greatapps.dev.br'),
|
|
144
|
+
pages: new ApiClient(process.env.GPAGES_R3_API_URL || 'https://r3-api.greatpages.dev.br'),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @deprecated use api.apps */
|
|
148
|
+
export const apiClient = api.apps;
|
|
149
|
+
|
|
150
|
+
export { api, ApiClient };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { cookies } from 'next/headers';
|
|
4
|
+
import { ApiError, JWTPayload } from '../../../infra/api/types';
|
|
5
|
+
|
|
6
|
+
const AUTH_COOKIE_NAME = 'greatapps';
|
|
7
|
+
|
|
8
|
+
export async function getUserContext(): Promise<{
|
|
9
|
+
id_account: number;
|
|
10
|
+
id_user: number;
|
|
11
|
+
}> {
|
|
12
|
+
const cookieStore = await cookies();
|
|
13
|
+
const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;
|
|
14
|
+
|
|
15
|
+
if (!token) {
|
|
16
|
+
throw new ApiError('Usuário não autenticado', 'NOT_AUTHENTICATED', 401);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const payload = JSON.parse(atob(token.split('.')[1])) as JWTPayload;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
id_account: payload.id_account,
|
|
24
|
+
id_user: payload.id_user,
|
|
25
|
+
};
|
|
26
|
+
} catch {
|
|
27
|
+
throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { PaginatedSuccessResult } from '../../../infra/api/types';
|
|
4
|
+
import type { IaCreditOperation } from '../types';
|
|
5
|
+
import { iaCreditsService } from '../services/ia-credits.service';
|
|
6
|
+
|
|
7
|
+
export async function listIaCreditsAction(
|
|
8
|
+
subscriptionId: number | string
|
|
9
|
+
): Promise<PaginatedSuccessResult<IaCreditOperation>> {
|
|
10
|
+
return iaCreditsService.listOperations(subscriptionId);
|
|
11
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useQuery } from '@tanstack/react-query';
|
|
5
|
+
import { useActiveSubscription } from '../../subscriptions/hooks/list-subscriptions.hook';
|
|
6
|
+
import { listIaCreditsAction } from '../actions/list-ia-credits.action';
|
|
7
|
+
import type { IaCreditOperation, IaCreditsSummary } from '../types';
|
|
8
|
+
|
|
9
|
+
function calculateSummary(operations: IaCreditOperation[]): IaCreditsSummary {
|
|
10
|
+
const today = new Date();
|
|
11
|
+
const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
12
|
+
|
|
13
|
+
const adds = operations.filter((op) => op.operation === 'add');
|
|
14
|
+
const subtracts = operations.filter((op) => op.operation === 'subtract');
|
|
15
|
+
|
|
16
|
+
let totalCredits = 0;
|
|
17
|
+
let usedCredits = 0;
|
|
18
|
+
let nextExpirationInDays: number | null = null;
|
|
19
|
+
let nextExpiringCredits: number | null = null;
|
|
20
|
+
|
|
21
|
+
for (const add of adds) {
|
|
22
|
+
const expirationRaw = add.expiration_time;
|
|
23
|
+
if (!expirationRaw) continue;
|
|
24
|
+
|
|
25
|
+
const expiration = new Date(expirationRaw);
|
|
26
|
+
const expirationDate = new Date(
|
|
27
|
+
expiration.getFullYear(),
|
|
28
|
+
expiration.getMonth(),
|
|
29
|
+
expiration.getDate()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const expiredByDate = expirationDate.getTime() < todayDate.getTime();
|
|
33
|
+
const hasExpired = add.has_expired || expiredByDate;
|
|
34
|
+
|
|
35
|
+
if (hasExpired) continue;
|
|
36
|
+
|
|
37
|
+
const addedCredits = add.operation_value;
|
|
38
|
+
|
|
39
|
+
const consumedForAdd = subtracts
|
|
40
|
+
.filter((sub) => String(sub.consumed_from_add_id) === String(add.id))
|
|
41
|
+
.reduce((sum, sub) => sum + sub.operation_value, 0);
|
|
42
|
+
|
|
43
|
+
const remainingForAdd = Math.max(0, addedCredits - consumedForAdd);
|
|
44
|
+
|
|
45
|
+
totalCredits += addedCredits;
|
|
46
|
+
usedCredits += consumedForAdd;
|
|
47
|
+
|
|
48
|
+
const diffMs = expirationDate.getTime() - todayDate.getTime();
|
|
49
|
+
const days = Math.max(0, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
|
|
50
|
+
|
|
51
|
+
if (remainingForAdd > 0) {
|
|
52
|
+
if (nextExpirationInDays === null || days < nextExpirationInDays) {
|
|
53
|
+
nextExpirationInDays = days;
|
|
54
|
+
nextExpiringCredits = remainingForAdd;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const availableCredits = Math.max(0, totalCredits - usedCredits);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
totalCredits,
|
|
63
|
+
usedCredits,
|
|
64
|
+
availableCredits,
|
|
65
|
+
nextExpiringCredits,
|
|
66
|
+
nextExpirationInDays,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function useIaCredits() {
|
|
71
|
+
const { data: subscriptionData, isPending: isSubscriptionLoading } = useActiveSubscription();
|
|
72
|
+
const subscription = subscriptionData?.data?.[0] ?? null;
|
|
73
|
+
const subscriptionId = subscription?.id;
|
|
74
|
+
|
|
75
|
+
const query = useQuery({
|
|
76
|
+
queryKey: ['ia-credits', subscriptionId],
|
|
77
|
+
queryFn: () => listIaCreditsAction(subscriptionId!),
|
|
78
|
+
enabled: subscriptionId != null,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const operations = query.data?.data ?? [];
|
|
82
|
+
|
|
83
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
84
|
+
const summary = useMemo(() => calculateSummary(operations), [operations]);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
subscription,
|
|
88
|
+
operations,
|
|
89
|
+
summary,
|
|
90
|
+
isLoading: isSubscriptionLoading || query.isPending,
|
|
91
|
+
isSubscriptionLoading,
|
|
92
|
+
isCreditsLoading: query.isPending,
|
|
93
|
+
isError: query.isError,
|
|
94
|
+
error: query.error,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { api } from '../../../infra/api/client';
|
|
4
|
+
import { ApiError, ApiPaginatedActionResult, PaginatedSuccessResult } from '../../../infra/api/types';
|
|
5
|
+
import { getUserContext } from '../../auth/utils/get-user-context';
|
|
6
|
+
import { IaCreditOperation, IaCreditOperationSchema } from '../types';
|
|
7
|
+
|
|
8
|
+
class IaCreditsService {
|
|
9
|
+
async listOperations(
|
|
10
|
+
subscriptionId: number | string
|
|
11
|
+
): Promise<PaginatedSuccessResult<IaCreditOperation>> {
|
|
12
|
+
const { id_account } = await getUserContext();
|
|
13
|
+
|
|
14
|
+
const endpoint = `/accounts/${id_account}/subscriptions/${subscriptionId}/items/balance/list?page=1&limit=20&sort=id:DESC`;
|
|
15
|
+
|
|
16
|
+
const response = await api.apps.get<ApiPaginatedActionResult<IaCreditOperation>>(endpoint);
|
|
17
|
+
|
|
18
|
+
if (response.status === 0) {
|
|
19
|
+
throw new ApiError(
|
|
20
|
+
response.message || 'Erro ao listar créditos de IA',
|
|
21
|
+
'LIST_IA_CREDITS_FAILED',
|
|
22
|
+
400
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const raw = response.data ?? [];
|
|
27
|
+
const data = raw.map((item) => IaCreditOperationSchema.parse(item));
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
data,
|
|
31
|
+
total: response.total ?? data.length,
|
|
32
|
+
success: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const iaCreditsService = new IaCreditsService();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
|
|
3
|
+
export const IaCreditOperationSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
id: z.union([z.number(), z.string()]),
|
|
6
|
+
deleted: z.number(),
|
|
7
|
+
datetime_add: z.string(),
|
|
8
|
+
id_wl: z.number(),
|
|
9
|
+
id_subscription: z.union([z.number(), z.string()]),
|
|
10
|
+
id_addon: z.number(),
|
|
11
|
+
operation: z.string(), // 'add' | 'subtract'
|
|
12
|
+
operation_value: z.union([z.number(), z.string()]).transform((v) => Number(v)),
|
|
13
|
+
balance_after_operation: z.union([z.number(), z.string()]).optional(),
|
|
14
|
+
balance_before_operation: z.union([z.number(), z.string()]).optional(),
|
|
15
|
+
operation_ref: z.string().nullable().optional(),
|
|
16
|
+
operation_origin: z.string().nullable().optional(),
|
|
17
|
+
expiration_time: z.string().nullable(),
|
|
18
|
+
has_expired: z.boolean(),
|
|
19
|
+
consumed_from_add_id: z.union([z.number(), z.string()]).nullable(),
|
|
20
|
+
id_user_add: z.union([z.number(), z.string()]).nullable(),
|
|
21
|
+
user_name: z.string().nullable().optional(),
|
|
22
|
+
})
|
|
23
|
+
.passthrough();
|
|
24
|
+
|
|
25
|
+
export type IaCreditOperation = z.infer<typeof IaCreditOperationSchema>;
|
|
26
|
+
|
|
27
|
+
export type IaCreditsSummary = {
|
|
28
|
+
totalCredits: number;
|
|
29
|
+
usedCredits: number;
|
|
30
|
+
availableCredits: number;
|
|
31
|
+
nextExpiringCredits: number | null;
|
|
32
|
+
nextExpirationInDays: number | null;
|
|
33
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { PaginatedSuccessResult } from '../../../infra/api/types';
|
|
4
|
+
import type { Subscription, FindSubscriptionsParams } from '../types';
|
|
5
|
+
import { subscriptionsService } from '../services/subscriptions.service';
|
|
6
|
+
|
|
7
|
+
export async function listSubscriptionsAction(
|
|
8
|
+
params?: Partial<FindSubscriptionsParams>
|
|
9
|
+
): Promise<PaginatedSuccessResult<Subscription>> {
|
|
10
|
+
return subscriptionsService.listSubscriptions(params);
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
4
|
+
import { listSubscriptionsAction } from '../actions/list-subscriptions.action';
|
|
5
|
+
import type { FindSubscriptionsParams } from '../types';
|
|
6
|
+
|
|
7
|
+
const SUBSCRIPTIONS_QUERY_KEY = ['subscriptions'];
|
|
8
|
+
|
|
9
|
+
export function useActiveSubscription() {
|
|
10
|
+
return useQuery({
|
|
11
|
+
queryKey: [...SUBSCRIPTIONS_QUERY_KEY, 'active'],
|
|
12
|
+
queryFn: () => listSubscriptionsAction({ active: true } as Partial<FindSubscriptionsParams>),
|
|
13
|
+
});
|
|
14
|
+
}
|