Dashboard: PeriodPicker replaces year + quarter dropdowns. Defaults to
current month. YoY stat card now compares same range vs previous year.
Comparison: two independent PeriodPicker blocks (Period A and Period B).
Changing Period A auto-updates Period B to same period previous year,
but Period B remains freely editable.
Both pages use filterDataByDateRange; Filters type drops year/quarter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PATCH /api/users/:id route to update user permissions
- Auth session stores and returns allowedMuseums/allowedChannels
- User type gains AllowedMuseums/AllowedChannels (JSON string fields)
- parseAllowed() with fail-closed semantics (empty string → null → no data)
- Dashboard/Comparison apply permission base filter before user filters
- Filter dropdowns (museums, channels, years, districts) derived from
permission-filtered data — restricted users only see their allowed options
- Settings UserRow component with inline checkbox pickers for access config
- Access badges in users table showing current restriction summary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Server checks PIN against env (super admin) + NocoDB Users table
- Session stores name + role (admin/viewer)
- Admin: sees Settings page (seasons + users management)
- Viewer: sees Dashboard + Comparison only, no Settings
- Users CRUD on Settings page: add name + PIN + role, delete
- Settings link + nav hidden for non-admin users
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Server: seasons CRUD routes + generic NocoDB helpers
- Client: Settings page at /settings with inline add/edit/delete
- Seasons stored in NocoDB Seasons table
- Vite proxy: /api/seasons routed to Express server
- Nav links added (desktop + mobile)
- Locale keys for EN + AR
- Seasons loaded non-blocking on app mount
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New MultiSelect component with checkbox dropdown
- Event and channel filters now accept multiple selections
- Empty array = all selected (no filter applied)
- URL params store selections as comma-separated values
- District and quarter remain single-select
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ETL writes District column to NocoDB DailySales
- Museums mapped: Hiraa (Revelation, Holy Quraan, Trail, Makkah, VIP)
AsSaffiyah (Creation Story, Best of Creation)
- District filter added to Dashboard and Comparison (cascades to museum)
- District Performance chart added (desktop + mobile)
- Locale keys added for both EN and AR
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ERP API can't handle concurrent requests — switch from batched
parallel (4 at a time) to sequential fetching.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace NocoDB museum data (Districts/Museums/DailyStats) with ERP API
- Client fetches via server proxy (/api/erp/sales) — no credentials in browser
- Aggregate transaction-level ERP data into daily/museum/channel records
- Replace "district" dimension with "channel" (B2C/HiHala, POS, B2B, etc.)
- Add product-to-museum mapping (46 products → 6 museums)
- NocoDB retained only for PilgrimStats
- Remove old server/index.js (replaced by modular TS in server/src/)
- Update all components, types, and locale files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enable strict: true in tsconfig.json (was false)
- Add proper interfaces for all component props (Dashboard, Comparison, Slides)
- Add SlideConfig, ChartTypeOption, MetricOption types
- Type all function parameters, callbacks, and state variables
- Fix dynamic property access with proper keyof assertions
- 233 type errors resolved across 5 files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Table IDs are now fetched at runtime via the NocoDB meta API using
VITE_NOCODB_BASE_ID, so the same code works against any NocoDB instance
(local or Cloudron). Also adds a migration script for moving data between
instances with correct FK remapping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Production NocoDB uses DistrictId/MuseumId columns instead of
nc_epk____ link columns. Code now checks both column names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CRA (react-scripts 5.0.1) is abandoned and incompatible with TypeScript 5.x.
Vite provides faster builds, active maintenance, and native TS5 support.
- Replace react-scripts with vite + @vitejs/plugin-react
- Move index.html to root with script module entry point
- Replace setupProxy.js with vite.config.ts proxy config
- Rename env vars from REACT_APP_ to VITE_ prefix
- Update tsconfig for bundler module resolution
- Add nginx setup script for deployment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Convert all .js files to .tsx/.ts
- Add types for data structures (MuseumRecord, Metrics, etc.)
- Add type declarations for react-chartjs-2
- Configure tsconfig with relaxed strictness for gradual adoption
- All components now use TypeScript
- Rename Revenue to GrossRevenue, add NetRevenue (excl. VAT)
- Add VAT toggle (Incl/Excl) on Dashboard and Comparison pages
- Add offline mode with localStorage caching (24h validity)
- Add refresh button and offline indicator in nav
- Remove Google Sheets fallback (archived to dataService.legacy.js)
- Add AR/EN translations for new UI elements
- Add official HiHala logo SVG (text portion from v1.5)
- Replace text-based nav brand with logo image + 'Museums Data'
- Update data source to default to NocoDB with Sheets fallback
- Move all credentials to environment variables (.env.local)
- Global data labels toggle in header (works on Dashboard & Comparison pages)
- Labels show formatted values (K/M suffix, max 2 decimals) with white pill background
- Capture Rate chart now shows pilgrims as curved line on right Y-axis
- Revenue Trends toggle moved to top-right corner of chart container
- Mobile: bottom navigation bar with Dashboard, Compare, Labels toggle
- Mobile: top nav simplified to brand only, bottom nav is thumb-friendly