Add sales tracking system with customers, analytics, and inventory management
All checks were successful
Deploy / deploy (push) Successful in 2m26s
All checks were successful
Deploy / deploy (push) Successful in 2m26s
- Add customers table (021) and sales/sale_items tables (022) migrations - Add customer CRUD, sale CRUD (transactional with auto inventory decrement/restore), and analytics API endpoints (overview, top sellers, revenue chart, inventory alerts) - Add sales page with NewSaleModal (customer autocomplete, multi-item form, color-based pricing, stock validation) and SaleDetailModal - Add customers page with search, inline editing, and purchase history - Add analytics dashboard with recharts (revenue line chart, top sellers bar, refill vs spulna pie chart, inventory alerts table with stockout estimates) - Add customerService, saleService, analyticsService to frontend API layer - Update sidebar navigation on all admin pages
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { Filament } from '@/src/types/filament';
|
||||
import { Customer, Sale, CreateSaleRequest, AnalyticsOverview, TopSeller, RevenueDataPoint, InventoryAlert, TypeBreakdown } from '@/src/types/sales';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api';
|
||||
|
||||
@@ -132,4 +133,79 @@ export const colorRequestService = {
|
||||
},
|
||||
};
|
||||
|
||||
export const customerService = {
|
||||
getAll: async (): Promise<Customer[]> => {
|
||||
const response = await api.get('/customers');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
search: async (q: string): Promise<Customer[]> => {
|
||||
const response = await api.get(`/customers/search?q=${encodeURIComponent(q)}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Customer & { sales: Sale[] }> => {
|
||||
const response = await api.get(`/customers/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (customer: Partial<Customer>): Promise<Customer> => {
|
||||
const response = await api.post('/customers', customer);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, customer: Partial<Customer>): Promise<Customer> => {
|
||||
const response = await api.put(`/customers/${id}`, customer);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const saleService = {
|
||||
getAll: async (page = 1, limit = 50): Promise<{ sales: Sale[]; total: number }> => {
|
||||
const response = await api.get(`/sales?page=${page}&limit=${limit}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Sale> => {
|
||||
const response = await api.get(`/sales/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: CreateSaleRequest): Promise<Sale> => {
|
||||
const response = await api.post('/sales', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`/sales/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const analyticsService = {
|
||||
getOverview: async (period = '30d'): Promise<AnalyticsOverview> => {
|
||||
const response = await api.get(`/analytics/overview?period=${period}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTopSellers: async (period = '30d'): Promise<TopSeller[]> => {
|
||||
const response = await api.get(`/analytics/top-sellers?period=${period}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getInventoryAlerts: async (): Promise<InventoryAlert[]> => {
|
||||
const response = await api.get('/analytics/inventory-alerts');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getRevenueChart: async (period = '6m', group = 'month'): Promise<RevenueDataPoint[]> => {
|
||||
const response = await api.get(`/analytics/revenue-chart?period=${period}&group=${group}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTypeBreakdown: async (period = '30d'): Promise<TypeBreakdown[]> => {
|
||||
const response = await api.get(`/analytics/type-breakdown?period=${period}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
91
src/types/sales.ts
Normal file
91
src/types/sales.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export interface Customer {
|
||||
id: string;
|
||||
name: string;
|
||||
phone?: string;
|
||||
city?: string;
|
||||
notes?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface SaleItem {
|
||||
id: string;
|
||||
sale_id: string;
|
||||
filament_id: string;
|
||||
item_type: 'refill' | 'spulna';
|
||||
quantity: number;
|
||||
unit_price: number;
|
||||
created_at?: string;
|
||||
// Joined fields
|
||||
filament_tip?: string;
|
||||
filament_finish?: string;
|
||||
filament_boja?: string;
|
||||
}
|
||||
|
||||
export interface Sale {
|
||||
id: string;
|
||||
customer_id?: string;
|
||||
total_amount: number;
|
||||
notes?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
// Joined fields
|
||||
customer_name?: string;
|
||||
customer_phone?: string;
|
||||
items?: SaleItem[];
|
||||
item_count?: number;
|
||||
}
|
||||
|
||||
export interface CreateSaleRequest {
|
||||
customer: {
|
||||
name: string;
|
||||
phone?: string;
|
||||
city?: string;
|
||||
notes?: string;
|
||||
};
|
||||
items: {
|
||||
filament_id: string;
|
||||
item_type: 'refill' | 'spulna';
|
||||
quantity: number;
|
||||
}[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsOverview {
|
||||
revenue: number;
|
||||
sales_count: number;
|
||||
avg_order_value: number;
|
||||
unique_customers: number;
|
||||
}
|
||||
|
||||
export interface TopSeller {
|
||||
boja: string;
|
||||
tip: string;
|
||||
finish: string;
|
||||
total_qty: number;
|
||||
total_revenue: number;
|
||||
}
|
||||
|
||||
export interface RevenueDataPoint {
|
||||
period: string;
|
||||
revenue: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface InventoryAlert {
|
||||
id: string;
|
||||
boja: string;
|
||||
tip: string;
|
||||
finish: string;
|
||||
refill: number;
|
||||
spulna: number;
|
||||
kolicina: number;
|
||||
avg_daily_sales: number;
|
||||
days_until_stockout: number | null;
|
||||
}
|
||||
|
||||
export interface TypeBreakdown {
|
||||
item_type: string;
|
||||
total_qty: number;
|
||||
total_revenue: number;
|
||||
}
|
||||
Reference in New Issue
Block a user