Skip to content

Commit 2b917a4

Browse files
Leoglmeoz-agent
andcommitted
refactor(dashboard): replace period selector with range preset picker
- Swap GoupixDexDashboardPeriodSelect for GoupixDexDashboardRangePresetSelect - Introduce dashboardRange utility for range preset logic - Update API stats route and service to support new range handling - Refactor dashboard charts, useStats composable, and dashboard page - Sync UI prefs local storage and default layout for new range flows Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 6d2c954 commit 2b917a4

10 files changed

Lines changed: 157 additions & 161 deletions

File tree

api/routes/stats_route.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ def _parse_iso(value: str | None) -> dt.datetime | None:
3333
def dashboard(
3434
db: Annotated[Session, Depends(get_db)],
3535
user: Annotated[User, Depends(get_current_user)],
36-
include_market: bool = Query(
37-
False,
38-
description="If true, sums Cardmarket EUR per article via PokéWallet (slower).",
39-
),
4036
start: str | None = Query(None, description="Range start (ISO date or datetime)."),
4137
end: str | None = Query(None, description="Range end (ISO date or datetime)."),
4238
period: Literal["daily", "weekly", "monthly"] = Query(
@@ -47,7 +43,6 @@ def dashboard(
4743
return compute_dashboard_stats(
4844
db,
4945
user.id,
50-
include_market=include_market,
5146
range_start=_parse_iso(start),
5247
range_end=_parse_iso(end),
5348
period=period,

api/services/stats_service.py

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
from models.article import Article
1111
from services import article_service
12-
from services.pricing_service import fetch_card_prices
13-
1412
Period = Literal["daily", "weekly", "monthly"]
1513

1614

@@ -88,7 +86,6 @@ def compute_dashboard_stats(
8886
db: Session,
8987
user_id: int,
9088
*,
91-
include_market: bool = False,
9289
range_start: dt.datetime | None = None,
9390
range_end: dt.datetime | None = None,
9491
period: Period = "daily",
@@ -168,10 +165,10 @@ def in_range(a: Article) -> bool:
168165
if p is not None
169166
)
170167

171-
top_profitable = sorted(sold, key=_profit_eur, reverse=True)[:5]
168+
top_profitable = sorted(sold, key=_profit_eur, reverse=True)
172169
with_duration = [(a, _hours_to_sell(a)) for a in sold]
173170
with_duration = [(a, h) for a, h in with_duration if h is not None]
174-
fastest = sorted(with_duration, key=lambda x: x[1])[:5]
171+
fastest = sorted(with_duration, key=lambda x: x[1])
175172

176173
unsold = [a for a in rows if not a.is_sold]
177174
inventory_count = len(unsold)
@@ -227,25 +224,6 @@ def in_range(a: Article) -> bool:
227224
}
228225
)
229226

230-
market_sum: float | None = None
231-
market_sum_unsold: float | None = None
232-
market_errors = 0
233-
if include_market:
234-
market_sum = 0.0
235-
market_sum_unsold = 0.0
236-
for a in rows:
237-
if not a.set_code or not a.card_number:
238-
continue
239-
p = fetch_card_prices(a.set_code, a.card_number, a.pokemon_name)
240-
if p.get("error"):
241-
market_errors += 1
242-
cm = p.get("cardmarket_eur")
243-
if cm is not None:
244-
v = float(cm)
245-
market_sum += v
246-
if not a.is_sold:
247-
market_sum_unsold += v
248-
249227
return {
250228
"range": {
251229
"start": range_start.isoformat(),
@@ -273,11 +251,9 @@ def in_range(a: Article) -> bool:
273251
"inventory_purchase_total_eur": round(inventory_purchase_total_eur, 2),
274252
"inventory_sell_total_eur": round(inventory_sell_total_eur, 2),
275253
"inventory_estimated_profit_eur": round(inventory_estimated_profit_eur, 2),
276-
"estimated_cardmarket_inventory_eur": round(market_sum, 2) if market_sum is not None else None,
277-
"estimated_cardmarket_unsold_eur": round(market_sum_unsold, 2)
278-
if market_sum_unsold is not None
279-
else None,
280-
"market_lookup_errors": market_errors,
254+
"estimated_cardmarket_inventory_eur": None,
255+
"estimated_cardmarket_unsold_eur": None,
256+
"market_lookup_errors": 0,
281257
"revenue_timeline": revenue_timeline,
282258
"recent_sales": recent_sales_payload,
283259
"top_profitable": [

web/app/components/dashboard/GoupixDexCharts.client.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
<UCard>
44
<template #header>
55
<p class="text-highlighted text-sm font-medium">Cartes les plus rentables</p>
6-
<p class="text-muted text-xs">Marge par article (toutes périodes)</p>
6+
<p class="text-muted text-xs">Toutes vos ventes classées par marge décroissante</p>
77
</template>
8-
<ul v-if="stats?.top_profitable?.length" class="divide-default divide-y">
8+
<ul
9+
v-if="stats?.top_profitable?.length"
10+
class="divide-default max-h-80 divide-y overflow-y-auto overscroll-contain"
11+
>
912
<li v-for="r in stats.top_profitable" :key="r.article_id" class="flex justify-between gap-2 py-2.5 text-sm">
1013
<span class="text-highlighted truncate">{{ r.title }}</span>
1114
<span class="text-primary shrink-0 font-semibold">{{ eur.format(r.profit_eur) }}</span>
@@ -17,9 +20,12 @@
1720
<UCard>
1821
<template #header>
1922
<p class="text-highlighted text-sm font-medium">Ventes les plus rapides</p>
20-
<p class="text-muted text-xs">Délai entre mise en ligne et vente</p>
23+
<p class="text-muted text-xs">Toutes vos ventes avec délai le plus court en premier</p>
2124
</template>
22-
<ul v-if="stats?.fastest_sold?.length" class="divide-default divide-y">
25+
<ul
26+
v-if="stats?.fastest_sold?.length"
27+
class="divide-default max-h-80 divide-y overflow-y-auto overscroll-contain"
28+
>
2329
<li v-for="r in stats.fastest_sold" :key="r.article_id" class="flex justify-between gap-2 py-2.5 text-sm">
2430
<span class="truncate">{{ r.title }}</span>
2531
<span class="text-muted shrink-0 tabular-nums">{{ formatTimeToSell(r.hours_to_sell) }}</span>

web/app/components/dashboard/GoupixDexDashboardPeriodSelect.vue

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<template>
2+
<USelect
3+
:model-value="selected"
4+
:items="items"
5+
value-key="value"
6+
variant="ghost"
7+
class="data-[state=open]:bg-elevated min-w-[12rem]"
8+
:ui="{ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
9+
@update:model-value="onSelect"
10+
/>
11+
</template>
12+
13+
<script setup lang="ts">
14+
import type { ComputedRef } from 'vue'
15+
import type { DashboardRange } from '~/composables/useStats'
16+
import {
17+
dashboardRangeForPreset,
18+
detectDashboardRangePreset,
19+
type DashboardRangePresetId,
20+
} from '~/utils/dashboardRange'
21+
22+
const range = defineModel<DashboardRange>({ required: true })
23+
24+
type Item = { value: DashboardRangePresetId; label: string; disabled?: boolean }
25+
26+
const BASE_ITEMS: Item[] = [
27+
{ value: 'week', label: 'Cette semaine' },
28+
{ value: 'month', label: 'Ce mois-ci' },
29+
{ value: 'days30', label: '30 derniers jours' },
30+
{ value: 'months3', label: '3 derniers mois' },
31+
{ value: 'all', label: 'Depuis toujours' },
32+
]
33+
34+
const selected: ComputedRef<DashboardRangePresetId> = computed(() => detectDashboardRangePreset(range.value))
35+
36+
const items: ComputedRef<Item[]> = computed(() => {
37+
const base = [...BASE_ITEMS]
38+
if (selected.value === 'custom') {
39+
base.push({ value: 'custom', label: 'Personnalisé', disabled: true })
40+
}
41+
return base
42+
})
43+
44+
function onSelect(value: unknown): void {
45+
if (value === 'custom' || typeof value !== 'string') {
46+
return
47+
}
48+
const id = value as Exclude<DashboardRangePresetId, 'custom'>
49+
const allowed: Exclude<DashboardRangePresetId, 'custom'>[] = ['week', 'month', 'days30', 'months3', 'all']
50+
if (!allowed.includes(id)) {
51+
return
52+
}
53+
range.value = dashboardRangeForPreset(id)
54+
}
55+
</script>

web/app/composables/useStats.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ export interface DashboardStats {
7878
}
7979

8080
export interface FetchDashboardOptions {
81-
includeMarket?: boolean
8281
range?: DashboardRange
8382
period?: DashboardPeriod
8483
}
@@ -97,21 +96,19 @@ function toIsoDate(date: Date): string {
9796
/**
9897
* Dashboard aggregates (`GET /stats/dashboard`).
9998
*
100-
* @returns `fetchDashboard` with optional range / period / Cardmarket enrichment flag.
99+
* @returns `fetchDashboard` with optional range and timeline bucket size.
101100
*/
102101
export function useStats() {
103102
const { $api } = useNuxtApp()
104103

105104
/**
106105
* Load dashboard KPIs for the selected window.
107106
*
108-
* @param options - Optional custom range, aggregation period, and market-valuation toggle.
107+
* @param options - Optional custom range and aggregation period for the revenue timeline.
109108
* @returns {Promise<DashboardStats>} Full dashboard payload from the API.
110109
*/
111110
async function fetchDashboard(options: FetchDashboardOptions = {}) {
112-
const params: Record<string, string | boolean> = {
113-
include_market: options.includeMarket ?? false,
114-
}
111+
const params: Record<string, string> = {}
115112
if (options.range) {
116113
params.start = toIsoDate(options.range.start)
117114
params.end = toIsoDate(options.range.end)

web/app/composables/useUiPrefsLocalStorage.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Persistent UI preferences (localStorage), client-only.
33
*/
44

5-
import type { DashboardPeriod, DashboardRange } from '~/composables/useStats'
5+
import type { DashboardRange } from '~/composables/useStats'
66

77
const ARTICLES_LIST_KEY = 'goupix_articles_list_prefs'
88
const DASHBOARD_KEY = 'goupix_dashboard_prefs'
@@ -76,8 +76,6 @@ export function saveArticleListPrefs(prefs: Partial<ArticleListPrefs>): void {
7676

7777
export interface DashboardPrefsPersisted {
7878
range: { startIso: string; endIso: string }
79-
period: DashboardPeriod
80-
fetchMarketData: boolean
8179
}
8280

8381
/**
@@ -108,12 +106,6 @@ export function loadDashboardPrefs(): Partial<DashboardPrefsPersisted> | null {
108106
endIso: (p.range as { endIso: string }).endIso,
109107
}
110108
}
111-
if (p.period === 'daily' || p.period === 'weekly' || p.period === 'monthly') {
112-
out.period = p.period
113-
}
114-
if (typeof p.fetchMarketData === 'boolean') {
115-
out.fetchMarketData = p.fetchMarketData
116-
}
117109
return Object.keys(out).length ? out : null
118110
} catch {
119111
return null
@@ -123,14 +115,10 @@ export function loadDashboardPrefs(): Partial<DashboardPrefsPersisted> | null {
123115
/**
124116
* Persist dashboard UI preferences (range as ISO strings).
125117
*
126-
* @param prefs - Selected date range, stats period, and whether to include Cardmarket lookups.
118+
* @param prefs - Selected date range for dashboard KPIs.
127119
* @returns {void} Nothing.
128120
*/
129-
export function saveDashboardPrefs(prefs: {
130-
range: DashboardRange
131-
period: DashboardPeriod
132-
fetchMarketData: boolean
133-
}): void {
121+
export function saveDashboardPrefs(prefs: { range: DashboardRange }): void {
134122
if (!import.meta.client) {
135123
return
136124
}
@@ -140,8 +128,6 @@ export function saveDashboardPrefs(prefs: {
140128
startIso: prefs.range.start.toISOString(),
141129
endIso: prefs.range.end.toISOString(),
142130
},
143-
period: prefs.period,
144-
fetchMarketData: prefs.fetchMarketData,
145131
}
146132
localStorage.setItem(DASHBOARD_KEY, JSON.stringify(payload))
147133
} catch {

web/app/layouts/default.vue

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,6 @@ const links: ComputedRef<NavigationMenuItem[][]> = computed(() => {
120120
open.value = false
121121
},
122122
},
123-
{
124-
label: 'Journal des publications',
125-
icon: 'i-lucide-scroll-text',
126-
to: '/articles/listing-logs',
127-
onSelect: () => {
128-
open.value = false
129-
},
130-
},
131123
{
132124
label: 'Prix du marché',
133125
icon: 'i-lucide-trending-up',

0 commit comments

Comments
 (0)