Skip to content

Commit d15d57c

Browse files
chore: wip
1 parent c1f4ad2 commit d15d57c

File tree

2 files changed

+311
-0
lines changed

2 files changed

+311
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import type { CategoryJsonResponse } from '../../../../orm/src/models/Category'
2+
import type { CategoryStats } from '../types'
3+
import { db } from '@stacksjs/database'
4+
5+
/**
6+
* Fetch all categories from the database
7+
*/
8+
export async function fetchAll(): Promise<CategoryJsonResponse[]> {
9+
const categories = await db
10+
.selectFrom('categories')
11+
.selectAll()
12+
.execute()
13+
14+
return categories
15+
}
16+
17+
/**
18+
* Fetch a category by ID
19+
*/
20+
export async function fetchById(id: number): Promise<CategoryJsonResponse | undefined> {
21+
return await db
22+
.selectFrom('categories')
23+
.where('id', '=', id)
24+
.selectAll()
25+
.executeTakeFirst()
26+
}
27+
28+
/**
29+
* Fetch a category by name
30+
*/
31+
export async function fetchByName(name: string): Promise<CategoryJsonResponse | undefined> {
32+
return await db
33+
.selectFrom('categories')
34+
.where('name', '=', name)
35+
.selectAll()
36+
.executeTakeFirst()
37+
}
38+
39+
/**
40+
* Fetch active categories (is_active = true)
41+
*/
42+
export async function fetchActive(): Promise<CategoryJsonResponse[]> {
43+
const categories = await db
44+
.selectFrom('categories')
45+
.where('is_active', '=', true)
46+
.selectAll()
47+
.execute()
48+
49+
return categories
50+
}
51+
52+
/**
53+
* Fetch root categories (parent_category_id is null)
54+
*/
55+
export async function fetchRootCategories(): Promise<CategoryJsonResponse[]> {
56+
const categories = await db
57+
.selectFrom('categories')
58+
.where('parent_category_id', 'is', null)
59+
.where('is_active', '=', true)
60+
.selectAll()
61+
.execute()
62+
63+
return categories
64+
}
65+
66+
/**
67+
* Fetch child categories for a given parent category ID
68+
*/
69+
export async function fetchChildCategories(parentId: string): Promise<CategoryJsonResponse[]> {
70+
const categories = await db
71+
.selectFrom('categories')
72+
.where('parent_category_id', '=', parentId)
73+
.where('is_active', '=', true)
74+
.selectAll()
75+
.execute()
76+
77+
return categories
78+
}
79+
80+
/**
81+
* Fetch categories sorted by display order
82+
*/
83+
export async function fetchByDisplayOrder(ascending: boolean = true): Promise<CategoryJsonResponse[]> {
84+
const query = db
85+
.selectFrom('categories')
86+
.where('is_active', '=', true)
87+
.selectAll()
88+
89+
if (ascending) {
90+
return await query.orderBy('display_order', 'asc').execute()
91+
} else {
92+
return await query.orderBy('display_order', 'desc').execute()
93+
}
94+
}
95+
96+
/**
97+
* Get category statistics
98+
*/
99+
export async function fetchStats(): Promise<CategoryStats> {
100+
// Total categories
101+
const totalCategories = await db
102+
.selectFrom('categories')
103+
.select(eb => eb.fn.count('id').as('count'))
104+
.executeTakeFirst()
105+
106+
// Active categories
107+
const activeCategories = await db
108+
.selectFrom('categories')
109+
.where('is_active', '=', true)
110+
.select(eb => eb.fn.count('id').as('count'))
111+
.executeTakeFirst()
112+
113+
// Root vs child categories
114+
const rootCategories = await db
115+
.selectFrom('categories')
116+
.where('parent_category_id', 'is', null)
117+
.select(eb => eb.fn.count('id').as('count'))
118+
.executeTakeFirst()
119+
120+
// Categories with images
121+
const categoriesWithImages = await db
122+
.selectFrom('categories')
123+
.where('image_url', 'is not', null)
124+
.select(eb => eb.fn.count('id').as('count'))
125+
.executeTakeFirst()
126+
127+
// Recently added categories (last 30 days)
128+
const thirtyDaysAgo = new Date()
129+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
130+
const thirtyDaysAgoStr = thirtyDaysAgo.toISOString()
131+
132+
const recentlyAddedCategories = await db
133+
.selectFrom('categories')
134+
.where('created_at', '>=', thirtyDaysAgoStr)
135+
.selectAll()
136+
.limit(5)
137+
.execute()
138+
139+
// Categories by parent (top 5 parents with most children)
140+
const categoriesByParent = await db
141+
.selectFrom('categories as c')
142+
.leftJoin('categories as parent', 'c.parent_category_id', 'parent.id')
143+
.where('c.parent_category_id', 'is not', null)
144+
.select([
145+
'c.parent_category_id',
146+
'parent.name as parent_name',
147+
eb => eb.fn.count('c.id').as('child_count'),
148+
])
149+
.groupBy(['c.parent_category_id', 'parent.name'])
150+
.orderBy('child_count', 'desc')
151+
.limit(5)
152+
.execute()
153+
154+
return {
155+
total: Number(totalCategories?.count || 0),
156+
active: Number(activeCategories?.count || 0),
157+
root_categories: Number(rootCategories?.count || 0),
158+
child_categories: Number(totalCategories?.count || 0) - Number(rootCategories?.count || 0),
159+
with_images: Number(categoriesWithImages?.count || 0),
160+
recently_added: recentlyAddedCategories,
161+
top_parent_categories: categoriesByParent.map(item => ({
162+
id: String(item.parent_category_id || ''),
163+
name: String(item.parent_name || ''),
164+
child_count: Number(item.child_count),
165+
})),
166+
}
167+
}
168+
169+
/**
170+
* Compare category growth between different time periods
171+
* @param daysRange Number of days to look back (7, 30, 60, etc.)
172+
*/
173+
export async function compareCategoryGrowth(daysRange: number = 30): Promise<{
174+
current_period: number
175+
previous_period: number
176+
difference: number
177+
percentage_change: number
178+
days_range: number
179+
}> {
180+
const today = new Date()
181+
const todayStr = today.toISOString()
182+
183+
// Current period (last N days)
184+
const currentPeriodStart = new Date()
185+
currentPeriodStart.setDate(today.getDate() - daysRange)
186+
const currentPeriodStartStr = currentPeriodStart.toISOString()
187+
188+
// Previous period (N days before the current period)
189+
const previousPeriodEnd = new Date(currentPeriodStart)
190+
previousPeriodEnd.setDate(previousPeriodEnd.getDate() - 1)
191+
const previousPeriodEndStr = previousPeriodEnd.toISOString()
192+
193+
const previousPeriodStart = new Date(previousPeriodEnd)
194+
previousPeriodStart.setDate(previousPeriodEnd.getDate() - daysRange)
195+
const previousPeriodStartStr = previousPeriodStart.toISOString()
196+
197+
// Get categories for current period
198+
const currentPeriodCategories = await db
199+
.selectFrom('categories')
200+
.select(db.fn.count('id').as('count'))
201+
.where('created_at', '>=', currentPeriodStartStr)
202+
.where('created_at', '<=', todayStr)
203+
.executeTakeFirst()
204+
205+
// Get categories for previous period
206+
const previousPeriodCategories = await db
207+
.selectFrom('categories')
208+
.select(db.fn.count('id').as('count'))
209+
.where('created_at', '>=', previousPeriodStartStr)
210+
.where('created_at', '<=', previousPeriodEndStr)
211+
.executeTakeFirst()
212+
213+
const currentCount = Number(currentPeriodCategories?.count || 0)
214+
const previousCount = Number(previousPeriodCategories?.count || 0)
215+
const difference = currentCount - previousCount
216+
217+
// Calculate percentage change, handling division by zero
218+
const percentageChange = previousCount !== 0
219+
? (difference / previousCount) * 100
220+
: (currentCount > 0 ? 100 : 0)
221+
222+
return {
223+
current_period: currentCount,
224+
previous_period: previousCount,
225+
difference,
226+
percentage_change: percentageChange,
227+
days_range: daysRange,
228+
}
229+
}
230+
231+
/**
232+
* Build category tree structure
233+
*/
234+
export async function fetchCategoryTree(): Promise<any[]> {
235+
// First get all categories
236+
const allCategories = await fetchAll()
237+
238+
// Create a map for quick access
239+
const categoryMap = new Map()
240+
allCategories.forEach(category => {
241+
categoryMap.set(category.id, {
242+
...category,
243+
children: [],
244+
})
245+
})
246+
247+
// Build the tree
248+
const rootCategories: any[] = []
249+
250+
allCategories.forEach(category => {
251+
const categoryWithChildren = categoryMap.get(category.id)
252+
253+
if (category.parent_category_id) {
254+
// This is a child category
255+
const parent = categoryMap.get(category.parent_category_id)
256+
if (parent) {
257+
parent.children.push(categoryWithChildren)
258+
}
259+
} else {
260+
// This is a root category
261+
rootCategories.push(categoryWithChildren)
262+
}
263+
})
264+
265+
// Sort root categories by display_order
266+
rootCategories.sort((a, b) => a.display_order - b.display_order)
267+
268+
// Sort children by display_order
269+
const sortChildrenByDisplayOrder = (categories: any[]) => {
270+
categories.sort((a, b) => a.display_order - b.display_order)
271+
categories.forEach(category => {
272+
if (category.children.length > 0) {
273+
sortChildrenByDisplayOrder(category.children)
274+
}
275+
})
276+
}
277+
278+
sortChildrenByDisplayOrder(rootCategories)
279+
280+
return rootCategories
281+
}

storage/framework/core/commerce/src/types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,36 @@ export interface ReviewStats {
239239
recent_reviews: ReviewJsonResponse[]
240240
}
241241

242+
/**
243+
* Represents comprehensive category statistics
244+
*/
245+
export interface CategoryStats {
246+
/** Total number of categories in the system */
247+
total: number
248+
249+
/** Number of currently active categories */
250+
active: number
251+
252+
/** Number of root categories (no parent) */
253+
root_categories: number
254+
255+
/** Number of child categories (has parent) */
256+
child_categories: number
257+
258+
/** Number of categories with images */
259+
with_images: number
260+
261+
/** Recently added categories */
262+
recently_added: Array<any>
263+
264+
/** Top parent categories with most children */
265+
top_parent_categories: Array<{
266+
id: string
267+
name: string
268+
child_count: number
269+
}>
270+
}
271+
242272
/**
243273
* Options for fetching product manufacturers
244274
*/

0 commit comments

Comments
 (0)