From b2d25bb0a6d174fe4abdd7148ce49864729cb124 Mon Sep 17 00:00:00 2001 From: mohamed mahmoud Date: Fri, 1 May 2026 12:06:20 +0300 Subject: [PATCH 1/3] the entire WS ui representation ,sorry for less_commits in WS feature phase but Inshaa Allah i will documenting that in near futrue --- .env | 5 +- src/domain/useCase/useAdminWallet.js | 75 ++ src/domain/useCase/useInitiatePayment.js | 25 + src/domain/useCase/useWallet.js | 50 + src/infrastructure/DTO/WalletDTO.js | 57 ++ .../repository/GlobalTagRepository.js | 9 + .../repository/PaymentRepository.js | 28 + .../repository/WalletRepository.js | 98 ++ .../CMSCreateCategorizationModal.jsx | 6 +- .../cms/components/CMSResourceTable.jsx | 261 +++-- .../course/CourseSubscriptionAnalysis.jsx | 141 +-- .../components/course/CourseWeeksEditor.jsx | 4 +- .../cms/components/course/WeekList.jsx | 46 +- .../viewers/CategoryDistributionViewer.jsx | 58 ++ .../viewers/DefaultDashboardViewer.jsx | 81 +- .../viewers/GrowthTimelineViewer.jsx | 1 - .../dashboard/widgets/TopContentWidget.jsx | 79 ++ .../dashboard/widgets/TrendTagsAnalyzer.jsx | 259 +++++ .../event/EventSubscriptionAnalysis.jsx | 116 ++- .../feature/cms/layout/CMSLayout.jsx | 9 +- .../feature/cms/routes/AddLessonPage.jsx | 14 +- .../cms/routes/CMSAdminNotificationsPage.jsx | 49 +- .../feature/cms/routes/CMSCoursesPage.jsx | 6 +- .../feature/cms/routes/CMSDashboardPage.jsx | 47 +- .../feature/cms/routes/CMSProblemsPage.jsx | 6 +- .../feature/cms/routes/CMSReportsPage.jsx | 44 +- .../cms/routes/CourseManagementPage.jsx | 45 +- .../feature/cms/routes/EditLessonPage.jsx | 14 +- .../cms/routes/EventManagementPage.jsx | 45 +- .../cms/routes/ProblemManagementPage.jsx | 45 +- .../course/components/CourseActionSidebar.jsx | 38 +- .../feature/course/components/CourseCard.jsx | 10 +- .../components/EventRecommendedCard.jsx | 5 +- .../event/components/EventInfoSidebar.jsx | 187 ++-- .../feature/payment/routes/PaymentResult.jsx | 94 ++ .../feature/wallet/routes/AllWalletsPage.jsx | 140 +++ .../feature/wallet/routes/MyWalletPage.jsx | 336 ++++++ .../wallet/routes/PlatformRevenuePage.jsx | 50 + .../feature/wallet/routes/WalletLayout.jsx | 61 ++ src/presentation/routes/AppRoutes.jsx | 32 + src/presentation/routes/paths.js | 7 + .../layout/header-parts/UserMenu.jsx | 13 +- .../shared/components/modals/PaymobModal.jsx | 56 + src/presentation/shared/layout/MainLayout.jsx | 7 +- src/wallet_payment_system_design.md | 969 ++++++++++++++++++ src/wallet_system_design.md | 802 +++++++++++++++ vite.config.js | 24 +- 47 files changed, 4083 insertions(+), 471 deletions(-) create mode 100644 src/domain/useCase/useAdminWallet.js create mode 100644 src/domain/useCase/useInitiatePayment.js create mode 100644 src/domain/useCase/useWallet.js create mode 100644 src/infrastructure/DTO/WalletDTO.js create mode 100644 src/infrastructure/repository/PaymentRepository.js create mode 100644 src/infrastructure/repository/WalletRepository.js create mode 100644 src/presentation/feature/cms/components/dashboard/viewers/CategoryDistributionViewer.jsx create mode 100644 src/presentation/feature/cms/components/dashboard/widgets/TopContentWidget.jsx create mode 100644 src/presentation/feature/cms/components/dashboard/widgets/TrendTagsAnalyzer.jsx create mode 100644 src/presentation/feature/payment/routes/PaymentResult.jsx create mode 100644 src/presentation/feature/wallet/routes/AllWalletsPage.jsx create mode 100644 src/presentation/feature/wallet/routes/MyWalletPage.jsx create mode 100644 src/presentation/feature/wallet/routes/PlatformRevenuePage.jsx create mode 100644 src/presentation/feature/wallet/routes/WalletLayout.jsx create mode 100644 src/presentation/shared/components/modals/PaymobModal.jsx create mode 100644 src/wallet_payment_system_design.md create mode 100644 src/wallet_system_design.md diff --git a/.env b/.env index 8e6f5f8..4ca20c7 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -VITE_API_BASE_URL=http://localhost:1338 +VITE_API_BASE_URL=http://127.0.0.1:1338 # VITE_API_BASE_URL=https://giving-cabbage-d8e17e469b.strapiapp.com VITE_API_REGISTER=/api/auth/local/register @@ -47,4 +47,5 @@ VITE_RECAPATCH=6LfLqG0sAAAAAIVBgjd_WSzhhcbIA8Z_boHtbtUv VITE_VAPID_PUBLIC_KEY=BJmu1K4k02ru__di6cZa40gJD1b8wRy_K2oivy6LIzAf-EMBYl3u2fjTH1yUKp2bp2YZIPqhE0A0GsaczxqAJ9A -VITE_CMS_ANALYTICS_ENDPOINT=/api/cms-analytics \ No newline at end of file +VITE_CMS_ANALYTICS_ENDPOINT=/api/cms-analytics +VITE_LISTEN_ALL=true \ No newline at end of file diff --git a/src/domain/useCase/useAdminWallet.js b/src/domain/useCase/useAdminWallet.js new file mode 100644 index 0000000..dfc84b3 --- /dev/null +++ b/src/domain/useCase/useAdminWallet.js @@ -0,0 +1,75 @@ +import { useState, useCallback, useMemo } from 'react'; +import { WalletRepository } from '@infrastructure/repository/WalletRepository'; + +export const useAdminWallet = () => { + const [platformWallet, setPlatformWallet] = useState(null); + const [allWallets, setAllWallets] = useState([]); + const [walletsMeta, setWalletsMeta] = useState(null); + + const [loadingPlatform, setLoadingPlatform] = useState(false); + const [loadingAll, setLoadingAll] = useState(false); + const [updatingCommission, setUpdatingCommission] = useState(false); + + const [error, setError] = useState(null); + + const repository = useMemo(() => new WalletRepository(), []); + + const fetchPlatformWallet = useCallback(async () => { + setLoadingPlatform(true); + setError(null); + try { + const data = await repository.getPlatformWallet(); + setPlatformWallet(data); + } catch (err) { + setError(err.response?.data?.error?.message || 'Failed to fetch platform wallet'); + } finally { + setLoadingPlatform(false); + } + }, [repository]); + + const fetchAllWallets = useCallback(async (params = { page: 1, pageSize: 25 }) => { + setLoadingAll(true); + setError(null); + try { + const result = await repository.getAllWallets(params); + setAllWallets(result.data); + setWalletsMeta(result.meta); + } catch (err) { + setError(err.response?.data?.error?.message || 'Failed to fetch all wallets'); + } finally { + setLoadingAll(false); + } + }, [repository]); + + const updateCommission = useCallback(async (walletId, newRate) => { + setUpdatingCommission(true); + setError(null); + try { + await repository.updateCommissionRate(walletId, newRate); + // After successful update, re-fetch the list to show updated values + await fetchAllWallets(); + return true; + } catch (err) { + const msg = err.response?.data?.error?.message || 'Failed to update commission rate'; + setError(msg); + throw new Error(msg); + } finally { + setUpdatingCommission(false); + } + }, [repository, fetchAllWallets]); + + return { + platformWallet, + allWallets, + walletsMeta, + + loadingPlatform, + loadingAll, + updatingCommission, + error, + + fetchPlatformWallet, + fetchAllWallets, + updateCommission + }; +}; diff --git a/src/domain/useCase/useInitiatePayment.js b/src/domain/useCase/useInitiatePayment.js new file mode 100644 index 0000000..0a58f14 --- /dev/null +++ b/src/domain/useCase/useInitiatePayment.js @@ -0,0 +1,25 @@ +import { useState, useCallback } from 'react'; +import { PaymentRepository } from '@infrastructure/repository/PaymentRepository'; + +export const useInitiatePayment = () => { + const [inProgress, setInProgress] = useState(false); + const [error, setError] = useState(null); + const paymentRepo = new PaymentRepository(); + + const initiatePayment = useCallback(async (itemId, contentType) => { + setInProgress(true); + setError(null); + try { + const data = await paymentRepo.initiateCheckout(itemId, contentType); + return data; + } catch (err) { + const message = err.response?.data?.error?.message || 'Failed to initiate payment. Please try again.'; + setError(message); + throw err; + } finally { + setInProgress(false); + } + }, [paymentRepo]); + + return { initiatePayment, inProgress, error }; +}; diff --git a/src/domain/useCase/useWallet.js b/src/domain/useCase/useWallet.js new file mode 100644 index 0000000..6e4138f --- /dev/null +++ b/src/domain/useCase/useWallet.js @@ -0,0 +1,50 @@ +import { useState, useCallback, useMemo } from 'react'; +import { WalletRepository } from '@infrastructure/repository/WalletRepository'; + +export const useWallet = () => { + const [wallet, setWallet] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [requestingPayout, setRequestingPayout] = useState(false); + + const repository = useMemo(() => new WalletRepository(), []); + + const fetchWallet = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await repository.getMyWallet(); + setWallet(data); + } catch (err) { + setError(err.response?.data?.error?.message || 'Failed to fetch wallet data'); + } finally { + setLoading(false); + } + }, [repository]); + + const requestPayout = useCallback(async (amount, method, details) => { + setRequestingPayout(true); + setError(null); + try { + const result = await repository.requestPayout({ amount, method, details }); + // Refresh wallet to show updated balance + await fetchWallet(); + return result; + } catch (err) { + const msg = err.response?.data?.error?.message || 'Payout request failed'; + setError(msg); + throw new Error(msg); + } finally { + setRequestingPayout(false); + } + }, [repository, fetchWallet]); + + return { + wallet, + loading, + error, + requestingPayout, + fetchWallet, + requestPayout + }; +}; diff --git a/src/infrastructure/DTO/WalletDTO.js b/src/infrastructure/DTO/WalletDTO.js new file mode 100644 index 0000000..995aace --- /dev/null +++ b/src/infrastructure/DTO/WalletDTO.js @@ -0,0 +1,57 @@ +/** + * Data Transfer Object for Wallet responses + */ +export class WalletDTO { + constructor(data) { + if (!data) return; + + this.id = data.id; + this.documentId = data.documentId; + this.ownerType = data.owner_type; + this.balance = Number(data.balance || 0); + this.pendingBalance = Number(data.pending_balance || 0); + this.currency = data.currency || 'EGP'; + this.commissionRate = data.commission_rate ? Number(data.commission_rate) : 0; + this.isActive = Boolean(data.is_active); + + // Owner info (can be populated) + this.owner = data.owner ? { + id: data.owner.id, + username: data.owner.username, + email: data.owner.email, + } : null; + + this.transactions = Array.isArray(data.transactions) ? data.transactions.map(t => new TransactionDTO(t)) : []; + this.payouts = Array.isArray(data.payouts) ? data.payouts.map(p => new PayoutDTO(p)) : []; + + this.createdAt = data.createdAt ? new Date(data.createdAt) : null; + } +} + +export class TransactionDTO { + constructor(data) { + if (!data) return; + + this.id = data.id; + this.type = data.type; // 'credit' | 'debit' | 'commission' | 'withdrawal' | 'refund' + this.amount = Number(data.amount || 0); + this.description = data.description || ''; + this.referenceType = data.reference_type; + this.referenceId = data.reference_id; + this.metadata = data.metadata || {}; + this.createdAt = data.createdAt ? new Date(data.createdAt) : null; + } +} + +export class PayoutDTO { + constructor(data) { + if (!data) return; + + this.id = data.id; + this.amount = Number(data.amount || 0); + this.status = data.status; // 'pending' | 'processing' | 'completed' | 'failed' | 'rejected' + this.method = data.method; + this.details = data.details || {}; + this.createdAt = data.createdAt ? new Date(data.createdAt) : null; + } +} diff --git a/src/infrastructure/repository/GlobalTagRepository.js b/src/infrastructure/repository/GlobalTagRepository.js index 0c9dcb4..14167be 100644 --- a/src/infrastructure/repository/GlobalTagRepository.js +++ b/src/infrastructure/repository/GlobalTagRepository.js @@ -30,4 +30,13 @@ export class GlobalTagRepository extends BaseRepository { async deleteTag(id) { return await this.delete(`${this.endpoint}/${id}`); } + + /** + * Fetches tag audience map: real user interest counts per tag. + * @returns {Promise<{tags: Array, totalUsers: number}>} + */ + async getTagAudience() { + const response = await this.get('/api/recommendations/tag-audience'); + return response?.data || { tags: [], totalUsers: 0 }; + } } diff --git a/src/infrastructure/repository/PaymentRepository.js b/src/infrastructure/repository/PaymentRepository.js new file mode 100644 index 0000000..3216747 --- /dev/null +++ b/src/infrastructure/repository/PaymentRepository.js @@ -0,0 +1,28 @@ +import { repositoryRegistry } from './RepositoryRegistry'; + +export class PaymentRepository { + constructor(apiClient = repositoryRegistry.apiClient) { + this.apiClient = apiClient; + } + + /** + * Initiates a checkout process for a specific item (Event or Course) + * @param {string} itemId - The documentId or id of the item + * @param {string} contentType - 'event' or 'course' + * @returns {Promise<{payment_key: string, order_id: string, iframe_url: string}>} + */ + async initiateCheckout(itemId, contentType = 'event') { + try { + // Using BaseRepository.post which handles path prefixing and wrapping + const response = await this.apiClient.post('/api/payments/initiate', { + itemId, + contentType + }, true); + + return response.data; + } catch (error) { + console.error('PaymentRepository initiateCheckout failed', error); + throw error; + } + } +} diff --git a/src/infrastructure/repository/WalletRepository.js b/src/infrastructure/repository/WalletRepository.js new file mode 100644 index 0000000..6b6ccf1 --- /dev/null +++ b/src/infrastructure/repository/WalletRepository.js @@ -0,0 +1,98 @@ +import { repositoryRegistry } from './RepositoryRegistry'; +import { WalletDTO } from '../DTO/WalletDTO'; + +export class WalletRepository { + constructor(apiClient = repositoryRegistry.apiClient) { + this.apiClient = apiClient; + } + + /** + * Gets the current user's wallet + * @returns {Promise} + */ + async getMyWallet() { + try { + const response = await this.apiClient.get('/api/wallet/me'); + const data = response?.data || response; + return new WalletDTO(data); + } catch (error) { + console.error('[WalletRepository] getMyWallet failed:', error); + throw error; + } + } + + /** + * Gets the platform's commission wallet (Admin only) + * @returns {Promise} + */ + async getPlatformWallet() { + try { + const response = await this.apiClient.get('/api/wallet/platform'); + const data = response?.data || response; + return new WalletDTO(data); + } catch (error) { + console.error('[WalletRepository] getPlatformWallet failed:', error); + throw error; + } + } + + /** + * Gets all wallets with pagination (Admin only) + * @param {Object} params { page, pageSize, owner_type, is_active } + * @returns {Promise<{data: WalletDTO[], meta: Object}>} + */ + async getAllWallets(params = {}) { + try { + const queryParams = new URLSearchParams(); + if (params.page) queryParams.append('page', params.page); + if (params.pageSize) queryParams.append('pageSize', params.pageSize); + if (params.owner_type) queryParams.append('owner_type', params.owner_type); + if (params.is_active !== undefined) queryParams.append('is_active', params.is_active); + + const queryString = queryParams.toString() ? `?${queryParams.toString()}` : ''; + const response = await this.apiClient.get(`/api/wallets${queryString}`); + + const rawData = response?.data || []; + const meta = response?.meta || {}; + + return { + data: Array.isArray(rawData) ? rawData.map(w => new WalletDTO(w)) : [], + meta + }; + } catch (error) { + console.error('[WalletRepository] getAllWallets failed:', error); + throw error; + } + } + + /** + * Updates a publisher's commission rate (Admin only) + * @param {number|string} id Wallet ID + * @param {number} commissionRate Number between 0 and 1 + * @returns {Promise} + */ + async updateCommissionRate(id, commissionRate) { + try { + const response = await this.apiClient.put(`/api/wallets/${id}/commission`, { commission_rate: commissionRate }, true); + return response?.data || response; + } catch (error) { + console.error('[WalletRepository] updateCommissionRate failed:', error); + throw error; + } + } + + /** + * Requests a payout from the publisher's wallet + * @param {Object} payload { amount, method, details } + * @returns {Promise} + */ + async requestPayout({ amount, method, details }) { + try { + const response = await this.apiClient.post('/api/payouts/request', { amount, method, details }, true); + return response?.data || response; + } catch (error) { + console.error('[WalletRepository] requestPayout failed:', error); + throw error; + } + } +} diff --git a/src/presentation/feature/cms/components/CMSCreateCategorizationModal.jsx b/src/presentation/feature/cms/components/CMSCreateCategorizationModal.jsx index 60c822e..bfbe59c 100644 --- a/src/presentation/feature/cms/components/CMSCreateCategorizationModal.jsx +++ b/src/presentation/feature/cms/components/CMSCreateCategorizationModal.jsx @@ -30,8 +30,8 @@ export const CMSCreateCategorizationModal = ({ /> {/* Modal Content */} -
-
+
+
@@ -49,7 +49,7 @@ export const CMSCreateCategorizationModal = ({
-
+