diff --git a/docs/superpowers/plans/2026-03-25-quick-medium-improvements.md b/docs/superpowers/plans/2026-03-25-quick-medium-improvements.md new file mode 100644 index 0000000..34997a4 --- /dev/null +++ b/docs/superpowers/plans/2026-03-25-quick-medium-improvements.md @@ -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 diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index 4ea4611..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useUrlState } from './useUrlState'; diff --git a/src/hooks/useUrlState.ts b/src/hooks/useUrlState.ts deleted file mode 100644 index ad17983..0000000 --- a/src/hooks/useUrlState.ts +++ /dev/null @@ -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} 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; diff --git a/src/services/sallaService.ts b/src/services/sallaService.ts deleted file mode 100644 index edfdeb5..0000000 --- a/src/services/sallaService.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 = {}; - - 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 { - const byStatus: Record = {}; - - orders.forEach(order => { - const status = order.status?.name || 'Unknown'; - byStatus[status] = (byStatus[status] || 0) + 1; - }); - - return byStatus; -}