From 1b03558f935ae0b592fff1fa0d8885acb96c2803 Mon Sep 17 00:00:00 2001 From: Edd Date: Wed, 31 Dec 2025 00:16:14 +0000 Subject: [PATCH] matches and transactions history fully operational --- api/app/Http/Controllers/UserController.php | 71 ++++- api/sample-requests.http | 6 + frontend/src/pages/user/UserPage.vue | 312 ++++++++++++-------- frontend/src/stores/api.js | 21 +- 4 files changed, 264 insertions(+), 146 deletions(-) diff --git a/api/app/Http/Controllers/UserController.php b/api/app/Http/Controllers/UserController.php index 675d0d3..2fa423c 100644 --- a/api/app/Http/Controllers/UserController.php +++ b/api/app/Http/Controllers/UserController.php @@ -183,16 +183,65 @@ class UserController extends Controller { $this->authorize('view', $user); - $matches = Game::query() - ->where(function ($q) use ($user) { - $q->where('player1_user_id', $user->id)->orWhere( - 'player2_user_id', - $user->id, - ); - }) - ->with(['winner', 'player1', 'player2']) - ->orderBy('began_at', 'desc') - ->paginate(10); + $outcomeSql = "CASE + WHEN is_draw = 1 THEN 'DRAW' + WHEN winner_user_id = {$user->id} THEN 'WIN' + ELSE 'LOSS' + END"; + + $query = Game::query() + ->select('*') + ->selectRaw("($outcomeSql) as outcome_text") + ->where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + }) + ->with(['winner', 'player1', 'player2']); + + if ($request->has('type')) { + $query->where('type', $request->type); + } + + if ($request->has('status')) { + $query->where('status', $request->status); + } + + if ($request->filled('outcome')) { + switch ($request->outcome) { + case 'win': + $query->where('winner_user_id', $user->id) + ->where('is_draw', 0); + break; + case 'loss': + case 'forfeit': // Forfeit treated as loss for now + $query->where('loser_user_id', $user->id) + ->where('is_draw', 0); + break; + case 'draw': + $query->where('is_draw', 1); + break; + } + } + + if ($request->has('date_from')) { + $query->whereDate('began_at', '>=', $request->date_from); + } + + if ($request->has('date_to')) { + $query->whereDate('ended_at', '<=', $request->date_to); + } + + $order = $request->get('sort_by', 'began_at'); + $direction = $request->get('sort_direction', 'desc'); + + if ($order === 'outcome') { + $query->orderBy('outcome_text', $direction); + } else { + $query->orderBy($order, $direction); + } + + $matches = $query->orderBy($order, $direction) + ->paginate(10); return response()->json($matches); } @@ -230,7 +279,7 @@ class UserController extends Controller if ($request->has('type')) { $query->whereHas('type', function ($q) use ($request) { - $q->where('name', $request->type); + $q->where('type', $request->type); }); } diff --git a/api/sample-requests.http b/api/sample-requests.http index 930be4c..b8f4745 100644 --- a/api/sample-requests.http +++ b/api/sample-requests.http @@ -29,4 +29,10 @@ Authorization: Bearer {{token}} GET http://localhost:8000/api/v1/users/{{id}}/matches Content-Type: application/json Accept: application/json +Authorization: Bearer {{token}} + +### Get me transactions +GET http://localhost:8000/api/v1/users/{{id}}/transactions?type=C +Content-Type: application/json +Accept: application/json Authorization: Bearer {{token}} \ No newline at end of file diff --git a/frontend/src/pages/user/UserPage.vue b/frontend/src/pages/user/UserPage.vue index 88ce40e..7eb603b 100644 --- a/frontend/src/pages/user/UserPage.vue +++ b/frontend/src/pages/user/UserPage.vue @@ -229,45 +229,44 @@
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
No transactions yet.
-
-
- - -
- -
- - -
- -
- - -
- - -
-
+ :class="[transaction.type === 'credit' ? 'border-green-500/30' : 'border-red-500/30']">
-
- {{ transaction.type }} Payment -
+
+ {{ transaction.category }}
- {{ new Date(transaction.created_at).toLocaleDateString() }} • {{ new - Date(transaction.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }} + {{ new Date(transaction.began_at).toLocaleDateString() }} • + {{ new Date(transaction.began_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + }}
@@ -290,11 +289,10 @@
- {{ transaction.type === 'credit' ? '+' : '-' }}${{ transaction.amount.toLocaleString() }} + {{ transaction.type === 'credit' ? '+' : '-' }}{{ transaction.amount }} coins
- {{ transaction.id.split('_')[0] }} -
+ {{ transaction.reference }}
@@ -308,62 +306,72 @@
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
+ +
+ + + +
+
+
+ +
No games played yet.
- -
-
- - -
- -
- - -
- - - -
-
- -
- - - -
-
-
- -
-
@@ -383,13 +391,13 @@
{{ game.amount }} pts
- {{ new Date(game.created_at).toLocaleTimeString() }} + {{ new Date(game.began_at).toLocaleDateString() }} • {{ new + Date(game.began_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}
- Marks: {{ game.details.marks }} Capotes: {{ game.details.capotes }} Bandeiras: {{ game.details.bandeiras }}
@@ -579,7 +587,6 @@ const error = ref(null) const activeTab = ref('info') const fileInput = ref(null) -// Profile form const profileForm = reactive({ name: '', nickname: '' @@ -588,7 +595,6 @@ const updatingProfile = ref(false) const profileMessage = ref('') const profileMessageType = ref('') -// Password form const passwordForm = reactive({ current_password: '', new_password: '', @@ -598,7 +604,6 @@ const updatingPassword = ref(false) const passwordMessage = ref('') const passwordMessageType = ref('') -// Delete account const deleteConfirmEmail = ref('') const showDeleteConfirmation = ref(false) const deletingAccount = ref(false) @@ -622,13 +627,12 @@ const filters = ref({ startDate: '', endDate: '', sort: { - column: 'created_at', + column: 'began_at', order: 'desc' } }); const handleSortChange = (column) => { - // If clicking same column, toggle order, else default to desc if (filters.value.sort.column === column) { filters.value.sort.order = filters.value.sort.order === 'asc' ? 'desc' : 'asc'; } else { @@ -660,18 +664,26 @@ const fetchData = async () => { try { let newData = []; - if (isTransaction) { - // const response = await apiStore.getTransactions(params); - // Logic: if response.data.length < limit, set hasMore.value = false - for (let i = 0; i < 15; i++) { - newData.push({ - id: `txn_${Date.now()}_${i}`, - amount: Math.floor(Math.random() * 1000), - type: Math.random() > 0.5 ? 'credit' : 'debit', - created_at: new Date().toISOString() - }); - } + if (isTransaction) { + const apiParams = { + page: state.page, + type: filters.value.type === 'credit' ? 'C' : filters.value.type === 'debit' ? 'D' : undefined, + date_from: filters.value.startDate || undefined, + date_to: filters.value.endDate || undefined, + }; + + const response = await apiStore.getCurrentUserTransactions(apiParams); + const apiTxns = response.data.data; + + newData = apiTxns.map(txn => ({ + id: txn.id.toString(), + type: txn.type.type === 'D' ? 'debit' : 'credit', + category: txn.type.name, + amount: Math.abs(txn.coins), + began_at: txn.transaction_datetime, + reference: txn.match_id ? `Match #${txn.match_id}` : txn.game_id ? `Game #${txn.game_id}` : 'N/A' + })); transactions.value.push(...newData); } @@ -680,25 +692,24 @@ const fetchData = async () => { const apiParams = { page: state.page, type: filters.value.variant !== 'all' ? filters.value.variant : undefined, - status: filters.value.outcome !== 'all' ? 'Ended' : undefined, + outcome: filters.value.outcome !== 'all' ? filters.value.outcome : undefined, + status: 'Ended', + date_from: filters.value.startDate || undefined, + date_to: filters.value.endDate || undefined, sort_by: filters.value.sort.column, - sort_direction: filters.value.sort.order + sort_direction: filters.value.sort.order, }; const response = await apiStore.getCurrentUserMatches(apiParams); - const apiGames = response.data.data + const apiGames = response.data.data; newData = apiGames.map(game => { - const isPlayer1 = game.player1_user_id === authStore.currentUserID; + const isPlayer1 = game.player1_user_id === authStore.currentUser.id; const opponent = isPlayer1 ? game.player2 : game.player1; let outcome = 'loss'; if (game.is_draw) outcome = 'draw'; - else if (game.winner_user_id === authStore.currentUserID) outcome = 'win'; - - const minutes = Math.floor(game.total_time / 60); - const seconds = game.total_time % 60; - const durationStr = `${minutes}m ${seconds}s`; + else if (game.winner_user_id === authStore.currentUser.id) outcome = 'win'; return { id: game.id, @@ -706,16 +717,14 @@ const fetchData = async () => { outcome: outcome, opponent: opponent?.nickname || 'Unknown Player', amount: isPlayer1 ? game.player1_points : game.player2_points, - duration: durationStr, - created_at: game.began_at, + duration: `${Math.floor(game.total_time / 60)}m ${game.total_time % 60}s`, + began_at: game.began_at, details: { - marks: game.custom?.marks || 0, - capotes: game.custom?.capotes || 0, - bandeiras: game.custom?.bandeiras || 0 + capotes: 0, + bandeiras: 0 } }; }); - games.value.push(...newData); } @@ -733,16 +742,23 @@ const setupObserver = () => { const scrollContainer = document.querySelector('.custom-scrollbar'); - observer.value = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !isLoading.value) { + observer.value = new IntersectionObserver(async (entries) => { + const isGame = activeTab.value === 'game-history'; + const hasMore = isGame ? pagination.value.games.hasMore : pagination.value.transactions.hasMore; + + if (entries[0].isIntersecting && !isLoading.value && hasMore) { fetchData(); } - }, { root: scrollContainer, threshold: 0.1 }); + }, { + root: scrollContainer, + threshold: 0.1, + rootMargin: '100px' + }); if (observerTarget.value) observer.value.observe(observerTarget.value); }; -const resetAndFetch = () => { +const resetAndFetch = async () => { if (activeTab.value === 'transaction-history') { transactions.value = []; pagination.value.transactions = { page: 1, hasMore: true }; @@ -753,7 +769,44 @@ const resetAndFetch = () => { pagination.value.games = { page: 1, hasMore: true }; } - fetchData(); + await nextTick(); + await fetchData(); + setupObserver(); +}; + +const clearFilters = () => { + const isGameTab = activeTab.value === 'game-history'; + const defaultSortColumn = isGameTab ? 'began_at' : 'transaction_datetime'; + + const isAlreadyDefault = + filters.value.startDate === '' && + filters.value.endDate === '' && + filters.value.sort.column === defaultSortColumn && + filters.value.sort.order === 'desc' && + (isGameTab + ? (filters.value.variant === 'all' && filters.value.outcome === 'all') + : (filters.value.type === 'all') + ); + + if (isAlreadyDefault) { + return; + } + + if (isGameTab) { + filters.value.variant = 'all'; + filters.value.outcome = 'all'; + } else { + filters.value.type = 'all'; + } + + filters.value.startDate = ''; + filters.value.endDate = ''; + filters.value.sort = { + column: defaultSortColumn, + order: 'desc' + }; + + resetAndFetch(); }; const formatDate = (dateString) => { @@ -895,13 +948,10 @@ const deleteAccount = async () => { try { await axios.delete(`${API_BASE_URL}/users/me`) - // Logout user await authStore.logout() - // Close modal showDeleteConfirmation.value = false - // Redirect to home/login router.push('/login') } catch (err) { deletingAccount.value = false diff --git a/frontend/src/stores/api.js b/frontend/src/stores/api.js index 619b762..4cdba6e 100644 --- a/frontend/src/stores/api.js +++ b/frontend/src/stores/api.js @@ -80,22 +80,35 @@ export const useAPIStore = defineStore('api', () => { const queryParams = new URLSearchParams({ page: params.page || 1, ...(params.type && { type: params.type }), + ...(params.outcome && { outcome: params.outcome }), ...(params.status && { status: params.status }), - sort_by: params.sort || 'began_at', - sort_direction: params.direction || 'desc', + ...(params.date_from && { date_from: params.date_from }), + ...(params.date_to && { date_to: params.date_to }), + sort_by: params.sort_by || 'began_at', + sort_direction: params.sort_direction || 'desc', }).toString() - console.log(params, queryParams) - return axios.get(`${API_BASE_URL}/users/${authStore.currentUserID}/matches?${queryParams}`) } + const getCurrentUserTransactions = (params = {}) => { + const queryParams = new URLSearchParams({ + page: params.page || 1, + ...(params.type && { type: params.type }), + ...(params.date_from && { date_from: params.date_from }), + ...(params.date_to && { date_to: params.date_to }), + }).toString() + + return axios.get(`${API_BASE_URL}/users/${authStore.currentUserID}/transactions?${queryParams}`) + } + return { postLogin, postLogout, getAuthUser, getGames, getCurrentUserMatches, + getCurrentUserTransactions, gameQueryParameters, } }) -- 2.54.0