diff --git a/package.json b/package.json
index f151ba8..0a83afe 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,8 @@
"@getbrevo/brevo": "^2.2.0",
"@supabase/ssr": "^0.4.0",
"@supabase/supabase-js": "^2.44.2",
+ "chart.js": "^4.4.6",
+ "simple-datatables": "^9.2.1",
"stripe": "^15.4.0",
"ts-debounce": "^4.0.0",
"winston": "^3.15.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fa1b771..e1e057c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,12 @@ importers:
'@supabase/supabase-js':
specifier: ^2.44.2
version: 2.45.4
+ chart.js:
+ specifier: ^4.4.6
+ version: 4.4.6
+ simple-datatables:
+ specifier: ^9.2.1
+ version: 9.2.1
stripe:
specifier: ^15.4.0
version: 15.12.0
@@ -937,6 +943,9 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+ '@kurkle/color@0.3.2':
+ resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1533,6 +1542,10 @@ packages:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ chart.js@4.4.6:
+ resolution: {integrity: sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==}
+ engines: {pnpm: '>=8'}
+
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -1670,6 +1683,9 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'}
+ dayjs@1.11.13:
+ resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
+
debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
@@ -1731,6 +1747,9 @@ packages:
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+ diff-dom@5.1.4:
+ resolution: {integrity: sha512-TSEaVdVGictY1KHg7VpVw2nuM02YKC9C8/qBkGiCnkiAybVbu1zQTMj2/dnVLRO7Z62UsqzHGpXweiOj5/jaZg==}
+
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -2993,6 +3012,9 @@ packages:
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
+ simple-datatables@9.2.1:
+ resolution: {integrity: sha512-R+KRyZ4GNQdqj5IoW08OYi9FHqCbndbw+MfdOJDnwOGs6me9+BhbNuj2QHZR/GXQ04TLra43Uj2/Lk1KcLiJRw==}
+
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
@@ -4530,6 +4552,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
+ '@kurkle/color@0.3.2': {}
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -5194,6 +5218,10 @@ snapshots:
chalk@5.3.0: {}
+ chart.js@4.4.6:
+ dependencies:
+ '@kurkle/color': 0.3.2
+
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -5337,6 +5365,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.1
+ dayjs@1.11.13: {}
+
debug@4.3.7:
dependencies:
ms: 2.1.3
@@ -5386,6 +5416,8 @@ snapshots:
didyoumean@1.2.2: {}
+ diff-dom@5.1.4: {}
+
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
@@ -6699,6 +6731,11 @@ snapshots:
simple-concat@1.0.1: {}
+ simple-datatables@9.2.1:
+ dependencies:
+ dayjs: 1.11.13
+ diff-dom: 5.1.4
+
simple-get@4.0.1:
dependencies:
decompress-response: 6.0.0
diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts
index 7252c76..e7e7dce 100644
--- a/src/lib/utils/supabase/admin.ts
+++ b/src/lib/utils/supabase/admin.ts
@@ -451,6 +451,28 @@ const getUserCredits = async (userId: string) => {
return creditData.credits_remaining;
};
+/**
+ * Fetches the credit transaction history for a specific user.
+ *
+ * @param userId - The ID of the user whose transactions are to be fetched.
+ * @returns Array of transaction records containing credits_change, description, and created_at.
+ */
+export const getUserCreditTransactions = async (userId: string) => {
+ if (!userId) throw new Error('User ID is required');
+
+ const { data, error } = await supabaseAdmin
+ .from('credit_transactions')
+ .select('credits_change, description, created_at')
+ .eq('user_id', userId)
+ .order('created_at', { ascending: false }); // Orders transactions with the latest ones first
+
+ if (error) {
+ throw new Error(`Error fetching transactions: ${error.message}`);
+ }
+
+ return data || [];
+};
+
type ProductWithPrices = Product & {
prices: Price[];
actualPrice?: number;
diff --git a/src/routes/account/+page.server.ts b/src/routes/(user)/account/+page.server.ts
similarity index 100%
rename from src/routes/account/+page.server.ts
rename to src/routes/(user)/account/+page.server.ts
diff --git a/src/routes/account/+page.svelte b/src/routes/(user)/account/+page.svelte
similarity index 94%
rename from src/routes/account/+page.svelte
rename to src/routes/(user)/account/+page.svelte
index 21cefd9..ede00e5 100644
--- a/src/routes/account/+page.svelte
+++ b/src/routes/(user)/account/+page.svelte
@@ -139,6 +139,12 @@
{/if}
+
+ Credit Usage
+ Monitor your credit balance and view transaction history.
+ View details
+
+
Billing History
@@ -250,12 +256,12 @@
Provide Feedback
-
-
+
+
diff --git a/src/routes/(user)/credits/+page.server.ts b/src/routes/(user)/credits/+page.server.ts
new file mode 100644
index 0000000..6d25a78
--- /dev/null
+++ b/src/routes/(user)/credits/+page.server.ts
@@ -0,0 +1,53 @@
+import { redirect } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+import { getUserCredits, getUserCreditTransactions } from '$lib/utils/supabase/admin';
+import logger from '$lib/utils/logger/logger';
+
+export const load: PageServerLoad = async ({ locals: { safeGetSession } }) => {
+ const { session } = await safeGetSession();
+
+ if (!session) {
+ redirect(303, '/');
+ }
+
+ const userId = session.user?.id;
+ if (!userId) {
+ return { error: 'User not authenticated' };
+ }
+
+ try {
+ const creditsRemaining = await getUserCredits(userId).catch((error) => {
+ logger.error('Failed to fetch user credits:', error);
+ throw new Error('Unable to retrieve credit balance');
+ });
+ const transactions = await getUserCreditTransactions(userId).catch((error) => {
+ logger.error('Failed to fetch credit transactions:', error);
+ throw new Error('Unable to retrieve transaction history');
+ });
+
+ const monthlyAggregates = transactions.reduce((acc, transaction) => {
+ const date = new Date(transaction.created_at);
+ const month = date.toISOString().substring(0, 7); // Returns YYYY-MM format
+
+ if (!acc[month]) acc[month] = { credits_added: 0, credits_deducted: 0 };
+
+ if (transaction.credits_change > 0) {
+ acc[month].credits_added += transaction.credits_change;
+ } else {
+ acc[month].credits_deducted += transaction.credits_change;
+ }
+
+ return acc;
+ }, {});
+
+ return {
+ creditsRemaining,
+ transactions,
+ monthlyAggregates
+ };
+ } catch (error) {
+ return {
+ error: error.message
+ };
+ }
+};
diff --git a/src/routes/(user)/credits/+page.svelte b/src/routes/(user)/credits/+page.svelte
new file mode 100644
index 0000000..ffafa94
--- /dev/null
+++ b/src/routes/(user)/credits/+page.svelte
@@ -0,0 +1,163 @@
+
+
+
+ Credit Dashboard
+ Credits Remaining: {creditsRemaining}
+
+
+
+
+
+
+
+
+
+
+ Transaction History
+
+ {#if transactions.length === 0}
+
No transactions found.
+ {:else}
+
+
+
+ | Date |
+ Description |
+ Change |
+
+
+
+ {#each transactions as transaction}
+
+ | {new Date(transaction.created_at).toLocaleDateString()} |
+ {transaction.description} |
+ 0}
+ class:text-red-600={transaction.credits_change < 0}
+ >
+ {transaction.credits_change > 0 ? '+' : ''}{transaction.credits_change}
+ |
+
+ {/each}
+
+
+ {/if}
+
+
+
+