diff --git a/CLAUDE.md b/CLAUDE.md index baf0a6857c..7dec9f45e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ When asked to start a new piece of work that isn't already specced, write the sp - **Translations**: 16 locales live under each sub-package's `resources/lang/` (`ar, bg, de, en, es, fa, fr, hr, hu, mn, nl, pl, pt_BR, ro, tr, vi`). When adding or renaming a translation key, update **every** locale — English first, then mirror the key (English value is acceptable as a placeholder) across the other 15. - **Public contract surface**: this package is consumed by downstream apps. Treat anything outside `Models/Concerns/`, `Support/`, and internal namespaces as a contract — breaking changes require a spec and a Rector rule in the `upgrade` package. -- **Migrations**: v2 ships a flat baseline. Schema changes go in a new migration; do not edit the baseline. The `upgrade` package handles v1 → v2 transformations. +- **Migrations**: v2 ships a flat baseline. While v2 is in alpha, fold schema changes into the existing baseline migrations rather than adding change migrations; once v2 is released, schema changes go in new migrations instead. The `upgrade` package handles v1 → v2 transformations. - **Filament**: v5 with the schemas refactor applied. Use schemas, not the deprecated wrapper traits. - **Comments**: describe code in its own vocabulary — use the terms the API actually exposes (a `Fulfilment` is a "fulfilment", not a "parcel"). Don't introduce a metaphor that appears nowhere in the code. Keep comments and docblocks ASCII-only, except the established house style — em dash (`—`), arrow (`→`) in state-transition notes, and ellipsis (`…`). No other non-ASCII symbols (`§`, `Σ`, `⟹`, `≥`, emoji, …); spell them out (`section A`, `sum of`, `implies`, `>=`). This applies to comments, docblocks, and user-facing strings — the `resources/lang/` translation files are the only place non-ASCII text belongs. - **Namespace**: `Lunar\Core\…` for core, `Lunar\Admin\…`, `Lunar\Search\…`, etc. for other sub-packages. diff --git a/TODO.md b/TODO.md index 10ac37d58a..26171797d4 100644 --- a/TODO.md +++ b/TODO.md @@ -9,6 +9,7 @@ Items tagged _(judgement)_ are genuine line-calls worth revisiting. ## Outstanding - Inertia admin panel — new `lunarphp/panel` package: auth, extension points (navigation, slots, table columns, row/bulk/page actions with shared ordering), Customers CRUD, Channels settings (spec 0049) +- Panel order-value chart on the customer edit page + `TimeSeriesChart` on the add-on surface (spec 0050) - Default professional customer notifications for the order lifecycle (spec 0036) _(judgement)_ - Bulk order operations — goal-oriented bulk actions on the orders table (spec 0026) - Line-item refunds — refund specific lines/quantities via a dedicated refund page (spec 0028) diff --git a/packages/core/database/migrations/2026_01_01_000008_create_customers_table.php b/packages/core/database/migrations/2026_01_01_000008_create_customers_table.php index 0965ab2656..3aafff5c18 100644 --- a/packages/core/database/migrations/2026_01_01_000008_create_customers_table.php +++ b/packages/core/database/migrations/2026_01_01_000008_create_customers_table.php @@ -20,6 +20,7 @@ public function up(): void $table->timestamps(); $table->jsonb('attribute_data')->nullable(); $table->string('account_ref')->nullable()->index(); + $table->text('admin_notes')->nullable(); }); } diff --git a/packages/core/src/Actions/Customers/CreateCustomerAddress.php b/packages/core/src/Actions/Customers/CreateCustomerAddress.php index 1c747f36c8..9918fd4fc5 100644 --- a/packages/core/src/Actions/Customers/CreateCustomerAddress.php +++ b/packages/core/src/Actions/Customers/CreateCustomerAddress.php @@ -10,7 +10,17 @@ class CreateCustomerAddress implements CreatesCustomerAddress { public function execute(Customer $customer, array $attributes): Address { - /** @var Address */ - return $customer->addresses()->create($attributes); + /** @var Address $address */ + $address = $customer->addresses()->create($attributes); + + // Explicit timeline entry on the customer, so address changes show + // up in the customer's activity log alongside created/updated. + activity() + ->causedBy(auth()->user()) + ->performedOn($customer) + ->event('address-created') + ->log('address-created'); + + return $address; } } diff --git a/packages/core/src/Actions/Customers/DeleteCustomerAddress.php b/packages/core/src/Actions/Customers/DeleteCustomerAddress.php index d9e6bd19ae..3aa24e0972 100644 --- a/packages/core/src/Actions/Customers/DeleteCustomerAddress.php +++ b/packages/core/src/Actions/Customers/DeleteCustomerAddress.php @@ -9,6 +9,16 @@ class DeleteCustomerAddress implements DeletesCustomerAddress { public function execute(Address $address): void { + $customer = $address->customer; + $address->delete(); + + if ($customer) { + activity() + ->causedBy(auth()->user()) + ->performedOn($customer) + ->event('address-deleted') + ->log('address-deleted'); + } } } diff --git a/packages/core/src/Actions/Customers/LinkCustomerUser.php b/packages/core/src/Actions/Customers/LinkCustomerUser.php index 046417dcfa..4fc74e3095 100644 --- a/packages/core/src/Actions/Customers/LinkCustomerUser.php +++ b/packages/core/src/Actions/Customers/LinkCustomerUser.php @@ -20,5 +20,11 @@ public function execute(Customer $customer, string $email): void $user = $userModel::where('email', $email)->firstOrFail(); $customer->users()->syncWithoutDetaching([$user->getKey()]); + + activity() + ->causedBy(auth()->user()) + ->performedOn($customer) + ->event('user-linked') + ->log('user-linked'); } } diff --git a/packages/core/src/Actions/Customers/UnlinkCustomerUser.php b/packages/core/src/Actions/Customers/UnlinkCustomerUser.php index c117a549ae..c31389e3f4 100644 --- a/packages/core/src/Actions/Customers/UnlinkCustomerUser.php +++ b/packages/core/src/Actions/Customers/UnlinkCustomerUser.php @@ -10,5 +10,11 @@ class UnlinkCustomerUser implements UnlinksCustomerUser public function execute(Customer $customer, int|string $userId): void { $customer->users()->detach($userId); + + activity() + ->causedBy(auth()->user()) + ->performedOn($customer) + ->event('user-unlinked') + ->log('user-unlinked'); } } diff --git a/packages/core/src/Actions/Customers/UpdateCustomer.php b/packages/core/src/Actions/Customers/UpdateCustomer.php index be7e3866bb..e9a34bebe3 100644 --- a/packages/core/src/Actions/Customers/UpdateCustomer.php +++ b/packages/core/src/Actions/Customers/UpdateCustomer.php @@ -6,16 +6,19 @@ use Lunar\Core\Models\Customer; /** - * Update a customer's attributes and sync it to the given customer groups — - * an empty set clears any groups the customer currently belongs to. + * Update a customer's attributes and, when a group set is given, sync it to + * those customer groups — an empty set clears any current groups, while null + * leaves group membership untouched. */ class UpdateCustomer implements UpdatesCustomer { - public function execute(Customer $customer, array $attributes, array $customerGroupIds = []): Customer + public function execute(Customer $customer, array $attributes, ?array $customerGroupIds = null): Customer { $customer->update($attributes); - $customer->customerGroups()->sync($customerGroupIds); + if ($customerGroupIds !== null) { + $customer->customerGroups()->sync($customerGroupIds); + } return $customer; } diff --git a/packages/core/src/Actions/Customers/UpdateCustomerAddress.php b/packages/core/src/Actions/Customers/UpdateCustomerAddress.php index 04c476d44d..0c48bfb070 100644 --- a/packages/core/src/Actions/Customers/UpdateCustomerAddress.php +++ b/packages/core/src/Actions/Customers/UpdateCustomerAddress.php @@ -11,6 +11,14 @@ public function execute(Address $address, array $attributes): Address { $address->update($attributes); + if ($address->customer) { + activity() + ->causedBy(auth()->user()) + ->performedOn($address->customer) + ->event('address-updated') + ->log('address-updated'); + } + return $address; } } diff --git a/packages/core/src/Contracts/Actions/Customers/UpdatesCustomer.php b/packages/core/src/Contracts/Actions/Customers/UpdatesCustomer.php index 5339d11f04..27f8bf6034 100644 --- a/packages/core/src/Contracts/Actions/Customers/UpdatesCustomer.php +++ b/packages/core/src/Contracts/Actions/Customers/UpdatesCustomer.php @@ -8,7 +8,7 @@ interface UpdatesCustomer { /** * @param array $attributes - * @param array $customerGroupIds + * @param ?array $customerGroupIds null leaves group membership untouched; an empty array clears it */ - public function execute(Customer $customer, array $attributes, array $customerGroupIds = []): Customer; + public function execute(Customer $customer, array $attributes, ?array $customerGroupIds = null): Customer; } diff --git a/packages/core/src/Models/Customer.php b/packages/core/src/Models/Customer.php index 071f4276d3..76fea4e54e 100644 --- a/packages/core/src/Models/Customer.php +++ b/packages/core/src/Models/Customer.php @@ -26,6 +26,7 @@ * @property ?string $company_name * @property ?string $tax_identifier * @property ?string $account_ref + * @property ?string $admin_notes * @property ?array $attribute_data * @property ?array $meta * @property ?Carbon $created_at diff --git a/packages/panel-addon-example/resources/js/pages/Widgets/Index.vue b/packages/panel-addon-example/resources/js/pages/Widgets/Index.vue index 07a86434b0..2e81bdcb25 100644 --- a/packages/panel-addon-example/resources/js/pages/Widgets/Index.vue +++ b/packages/panel-addon-example/resources/js/pages/Widgets/Index.vue @@ -12,7 +12,7 @@ import { computed } from 'vue'; // under `example-addon::{group}` keys — served, cached and versioned by the // panel's translations endpoint like the panel's own strings. import { useI18n } from 'vue-i18n'; -import { Breadcrumbs, DataTable, PageHeader, PageZone, Button, SideCard, StatusBadge } from '@lunarphp/panel'; +import { Breadcrumbs, DataTable, FlashMessage, PageHeader, PageZone, Button, SideCard, StatusBadge, TimeSeriesChart } from '@lunarphp/panel'; defineProps<{ message?: string; @@ -43,6 +43,16 @@ const columns = [ const rowActions = [ { key: 'ping', label: 'Ping', icon: 'refresh', method: 'get', primary: false }, ]; + +// Static demo series for the panel's TimeSeriesChart, also on the public surface. +const chartPoints = [ + { label: 'Jan', value: 120, display: '£120.00' }, + { label: 'Feb', value: 210, display: '£210.00' }, + { label: 'Mar', value: 160, display: '£160.00' }, + { label: 'Apr', value: 290, display: '£290.00' }, + { label: 'May', value: 240, display: '£240.00' }, + { label: 'Jun', value: 330, display: '£330.00' }, +];