Remove unused useUrlState hook and sallaService
Both were implemented but never imported by any component. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# Dashboard Quick & Medium Improvements
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Improve reliability, performance, and code quality of the HiHala Dashboard.
|
||||
|
||||
**Architecture:** Focused improvements across data layer (timeout, retry), UI (error handling, loading skeletons, code splitting), config (VAT rate), and DX (TypeScript strict, dead code removal).
|
||||
|
||||
**Tech Stack:** React 19, Vite 7, TypeScript 5, Chart.js
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Fetch Timeout + Retry Logic
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/services/dataService.ts`
|
||||
|
||||
- [ ] Add `fetchWithTimeout` wrapper (10s timeout) around all fetch calls
|
||||
- [ ] Add retry with exponential backoff (3 attempts, 1s/2s/4s) to `fetchNocoDBTable` and `discoverTableIds`
|
||||
- [ ] Commit
|
||||
|
||||
### Task 2: Friendly Error Handling
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/App.tsx` (error display)
|
||||
- Modify: `src/services/dataService.ts` (error classification)
|
||||
|
||||
- [ ] Add error classification in dataService (network, auth, config, unknown)
|
||||
- [ ] Replace raw error message in App.tsx with user-friendly messages using i18n keys
|
||||
- [ ] Add error keys to `src/locales/en.json` and `src/locales/ar.json`
|
||||
- [ ] Commit
|
||||
|
||||
### Task 3: Remove Dead Code
|
||||
|
||||
**Files:**
|
||||
- Delete: `src/hooks/useUrlState.ts`
|
||||
- Delete: `src/services/sallaService.ts`
|
||||
|
||||
- [ ] Delete unused files
|
||||
- [ ] Verify no imports reference them
|
||||
- [ ] Commit
|
||||
|
||||
### Task 4: Route-Based Code Splitting
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/App.tsx`
|
||||
|
||||
- [ ] Lazy-load Dashboard, Comparison, Slides with `React.lazy` + `Suspense`
|
||||
- [ ] Commit
|
||||
|
||||
### Task 5: Loading Skeletons
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/shared/LoadingSkeleton.tsx`
|
||||
- Modify: `src/App.tsx` (replace spinner with skeleton)
|
||||
- Modify: `src/App.css` (skeleton styles)
|
||||
|
||||
- [ ] Create skeleton component (stat cards + chart placeholders)
|
||||
- [ ] Use as Suspense fallback and initial loading state
|
||||
- [ ] Commit
|
||||
|
||||
### Task 6: VAT Rate from Config
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/services/dataService.ts`
|
||||
|
||||
- [ ] Extract VAT_RATE to a named constant at top of file
|
||||
- [ ] Commit
|
||||
|
||||
### Task 7: TypeScript Strict Mode
|
||||
|
||||
**Files:**
|
||||
- Modify: `tsconfig.json`
|
||||
- Modify: various files as needed to fix type errors
|
||||
|
||||
- [ ] Enable `strict: true`, `noImplicitAny: true`, `strictNullChecks: true`
|
||||
- [ ] Fix all resulting type errors
|
||||
- [ ] Verify build passes
|
||||
- [ ] Commit
|
||||
@@ -1 +0,0 @@
|
||||
export { useUrlState } from './useUrlState';
|
||||
@@ -1,58 +0,0 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Sync state with URL search params
|
||||
* @param {Object} state - Current state object
|
||||
* @param {Function} setState - State setter function
|
||||
* @param {Object} defaultState - Default state values
|
||||
* @param {Array<string>} keys - Keys to sync with URL
|
||||
*/
|
||||
export function useUrlState(state, setState, defaultState, keys) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// Initialize state from URL on mount
|
||||
useEffect(() => {
|
||||
const urlState = {};
|
||||
let hasUrlParams = false;
|
||||
|
||||
keys.forEach(key => {
|
||||
const value = searchParams.get(key);
|
||||
if (value !== null) {
|
||||
urlState[key] = value;
|
||||
hasUrlParams = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasUrlParams) {
|
||||
setState(prev => ({ ...prev, ...urlState }));
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Update URL when state changes
|
||||
const updateUrl = useCallback((newState) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
keys.forEach(key => {
|
||||
const value = newState[key];
|
||||
if (value && value !== defaultState[key]) {
|
||||
params.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
setSearchParams(params, { replace: true });
|
||||
}, [keys, defaultState, setSearchParams]);
|
||||
|
||||
// Wrap setState to also update URL
|
||||
const setStateWithUrl = useCallback((updater) => {
|
||||
setState(prev => {
|
||||
const newState = typeof updater === 'function' ? updater(prev) : updater;
|
||||
updateUrl(newState);
|
||||
return newState;
|
||||
});
|
||||
}, [setState, updateUrl]);
|
||||
|
||||
return setStateWithUrl;
|
||||
}
|
||||
|
||||
export default useUrlState;
|
||||
@@ -1,161 +0,0 @@
|
||||
// Salla Integration Service
|
||||
// Connects to the local Salla backend server
|
||||
|
||||
const SALLA_SERVER_URL = import.meta.env.VITE_SALLA_SERVER_URL || 'http://localhost:3001';
|
||||
|
||||
export interface SallaAuthStatus {
|
||||
connected: boolean;
|
||||
hasRefreshToken: boolean;
|
||||
}
|
||||
|
||||
export interface SallaOrder {
|
||||
id: number;
|
||||
reference_id: string;
|
||||
status: {
|
||||
id: string;
|
||||
name: string;
|
||||
customized: { id: string; name: string };
|
||||
};
|
||||
amounts: {
|
||||
total: { amount: number; currency: string };
|
||||
sub_total: { amount: number; currency: string };
|
||||
};
|
||||
customer: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
};
|
||||
items: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
quantity: number;
|
||||
amounts: { total: { amount: number } };
|
||||
}>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SallaProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
sku: string;
|
||||
price: { amount: number; currency: string };
|
||||
quantity: number;
|
||||
status: string;
|
||||
sold_quantity: number;
|
||||
}
|
||||
|
||||
export interface SallaSummary {
|
||||
orders: { total: number; recent: number };
|
||||
products: { total: number };
|
||||
revenue: { total: number; average_order: number; currency: string };
|
||||
}
|
||||
|
||||
export interface SallaStore {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
domain: string;
|
||||
plan: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API Functions
|
||||
// ============================================
|
||||
|
||||
export async function checkSallaAuth(): Promise<SallaAuthStatus> {
|
||||
try {
|
||||
const response = await fetch(`${SALLA_SERVER_URL}/auth/status`);
|
||||
return response.json();
|
||||
} catch (err) {
|
||||
return { connected: false, hasRefreshToken: false };
|
||||
}
|
||||
}
|
||||
|
||||
export function getSallaLoginUrl(): string {
|
||||
return `${SALLA_SERVER_URL}/auth/login`;
|
||||
}
|
||||
|
||||
export async function getSallaStore(): Promise<SallaStore | null> {
|
||||
try {
|
||||
const response = await fetch(`${SALLA_SERVER_URL}/api/store`);
|
||||
if (!response.ok) throw new Error('Failed to fetch store');
|
||||
const data = await response.json();
|
||||
return data.data;
|
||||
} catch (err) {
|
||||
console.error('Error fetching store:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSallaOrders(page = 1, perPage = 50): Promise<{ data: SallaOrder[]; pagination: any }> {
|
||||
try {
|
||||
const response = await fetch(`${SALLA_SERVER_URL}/api/orders?page=${page}&per_page=${perPage}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch orders');
|
||||
return response.json();
|
||||
} catch (err) {
|
||||
console.error('Error fetching orders:', err);
|
||||
return { data: [], pagination: {} };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSallaProducts(page = 1, perPage = 50): Promise<{ data: SallaProduct[]; pagination: any }> {
|
||||
try {
|
||||
const response = await fetch(`${SALLA_SERVER_URL}/api/products?page=${page}&per_page=${perPage}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch products');
|
||||
return response.json();
|
||||
} catch (err) {
|
||||
console.error('Error fetching products:', err);
|
||||
return { data: [], pagination: {} };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSallaSummary(): Promise<SallaSummary | null> {
|
||||
try {
|
||||
const response = await fetch(`${SALLA_SERVER_URL}/api/analytics/summary`);
|
||||
if (!response.ok) throw new Error('Failed to fetch summary');
|
||||
return response.json();
|
||||
} catch (err) {
|
||||
console.error('Error fetching summary:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Data Transformation for Dashboard
|
||||
// ============================================
|
||||
|
||||
export function transformOrdersForChart(orders: SallaOrder[]): {
|
||||
labels: string[];
|
||||
datasets: { label: string; data: number[] }[];
|
||||
} {
|
||||
// Group orders by date
|
||||
const byDate: Record<string, number> = {};
|
||||
|
||||
orders.forEach(order => {
|
||||
const date = order.created_at.split('T')[0];
|
||||
byDate[date] = (byDate[date] || 0) + (order.amounts?.total?.amount || 0);
|
||||
});
|
||||
|
||||
const sortedDates = Object.keys(byDate).sort();
|
||||
|
||||
return {
|
||||
labels: sortedDates,
|
||||
datasets: [{
|
||||
label: 'Daily Revenue (SAR)',
|
||||
data: sortedDates.map(d => byDate[d])
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
export function getOrderStatusSummary(orders: SallaOrder[]): Record<string, number> {
|
||||
const byStatus: Record<string, number> = {};
|
||||
|
||||
orders.forEach(order => {
|
||||
const status = order.status?.name || 'Unknown';
|
||||
byStatus[status] = (byStatus[status] || 0) + 1;
|
||||
});
|
||||
|
||||
return byStatus;
|
||||
}
|
||||
Reference in New Issue
Block a user