- - - - {{ p.label }} + + + + 时间范围 + + + {{ p.label }} + + + + + + 渠道/终端/会员/新老:待接入数据后开放 - - + + - 总用户数 - {{ formatInt(userData.total_users) }} - 较上期:{{ formatPct(userData.user_growth) }} - - - 新用户 + 新增用户 {{ formatInt(userData.new_users) }} 较上期:{{ formatPct(userData.new_user_growth) }} - 用户活跃度 - {{ formatPct(userData.active_rate) }} + 活跃用户(DAU) + {{ formatInt(userData.active_users) }} 较上期:{{ formatPct(userData.active_growth) }} + + 下单用户数 + {{ formatInt(userData.ordering_users) }} + 较上期:{{ formatPct(userData.ordering_growth) }} + + + 支付用户数 + {{ formatInt(userData.paid_users) }} + 较上期:{{ formatPct(userData.paid_growth) }} + + + 新客转化率 + {{ formatPct(userData.new_user_conversion_rate) }} + 新客 → 下单/支付 + 复购率 {{ formatPct(userData.repurchase_rate) }} @@ -64,11 +83,11 @@ - + - 用户增长趋势 - {{ selectedPeriodText }} · 新用户 vs 总用户 + 增长与活跃趋势 + {{ selectedPeriodText }} · 新增 vs 活跃(DAU) {{ loading ? '加载中...' : '暂无数据' }} @@ -76,60 +95,110 @@ - - - - 用户洞察 - 留存率 / 新老对比 / 活跃度 / 画像 + + + + 新客转化趋势 + 新客 → 下单/支付(待接入) + + + 暂无数据 / 待接入 + - - - - 用户留存率 - 按留存天数统计 - - + + + + 回访 / 复购趋势 + 复购人数 / 复购率(待接入) - - - 新老用户对比 - GMV、订单数、客单价 - - - - - - 用户活跃度 - 日活/周活/月活 - - - - - - 用户画像 - 性别/年龄/地域 - - + + 暂无数据 / 待接入 - + - 用户分群 & 流量来源 - {{ selectedPeriodText }} · 分群占比 & 来源分布 + 转化漏斗 + 拉新 → 激活 → 转化(待接入埋点/事件) + + + + + + {{ idx + 1 }} + {{ s.step }} + + + {{ formatInt(s.value) }} + 转化:{{ formatPct(calcFunnelRate(idx)) }} + + + + + + + 暂无漏斗数据 / 待接入:UV、PDP、加购、下单、支付 + + + + + + + + 留存与回访 + {{ selectedPeriodText }} · 1/3/7/14/30日留存(Cohort 后续补) + + + + + 留存曲线 + 留存率趋势(待接入) + + + + + + 流失用户占比 + 7/14天未活跃(待接入) + + + 暂无数据 / 待接入 + + + + + + + + + 用户分群(运营可用) + RFM / LTV / 新客分层(后续补) · 当前为基础结构占比 - + + 用户画像(基础) + 性别/年龄/地域(待接入) + + + + + + 渠道来源 + {{ selectedPeriodText }} · 渠道占比(后续可扩展渠道质量表) + + + + @@ -138,272 +207,321 @@ - diff --git a/services/analytics/couponAnalysisService.uts b/services/analytics/couponAnalysisService.uts index d7cae687..960195b0 100644 --- a/services/analytics/couponAnalysisService.uts +++ b/services/analytics/couponAnalysisService.uts @@ -1,4 +1,4 @@ -import { computeDateRange } from './dateRange.uts' +import { computeDateRange, toDateOnly } from './dateRange.uts' import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts' export type CouponAnalysisData = { @@ -11,31 +11,21 @@ export type CouponAnalysisData = { export async function fetchCouponAnalysis(period: string): Promise { const { startIso, endIso } = computeDateRange(period) + const p_start_date = toDateOnly(startIso) + const p_end_date = toDateOnly(endIso) - const overviewRow = await rpcOrNull('rpc_coupon_effectiveness_overview', { - p_start: startIso, - p_end: endIso - } as UTSJSONObject) + const params = { + p_start_date, + p_end_date + } as any - const typeList = await rpcOrEmptyArray('rpc_coupon_type_stats', { - p_start: startIso, - p_end: endIso - } as UTSJSONObject) - - const channelList = await rpcOrEmptyArray('rpc_coupon_channel_stats', { - p_start: startIso, - p_end: endIso - } as UTSJSONObject) - - const trendList = await rpcOrEmptyArray('rpc_coupon_trend_daily', { - p_start: startIso, - p_end: endIso - } as UTSJSONObject) - - const conversionList = await rpcOrEmptyArray('rpc_coupon_conversion_effect', { - p_start: startIso, - p_end: endIso - } as UTSJSONObject) + const [overviewRow, typeList, channelList, trendList, conversionList] = await Promise.all([ + rpcOrNull('rpc_analytics_coupon_overview', params), + rpcOrEmptyArray('rpc_analytics_coupon_by_type', params), + rpcOrEmptyArray('rpc_analytics_coupon_by_channel', params), + rpcOrEmptyArray('rpc_analytics_coupon_trend', params), + rpcOrEmptyArray('rpc_analytics_coupon_conversion', params) + ]) return { overviewRow, typeList, channelList, trendList, conversionList } } diff --git a/services/analytics/customReportService.uts b/services/analytics/customReportService.uts index 0e8efcc9..b5ddc587 100644 --- a/services/analytics/customReportService.uts +++ b/services/analytics/customReportService.uts @@ -1,5 +1,4 @@ -import supa from '@/components/supadb/aksupainstance.uts' -import { rpcOrValue } from './rpc.uts' +import { rpcOrEmptyArray, rpcOrValue } from './rpc.uts' export type CustomReportListItem = { id: string @@ -24,41 +23,41 @@ export type UpdateCustomReportParams = { period: string | null } +function safeString(v: any): string { + return v != null ? `${v}` : '' +} + +// 改造:不再直查 analytics_reports 表,统一通过 RPC 获取当前用户的报表列表 export async function listCustomReports(ownerUserId: string): Promise> { - const res: any = await supa - .from('analytics_reports') - .select('id, title, description, period, updated_at') - .eq('type', 'custom') - .eq('owner_user_id', ownerUserId) - .order('updated_at', { ascending: false } as any) - - if (res?.error != null) { - throw res.error - } - - const rows: Array = Array.isArray(res.data) ? (res.data as Array) : [] + const rows = await rpcOrEmptyArray('rpc_get_custom_reports', {} as any) const list: Array = [] for (let i = 0; i < rows.length; i++) { - const r = rows[i] + const r: any = rows[i] list.push({ - id: `${r.id}`, - title: `${r.title}`, - description: `${r.description || ''}`, - period: `${r.period || ''}`, - updated_at: `${r.updated_at || ''}` + id: safeString(r.getAny?.('id') ?? r.getString?.('id')), + title: safeString(r.getAny?.('title') ?? r.getString?.('title')), + description: safeString(r.getAny?.('description') ?? r.getString?.('description')), + // 兼容旧 UI 字段:custom-report 页面里可能还在用 period 字段 + period: '', + updated_at: safeString(r.getAny?.('updated_at') ?? r.getString?.('updated_at')) }) } return list } +// 改造:RPC 参数改为 p_definition(JSONB),承载 period/metrics/chartType export async function createCustomReport(params: CreateCustomReportParams): Promise { + const definition = { + period: params.period, + metrics: params.metrics, + chartType: params.chartType || 'line' + } + const data = await rpcOrValue('rpc_create_custom_report', { p_title: params.title, p_description: params.description || '', - p_period: params.period, - p_metrics: params.metrics, - p_chart_type: params.chartType || 'line' - } as UTSJSONObject) + p_definition: definition + } as any) if (data == null) { throw new Error('保存失败:未返回报表ID') @@ -68,12 +67,17 @@ export async function createCustomReport(params: CreateCustomReportParams): Prom } export async function updateCustomReport(params: UpdateCustomReportParams): Promise { + // 注意:旧 UI 只传 title/description/period,这里把 period 合并进 definition + const definition = { + period: params.period + } + await rpcOrValue('rpc_update_custom_report', { p_report_id: params.reportId, p_title: params.title, p_description: params.description, - p_period: params.period - } as UTSJSONObject) + p_definition: definition + } as any) return true } @@ -81,7 +85,7 @@ export async function updateCustomReport(params: UpdateCustomReportParams): Prom export async function deleteCustomReport(reportId: string): Promise { await rpcOrValue('rpc_delete_custom_report', { p_report_id: reportId - } as UTSJSONObject) + } as any) return true } diff --git a/services/analytics/dashboardService.uts b/services/analytics/dashboardService.uts index c8ece9ee..453e212b 100644 --- a/services/analytics/dashboardService.uts +++ b/services/analytics/dashboardService.uts @@ -1,5 +1,5 @@ import { computeDateRange, toDateOnly } from './dateRange.uts' -import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts' +import { rpcOrEmptyArray, rpcOrNull, rpcOrValue } from './rpc.uts' export type TrendData = { x: Array; gmv: Array; orders: Array } export type SegmentItem = { name: string; value: number } @@ -17,10 +17,9 @@ export async function fetchDashboardTrend(period: string): Promise { const p_start_date = toDateOnly(startIso) const p_end_date = toDateOnly(endIso) - const rows = await rpcOrEmptyArray('rpc_analytics_trend_data', { + const rows = await rpcOrEmptyArray('rpc_analytics_sales_trend', { p_start_date, - p_end_date, - p_merchant_id: null + p_end_date } as any) const x: Array = [] @@ -28,45 +27,30 @@ export async function fetchDashboardTrend(period: string): Promise { const orders: Array = [] for (let i = 0; i < rows.length; i++) { const row: any = rows[i] - const d = `${row.getString?.('date') ?? row.getString?.('day') ?? row.getString?.('date_key') ?? ''}` - if (d && d.length >= 10) x.push(d.slice(5)) - else x.push(`${i + 1}`) - gmv.push(safeNumber(row.getAny?.('gmv') ?? row.getAny?.('total_amount') ?? 0)) - orders.push(safeNumber(row.getAny?.('orders') ?? row.getAny?.('order_count') ?? 0)) + const d = `${row.getAny?.('date') ?? ''}` + x.push(d.length >= 10 ? d.slice(5) : d) + gmv.push(safeNumber(row.getAny?.('gmv') ?? 0)) + orders.push(safeNumber(row.getAny?.('orders') ?? 0)) } return { x, gmv, orders } } export async function fetchDashboardRealtime(): Promise { - const now = new Date() - const today0 = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const todayISO = today0.toISOString() + const [kpiRow, onlineUsersVal] = await Promise.all([ + rpcOrNull('rpc_analytics_realtime_kpis', {} as any), + rpcOrValue('rpc_analytics_online_users', {} as any) + ]) - const ySame = new Date(now.getTime() - 24 * 60 * 60 * 1000) - const y0 = new Date(ySame.getFullYear(), ySame.getMonth(), ySame.getDate()) + const obj: any = kpiRow != null ? kpiRow : ({} as any) - const row = await rpcOrNull('rpc_analytics_realtime_kpis', { - p_start: todayISO, - p_end: now.toISOString(), - p_compare_start: y0.toISOString(), - p_compare_end: ySame.toISOString(), - p_merchant_id: null - } as any) - - const safe = (v: any): number => { - const n = Number(v) - return isFinite(n) ? n : 0 - } - - const obj: any = row != null ? row : ({} as any) return { - gmv: Math.round(safe(obj.getAny?.('gmv') ?? obj.getAny?.('total_gmv') ?? obj.getAny?.('revenue') ?? 0)), - gmv_growth: safe(obj.getAny?.('gmv_growth') ?? obj.getAny?.('gmv_growth_rate') ?? obj.getAny?.('revenue_growth') ?? 0), - orders: Math.round(safe(obj.getAny?.('orders') ?? obj.getAny?.('order_count') ?? obj.getAny?.('total_orders') ?? 0)), - order_growth: safe(obj.getAny?.('order_growth') ?? obj.getAny?.('order_growth_rate') ?? 0), - online_users: Math.round(safe(obj.getAny?.('online_users') ?? obj.getAny?.('active_users') ?? obj.getAny?.('current_users') ?? 0)), - conversion_rate: safe(obj.getAny?.('conversion_rate') ?? obj.getAny?.('conversion') ?? 0), - conversion_growth: safe(obj.getAny?.('conversion_growth') ?? obj.getAny?.('conversion_growth_rate') ?? 0) + gmv: Math.round(safeNumber(obj.getAny?.('gmv') ?? 0)), + gmv_growth: safeNumber(obj.getAny?.('gmv_growth') ?? 0), + orders: Math.round(safeNumber(obj.getAny?.('orders') ?? 0)), + order_growth: safeNumber(obj.getAny?.('order_growth') ?? 0), + online_users: Math.round(safeNumber(onlineUsersVal ?? 0)), + conversion_rate: safeNumber(obj.getAny?.('conversion_rate') ?? 0), + conversion_growth: safeNumber(obj.getAny?.('conversion_growth') ?? 0) } } @@ -75,8 +59,7 @@ export async function fetchDashboardTopProducts(period: string, limit: number = const rows = await rpcOrEmptyArray('rpc_analytics_top_products', { p_start_date: toDateOnly(startIso), p_end_date: toDateOnly(endIso), - p_limit: limit, - p_merchant_id: null + p_limit: limit } as any) const list: Array = [] @@ -86,7 +69,7 @@ export async function fetchDashboardTopProducts(period: string, limit: number = id: `${row.getAny?.('id') ?? i}`, rank: i + 1, name: `${row.getAny?.('name') ?? '未知商品'}`, - sales: safeNumber(row.getAny?.('sales') ?? row.getAny?.('total_amount') ?? 0) + sales: safeNumber(row.getAny?.('sales') ?? 0) }) } return list @@ -107,8 +90,8 @@ export async function fetchDashboardTopMerchants(period: string, limit: number = id: `${row.getAny?.('id') ?? i}`, rank: i + 1, name: `${row.getAny?.('name') ?? row.getAny?.('shop_name') ?? '未知商家'}`, - sales: safeNumber(row.getAny?.('sales') ?? row.getAny?.('total_amount') ?? 0), - growth: safeNumber(row.getAny?.('growth') ?? row.getAny?.('growth_rate') ?? 0) + sales: safeNumber(row.getAny?.('sales') ?? 0), + growth: safeNumber(row.getAny?.('growth') ?? 0) }) } return list diff --git a/services/analytics/dataDetailService.uts b/services/analytics/dataDetailService.uts index 579c4fe9..6a3065cf 100644 --- a/services/analytics/dataDetailService.uts +++ b/services/analytics/dataDetailService.uts @@ -1,70 +1,58 @@ import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts' -export type DataDetailReportInfo = { - period: string -} - -export type DataDetailRow = { +export type ReportInfo = { id: string - date: string - gmv: number - orders: number - users: number + title: string + description: string + definition: any + updated_at: string } -export type DataDetailDrillItem = { - id: string - label: string - value: string - type: string +function safeString(v: any): string { + return v != null ? `${v}` : '' } -export async function fetchDataDetailReportInfo(reportId: string): Promise { - const info = await rpcOrNull('rpc_data_detail_report_info', { +// 改造:调用 rpc_data_detail_report_info +export async function fetchReportInfo(reportId: string): Promise { + const row = await rpcOrNull('rpc_data_detail_report_info', { p_report_id: reportId - } as UTSJSONObject) - if (info == null) return null - return { period: info.getString('period') ?? '' } + } as any) + + if (row == null) return null + + return { + id: safeString(row.getAny?.('id')), + title: safeString(row.getAny?.('title')), + description: safeString(row.getAny?.('description')), + definition: row.getAny?.('definition'), + updated_at: safeString(row.getAny?.('updated_at')) + } } -export async function fetchDataDetailRows(reportId: string, sortBy: string, sortDir: string, limit: number, offset: number): Promise> { - const rows = await rpcOrEmptyArray('rpc_data_detail_rows', { +// 改造:调用 rpc_data_detail_rows +export async function fetchReportRows(reportId: string, params: any): Promise> { + const result = await rpcOrNull('rpc_data_detail_rows', { p_report_id: reportId, - p_sort_by: sortBy, - p_sort_dir: sortDir, - p_limit: limit, - p_offset: offset - } as UTSJSONObject) + p_params: params + } as any) - const out: Array = [] - for (let i = 0; i < rows.length; i++) { - const r = rows[i] - const dayStr = r.getString('row_date') ?? '' - out.push({ - id: dayStr + '_' + i.toString(), - date: dayStr, - gmv: r.getNumber('gmv') ?? 0, - orders: r.getNumber('orders') ?? 0, - users: r.getNumber('users') ?? 0 - }) - } - return out + if (result == null) return [] + const anyData = result as any + return Array.isArray(anyData) ? (anyData as Array) : ([] as Array) } -export async function fetchDataDetailDrillItems(reportId: string): Promise> { - const rows = await rpcOrEmptyArray('rpc_data_detail_drill_items', { - p_report_id: reportId - } as UTSJSONObject) - - const out: Array = [] - for (let i = 0; i < rows.length; i++) { - const r = rows[i] - out.push({ - id: `${r.getAny('id') ?? i}`, - label: `${r.getString('label') ?? ''}`, - value: `${r.getAny('value') ?? ''}`, - type: `${r.getString('type') ?? ''}` - }) - } - return out +// 保留调用,但 RPC 是模拟数据 +export async function fetchDrilldown(reportId: string, itemId: string): Promise> { + return await rpcOrEmptyArray('rpc_data_detail_drill_items', { + p_report_id: reportId, + p_item_id: itemId + } as any) +} + +// 保留调用,但 RPC 是模拟数据 +export async function fetchComparison(itemId: string, period: string): Promise> { + return await rpcOrEmptyArray('rpc_data_detail_compare_gmv', { + p_item_id: itemId, + p_period: period + } as any) } diff --git a/services/analytics/deliveryAnalysisService.uts b/services/analytics/deliveryAnalysisService.uts index 548c7adc..4e4f9fe5 100644 --- a/services/analytics/deliveryAnalysisService.uts +++ b/services/analytics/deliveryAnalysisService.uts @@ -1,5 +1,5 @@ -import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts' import { computeDateRange } from './dateRange.uts' +import { rpcOrEmptyArray } from './rpc.uts' export type DeliveryAnalysisData = { trendList: Array @@ -11,122 +11,16 @@ export type DeliveryAnalysisData = { export async function fetchDeliveryAnalysis(period: string): Promise { const { startIso, endIso } = computeDateRange(period) - await ensureSupabaseReady() - - // 优先走 RPC(需要在 Supabase 执行 DELIVERY_ANALYSIS_RPCS.sql 创建函数) - let trendList: Array = [] - let topList: Array = [] - - const trendRes: any = await supa.rpc('rpc_delivery_efficiency_daily', { + const trendList = await rpcOrEmptyArray('rpc_delivery_efficiency_daily', { p_start: startIso, p_end: endIso - } as UTSJSONObject) + } as any) - if (trendRes.status === 404) { - // RPC 不存在:降级到直查表聚合(测试阶段兜底) - const taskRes: any = await supa - .from('ml_delivery_tasks') - .select('id,driver_id,assigned_at,delivered_at,delivery_fee', {}) - .eq('status', 5) - .gte('assigned_at', startIso) - .order('assigned_at', { ascending: true } as any) - .execute() - - if (taskRes?.error != null) throw taskRes.error - - const rowsAny = (taskRes.data != null ? taskRes.data : []) as any - const tasks = Array.isArray(rowsAny) ? (rowsAny as Array) : [] - - const dayAgg = new Map() - const driverAgg = new Map() - const driverFeeAgg = new Map() - const driverTimeAgg = new Map() - - for (let i = 0; i < tasks.length; i++) { - const t = tasks[i] - const assignedAt = t.getString('assigned_at') ?? '' - const deliveredAt = t.getString('delivered_at') ?? '' - const driverId = t.getString('driver_id') ?? '' - if (assignedAt.trim() === '' || deliveredAt.trim() === '') continue - - const day = assignedAt.length >= 10 ? assignedAt.substring(0, 10) : assignedAt - const a = new Date(assignedAt) - const d = new Date(deliveredAt) - const diffMin = Math.max(0, (d.getTime() - a.getTime()) / 60000) - const fee = t.getNumber('delivery_fee') ?? 0 - - const old = dayAgg.get(day) - if (old == null) { - const obj = new UTSJSONObject() - obj.set('day', day) - obj.set('completed_orders', 1) - obj.set('sum_minutes', diffMin) - obj.set('total_fee', fee) - dayAgg.set(day, obj) - } else { - old.set('completed_orders', (old.getNumber('completed_orders') ?? 0) + 1) - old.set('sum_minutes', (old.getNumber('sum_minutes') ?? 0) + diffMin) - old.set('total_fee', (old.getNumber('total_fee') ?? 0) + fee) - } - - if (driverId.trim() !== '') { - driverAgg.set(driverId, (driverAgg.get(driverId) ?? 0) + 1) - driverFeeAgg.set(driverId, (driverFeeAgg.get(driverId) ?? 0) + fee) - driverTimeAgg.set(driverId, (driverTimeAgg.get(driverId) ?? 0) + diffMin) - } - } - - // dayAgg -> trendList - const days = Array.from(dayAgg.keys()).sort() - for (let i = 0; i < days.length; i++) { - const day = days[i] - const obj = dayAgg.get(day) - if (obj != null) { - const completed = obj.getNumber('completed_orders') ?? 0 - const sumMin = obj.getNumber('sum_minutes') ?? 0 - const totalFee = obj.getNumber('total_fee') ?? 0 - const out = new UTSJSONObject() - out.set('day', day) - out.set('avg_delivery_time', completed > 0 ? sumMin / completed : 0) - out.set('total_fee', totalFee) - out.set('completed_orders', completed) - trendList.push(out) - } - } - - // driverAgg -> topList (Top10) - const drivers = Array.from(driverAgg.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10) - for (let i = 0; i < drivers.length; i++) { - const [driverId, orders] = drivers[i] - const out = new UTSJSONObject() - out.set('driver_id', driverId) - out.set('orders', orders) - out.set('total_fee', driverFeeAgg.get(driverId) ?? 0) - out.set('total_minutes', driverTimeAgg.get(driverId) ?? 0) - topList.push(out) - } - } else if (trendRes.error != null) { - throw trendRes.error - } else { - const anyData = trendRes.data as any - trendList = Array.isArray(anyData) ? (anyData as Array) : [] - - // Top drivers - const topRes = await supa.rpc('rpc_delivery_efficiency_top_drivers', { - p_start: startIso, - p_end: endIso, - p_limit: 10 - }) - - if (topRes.status === 404) { - console.warn('rpc_delivery_efficiency_top_drivers not found, top drivers will be empty') - } else if (topRes.error != null) { - throw topRes.error - } else { - const topAny = topRes.data as any - topList = Array.isArray(topAny) ? (topAny as Array) : [] - } - } + const topList = await rpcOrEmptyArray('rpc_delivery_efficiency_top_drivers', { + p_start: startIso, + p_end: endIso, + p_limit: 10 + } as any) return { trendList, topList, startIso, endIso } } diff --git a/services/analytics/productInsightsService.uts b/services/analytics/productInsightsService.uts index 29bdbeaf..34c13b19 100644 --- a/services/analytics/productInsightsService.uts +++ b/services/analytics/productInsightsService.uts @@ -23,8 +23,8 @@ function safeNumber(v: any): number { export async function fetchProductOverview(period: string): Promise { const { startIso, endIso } = computeDateRange(period) const row = await rpcOrNull('rpc_product_insights_overview', { - p_start: startIso, - p_end: endIso + p_start: toDateOnly(startIso), + p_end: toDateOnly(endIso) } as any) const obj: any = row != null ? row : ({} as any) @@ -44,8 +44,7 @@ export async function fetchTopProducts(period: string, limit: number = 10): Prom const rows = await rpcOrEmptyArray('rpc_analytics_top_products', { p_start_date: toDateOnly(startIso), p_end_date: toDateOnly(endIso), - p_limit: limit, - p_merchant_id: null + p_limit: limit } as any) const list: Array = [] @@ -55,8 +54,8 @@ export async function fetchTopProducts(period: string, limit: number = 10): Prom id: `${r.getAny?.('id') ?? i}`, rank: i + 1, name: `${r.getAny?.('name') ?? '未知商品'}`, - sales: safeNumber(r.getAny?.('sales') ?? r.getAny?.('total_amount') ?? 0), - growth: safeNumber(r.getAny?.('growth') ?? r.getAny?.('growth_rate') ?? 0) + sales: safeNumber(r.getAny?.('sales') ?? 0), + growth: safeNumber(r.getAny?.('growth') ?? 0) }) } return list @@ -73,12 +72,12 @@ export async function fetchProductTrend(period: string, productId: string): Prom const out: Array = [] for (let i = 0; i < rows.length; i++) { const r: any = rows[i] - const date = `${r.getAny?.('date') ?? r.getAny?.('day') ?? r.getAny?.('date_key') ?? ''}` + const date = `${r.getAny?.('date') ?? ''}` out.push({ date, - gmv: safeNumber(r.getAny?.('gmv') ?? r.getAny?.('total_amount') ?? 0), - qty: safeNumber(r.getAny?.('qty') ?? r.getAny?.('sales_qty') ?? 0), - orders: safeNumber(r.getAny?.('orders') ?? r.getAny?.('order_count') ?? 0) + gmv: safeNumber(r.getAny?.('gmv') ?? 0), + qty: safeNumber(r.getAny?.('qty') ?? 0), + orders: safeNumber(r.getAny?.('orders') ?? 0) }) } return out @@ -93,11 +92,7 @@ export async function fetchCategorySales(period: string): Promise> { - const { startIso, endIso } = computeDateRange(period) - return await rpcOrEmptyArray('rpc_product_insights_stock', { - p_start: startIso, - p_end: endIso - } as any) + return await rpcOrEmptyArray('rpc_product_insights_stock', {} as any) } export async function fetchPriceTrend(period: string): Promise> { diff --git a/services/analytics/salesReportService.uts b/services/analytics/salesReportService.uts index c14f3a21..b8749f1b 100644 --- a/services/analytics/salesReportService.uts +++ b/services/analytics/salesReportService.uts @@ -24,22 +24,9 @@ function safeNumber(v: any): number { export async function fetchSalesKpis(period: string): Promise { const { startIso, endIso } = computeDateRange(period) - const days = period === '7d' ? 7 : period === '30d' ? 30 : period === '90d' ? 90 : 365 - - const startDateObj = new Date(startIso) - const endDateObj = new Date(endIso) - - const periodStart = new Date(startDateObj.getFullYear(), startDateObj.getMonth(), startDateObj.getDate()) - const periodEnd = new Date(endDateObj.getFullYear(), endDateObj.getMonth(), endDateObj.getDate() + 1) - const prevStart = new Date(periodStart.getTime() - days * 24 * 60 * 60 * 1000) - const prevEnd = new Date(periodStart.getTime()) - - const row = await rpcOrNull('rpc_analytics_realtime_kpis', { - p_start: periodStart.toISOString(), - p_end: periodEnd.toISOString(), - p_compare_start: prevStart.toISOString(), - p_compare_end: prevEnd.toISOString(), - p_merchant_id: null + const row = await rpcOrNull('rpc_analytics_sales_kpis', { + p_start_date: toDateOnly(startIso), + p_end_date: toDateOnly(endIso) } as any) const obj: any = row != null ? row : ({} as any) @@ -61,10 +48,9 @@ export async function fetchSalesKpis(period: string): Promise { export async function fetchSalesTrend(period: string): Promise { const { startIso, endIso } = computeDateRange(period) - const rows = await rpcOrEmptyArray('rpc_analytics_trend_data', { + const rows = await rpcOrEmptyArray('rpc_analytics_sales_trend', { p_start_date: toDateOnly(startIso), - p_end_date: toDateOnly(endIso), - p_merchant_id: null + p_end_date: toDateOnly(endIso) } as any) const x: Array = [] @@ -87,8 +73,7 @@ export async function fetchSalesTopProducts(period: string, limit: number = 50): const rows = await rpcOrEmptyArray('rpc_analytics_top_products', { p_start_date: toDateOnly(startIso), p_end_date: toDateOnly(endIso), - p_limit: limit, - p_merchant_id: null + p_limit: limit } as any) const list: Array = [] diff --git a/types/analytics.uts b/types/analytics.uts new file mode 100644 index 00000000..b3891595 --- /dev/null +++ b/types/analytics.uts @@ -0,0 +1,3 @@ +// types/analytics.uts + +export type AnalyticsTypesMigrated = true diff --git a/types/analytics/common.uts b/types/analytics/common.uts new file mode 100644 index 00000000..d6e186f6 --- /dev/null +++ b/types/analytics/common.uts @@ -0,0 +1,5 @@ +// types/analytics/common.uts + +export type TimePeriod = { value: string; label: string } +export type ChartType = { value: string; label: string } +export type Metric = { key: string; label: string } diff --git a/types/analytics/coupon.uts b/types/analytics/coupon.uts new file mode 100644 index 00000000..985cab5d --- /dev/null +++ b/types/analytics/coupon.uts @@ -0,0 +1,11 @@ +// types/analytics/coupon.uts + +export type CouponData = { + total_issued: number + issued_growth: number + total_used: number + usage_rate: number + gmv_increase: number + gmv_growth: number + roi: number +} diff --git a/types/analytics/custom-report.uts b/types/analytics/custom-report.uts new file mode 100644 index 00000000..c52f4298 --- /dev/null +++ b/types/analytics/custom-report.uts @@ -0,0 +1,29 @@ +// types/analytics/custom-report.uts + +import type { Metric, TimePeriod, ChartType } from './common.uts' + +export type CustomReport = { + id: string + name: string + description: string + metrics: Array + charts: Array + updated_at: string + period?: string +} + +export type ReportForm = { + name: string + description: string + metrics: Array + period: string + chartType: string +} + +export type ReportFormErrors = { + name: string + description: string + metrics: string + period: string + chartType: string +} diff --git a/types/analytics/dashboard.uts b/types/analytics/dashboard.uts new file mode 100644 index 00000000..f73c7c96 --- /dev/null +++ b/types/analytics/dashboard.uts @@ -0,0 +1,7 @@ +// types/analytics/dashboard.uts + +export type TrendData = { x: Array; gmv: Array; orders: Array } +export type SegmentItem = { name: string; value: number } +export type TrafficItem = { name: string; value: number } +export type TopProductItem = { id: string; rank: number; name: string; sales: number } +export type TopMerchantItem = { id: string; rank: number; name: string; sales: number; growth: number } diff --git a/types/analytics/data-detail.uts b/types/analytics/data-detail.uts new file mode 100644 index 00000000..76d09564 --- /dev/null +++ b/types/analytics/data-detail.uts @@ -0,0 +1,4 @@ +// types/analytics/data-detail.uts + +export type TableColumn = { key: string; label: string; type: string; sortable: boolean } +export type DrillDownItem = { id: string; label: string; value: string; type: string } diff --git a/types/analytics/delivery.uts b/types/analytics/delivery.uts new file mode 100644 index 00000000..203451dd --- /dev/null +++ b/types/analytics/delivery.uts @@ -0,0 +1,25 @@ +// types/analytics/delivery.uts + +/** + * Key Performance Indicators for the Delivery Analysis page. + */ +export type DeliveryData = { + avg_delivery_time: number; + time_growth: number; + total_fee: number; + avg_fee: number; + avg_orders_per_driver: number; + satisfaction_rate: number; + satisfaction_growth: number; +}; + +/** + * Represents a driver's ranking based on performance. + */ +export type DriverRank = { + id: string; + rank: number; + name: string; + orders: number; + rating: number +}; diff --git a/types/analytics/insight.uts b/types/analytics/insight.uts new file mode 100644 index 00000000..f86175a3 --- /dev/null +++ b/types/analytics/insight.uts @@ -0,0 +1,19 @@ +// types/analytics/insight.uts + +export type InsightDetail = { + id: string + report_id: string + type: string + impact: string + title: string + content: string + created_at: string +} + +export type RelatedReport = { + id: string + title: string + type: string + period: string + generated_at: string +} diff --git a/types/analytics/market.uts b/types/analytics/market.uts new file mode 100644 index 00000000..8ea60a5c --- /dev/null +++ b/types/analytics/market.uts @@ -0,0 +1,9 @@ +// types/analytics/market.uts + +export type MarketTrendsResponse = { + trendRows: any + categoryRows: any + seasonalRows: any + priceRows: any + competitionRows: any +} diff --git a/types/analytics/product.uts b/types/analytics/product.uts new file mode 100644 index 00000000..46286048 --- /dev/null +++ b/types/analytics/product.uts @@ -0,0 +1,14 @@ +// types/analytics/product.uts + +export type ProductData = { + total_products: number + product_growth: number + hot_products: number + turnover_rate: number + turnover_growth: number + avg_stock: number + stock_growth: number +} + +export type ProductRank = { id: string; rank: number; name: string; sales: number; growth: number } +export type ProductTrendRow = { date: string; gmv: number; qty: number; orders: number } diff --git a/types/analytics/profile.uts b/types/analytics/profile.uts new file mode 100644 index 00000000..610d0221 --- /dev/null +++ b/types/analytics/profile.uts @@ -0,0 +1,38 @@ +// types/analytics/profile.uts + +export type ReportStatus = 'pending' | 'ready' | 'failed' | 'scheduled' | 'shared' | string + +export type RecentReport = { + id: string + title: string + description: string + status: ReportStatus + created_at: string +} + +export type OverviewData = { + totalSales: string + salesGrowth: number + totalUsers: string + userGrowth: number + totalOrders: string + orderGrowth: number + conversionRate: number + conversionGrowth: number +} + +export type ReportCounts = { + total: number + pending: number + scheduled: number + shared: number +} + +export type TodayInsights = { + hotProduct: string + peakTraffic: string + conversionAnomaly: string + mobileRatio: number +} + +export type TrendDatum = { label: string; sales: number; orders: number } diff --git a/types/analytics/report-detail.uts b/types/analytics/report-detail.uts new file mode 100644 index 00000000..3cdc1786 --- /dev/null +++ b/types/analytics/report-detail.uts @@ -0,0 +1,46 @@ +// types/analytics/report-detail.uts + +export type ReportType = { + id: string; + title: string; + type: string; + period: string; + generated_at: string; + description: string; +}; + +export type MetricType = { + key: string; + label: string; + value: number; + format: string; + icon: string; + color: string; + change: number; +}; + +export type ChartTabType = { + key: string; + label: string; +}; + +export type ChartLegendType = { + key: string; + label: string; + color: string; +}; + +export type TableColumnType = { + key: string; + title: string; + width: string; + type: string; +}; + +export type InsightType = { + id: string; + type: string; + title: string; + content: string; + impact: string; +}; diff --git a/types/analytics/sales.uts b/types/analytics/sales.uts new file mode 100644 index 00000000..55e4757b --- /dev/null +++ b/types/analytics/sales.uts @@ -0,0 +1,22 @@ +// types/analytics/sales.uts + +// Re-exporting shared types from dashboard for semantic clarity in the sales context. +import type { TrendData, TopProductItem, TopMerchantItem } from './dashboard.uts' + +export type SalesTrendData = TrendData +export type ProductRank = TopProductItem +export type MerchantRank = TopMerchantItem + +/** + * Key Performance Indicators for the Sales Report page. + */ +export type SalesData = { + gmv: number + gmv_growth: number + orders: number + order_growth: number + conversion_rate: number + conversion_growth: number + avg_order_amount: number + avg_order_growth: number +} diff --git a/types/analytics/user.uts b/types/analytics/user.uts new file mode 100644 index 00000000..216404cb --- /dev/null +++ b/types/analytics/user.uts @@ -0,0 +1,25 @@ +// types/analytics/user.uts + +/** + * Key Performance Indicators for the User Analysis page. + */ +export type UserData = { + total_users: number; + user_growth: number; + new_users: number; + new_user_growth: number; + active_users: number; + active_growth: number; + ordering_users: number; + ordering_growth: number; + paid_users: number; + paid_growth: number; + new_user_conversion_rate: number; + repurchase_rate: number; + repurchase_growth: number; +} + +/** + * Represents a single step in a conversion funnel. + */ +export type FunnelStep = { step: string; value: number };