# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Skills

The following skills are installed at `.claude/skills/` and must be used proactively:

| Skill | Trigger | Notes |
|-------|---------|-------|
| `livewire-development` | Any Livewire component work — `wire:` directives, Livewire page classes, Livewire JS hooks, component state | Project runs **Livewire 3** not 4; use skill for patterns but follow existing Livewire 3 conventions in the codebase |

`starter-kit-upgrade` is installed but not applicable — this project is a fork of AureusERP, not a Laravel starter kit.

## What this is

**Cydekick** is a customised fork of [AureusERP](https://aureuserp.com) — a Laravel 11 + Filament 4 ERP system. The upstream project is a monorepo of Webkul plugins; Cydekick extends and overrides those plugins for a UK automotive parts company (Banwell / Somerset4x4).

## Commands

```bash
# Local development (XAMPP — Apache serves the site directly)
php artisan serve              # only if not using XAMPP Apache
php -d max_execution_time=0 artisan queue:work --queue=high,default   # run jobs locally
npm run dev                    # Vite HMR

# Build
npm run build
php artisan optimize           # after config/route/view changes
php artisan view:clear && php artisan optimize:clear   # bust stale caches

# Migrations
php artisan migrate
php artisan migrate --path="plugins/webkul/<plugin>/database/migrations"

# Code style (Laravel Pint — must pass before commit)
./vendor/bin/pint

# Tests
php artisan test
php artisan test --filter=TestClassName

# Create Filament user
php artisan make:filament-user
```

**Production deploy** (server runs Supervisor):
```bash
git pull && composer install --no-dev --optimize-autoloader
npm ci --production=false && npm run build
php artisan migrate --force
php artisan view:clear && php artisan optimize:clear && php artisan optimize
sudo supervisorctl restart cydekick-worker:* cydekick-scheduler
```

## Architecture

### Plugin system

All business logic lives in `plugins/webkul/<plugin>/`. Each plugin has:
- `src/<Name>ServiceProvider.php` — extends `Webkul\Support\PackageServiceProvider`, declares migrations, views, routes
- `src/<Name>Plugin.php` — Filament panel plugin registration
- `database/migrations/` — plugin-scoped migrations (registered by name in the ServiceProvider, not auto-discovered)
- `resources/views/` — Blade views namespaced as `<plugin>::`

Plugins are loaded via Composer path repositories (see `composer.json`). When adding a migration to an existing plugin, register its filename in the ServiceProvider's `hasMigrations([...])` array.

### Filament structure

The admin panel is a single Filament panel at `/admin`. Resources follow:
```
plugins/webkul/<plugin>/src/Filament/
  Clusters/<ClusterName>/Resources/<Resource>.php
  Clusters/<ClusterName>/Resources/<Resource>/Pages/
  Pages/           ← standalone dashboard pages
  Widgets/
```

**Record sub-navigation tabs** (the tab bar shown on product/order detail pages) use `HasRecordNavigationTabs` trait. To add a tab to an existing resource:
1. Create a `Page` class in `Resources/<Resource>/Pages/`, use `InteractsWithRecord` + `HasRecordNavigationTabs`
2. Add it to `getRecordSubNavigation()` and `getPages()` in the Resource class
3. Set `protected static string $resource = TheResource::class` and `protected string $view = 'plugin::path.to.view'`

### Key plugins

| Plugin | Purpose |
|--------|---------|
| `products` | Base `Product` model, `ProductResource` form/table — extended by inventories |
| `inventories` | Inventory-specific product fields, stock moves, bin racks, warehouses |
| `channels` | Shopify channel sync: `ManageListings` page, SKU mappings, job dispatch |
| `console` | Real-time job status bar (Livewire) + `JobLogger` service |
| `allmakes-psp` | Allmakes PSP scraper: fitment data (`allmakes_psp_fitments`), price checks |
| `support` | Shared traits, `PackageServiceProvider` base, company/permission helpers |
| `channel-orders` | Shopify order sync and display |

### Channels / Shopify sync

`ManageListings` (`plugins/webkul/channels/src/Filament/Pages/ManageListings.php`) is a custom Livewire+Filament page. Key concepts:
- **Tabs**: Listed / Needs Update / Pending Sync / Not Listed / Errors — driven by `$statusFilter` and `getStatusCounts()`
- **`channel_listings`** table stores per-product listing status; `channels_sku_mappings` stores Shopify variant links
- **Jobs**: `SyncProductToChannels` (update existing), `CreateProductInShopify` (new listing) — both use `JobLogger` for console feedback
- **Cancel sync**: `cancelPendingSync()` deletes unreserved jobs from `jobs` table + resets `pending_sync` DB rows
- **`not_synced` filter** includes statuses: `not_listed`, `pending_sync`, and `NULL`

### Console / job logging

`plugins/webkul/console/` provides a persistent job activity log:
- `JobLogger::running($name)` → creates a `job_activity_logs` row, returns `$logId`
- `JobLogger::ok($name, $logId)` / `::error(...)` → closes the log entry
- `JobStatusBar` Livewire component polls every 3 s and displays the active job in the top nav
- Jobs that call `JobLogger` explicitly must be added to `$skipClasses` in `ConsoleServiceProvider` to prevent double-logging
- Entries stuck as `running` for >10 min are auto-closed as `error — timed out`

### Database conventions

- All plugin tables are prefixed: `products_products`, `channels_sku_mappings`, `allmakes_psp_fitments`, etc.
- **Company gating**: Products and most records have `company_id` FK → `companies` table. Somerset4x4 = `company_id = 2`.
- Queue uses `QUEUE_CONNECTION=database` — jobs are in the `jobs` table with `reserved_at` column
- JSON columns (e.g. `images`, `vehicle_fitment`) are cast as `'array'` in models and stored as MySQL JSON

### Overriding upstream behaviour

- To hide a feature: set `protected static bool $shouldRegisterNavigation = false;` — log it in `z_notes/hidden_removed_features.md`
- Plugin models extend upstream models (e.g. `Webkul\Inventory\Models\Product` extends `Webkul\Product\Models\Product`) — always check both layers when tracing model behaviour
- Image uploads use Cloudflare R2 when configured in Settings → R2 Storage; falls back to `public` disk locally
