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} + + + + + + + + + + {#each transactions as transaction} + + + + + + {/each} + +
DateDescriptionChange
{new Date(transaction.created_at).toLocaleDateString()}{transaction.description} 0} + class:text-red-600={transaction.credits_change < 0} + > + {transaction.credits_change > 0 ? '+' : ''}{transaction.credits_change} +
+ {/if} +
+
+ +