Filament checks view before edit for row URL. Added a shared
ViewRecord base that redirects to edit if canEdit() is true,
so admins and managers get the editable form directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nav entry was fully hidden; now visible only to admins for management.
Delete action added to table rows (bulk delete was already there).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- canView() + canViewAny() in EnforcesRoles: all SMS roles can view
- View pages for all 7 resources so viewers can open records read-only
- Custom ActivityResource restricts activity log to admin + manager only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Import was present but use statement inside class body was missing,
so role checks had no effect on these two resources.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Overriding mount() conflicts with parent void return type; intercepting
the URL in routes/web.php (registered before Filament) is the clean fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
send()+exit breaks in Livewire's mount() context; returning the
redirect response is the correct Livewire v3 approach.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes email/password fields entirely; custom login page shows only
the blue "Mit Blue Spices Zanzibar Cloud anmelden" button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every delivery and batch now writes a signed movement entry instead of
directly incrementing/decrementing spice.quantity. The balance is always
the sum of all movements. Existing data is backfilled in the migration.
Rohstoff edit page shows the full movement history (Lagerbewegungen tab).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows a warning in the confirmation dialog when stock would go below zero.
Deletion proceeds and sets stock to 0 rather than blocking the action.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Products now have a configurable lot_prefix (e.g. "112"). In the batch form
the prefix is shown read-only next to the suffix input (JJMMTT), and the full
lot_number (e.g. "112-260912") is composed automatically and stored.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the recipe is changed in a batch form, keep origin data for spices
that exist in both the old and new recipe. Only add empty rows for
spices that are new to the selected recipe version.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each BatchOrigin (spice) now has multiple BatchOriginEntry rows, replacing the flat
single-supplier/farmer fields. Lieferant and Landwirt are Select fields backed by
dedicated tables with inline create support. Migration preserves existing data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each batch origin row now shows a searchable Rohstoff Select so the same
ingredient can appear in multiple rows with different suppliers or farmers.
Pre-fill from recipe still works (one row per ingredient); user clicks
'Eintrag hinzufügen' to add a second entry for the same ingredient.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Selecting a Produkt auto-sets the current Rezeptur and pre-fills one
Herkunft row per Rohstoff. Changing the Rezeptur resets the rows.
Rohstoff is shown read-only (Hidden + Placeholder); user fills in
supplier, origin country, farmer and notes only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When creating a new batch, the Herkunft-Repeater is automatically populated
with one row per Rohstoff from the product's current recipe. The Rohstoff
field is shown as a read-only label (Hidden spice_id + Placeholder name)
so the user only fills in supplier, origin country, farmer and notes.
Changing the recipe field updates the origin rows accordingly.
On edit, existing origin data is preserved unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Filament table boot lifecycle (bootedInteractsWithTable) fires before
mount(), causing issues with typed $record properties on custom resource
pages. Replaced with a plain Page + Blade view rendering recipes directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bootedInteractsWithTable() calls table() before mount() sets $this->record.
Accessing $this->record eagerly causes a TypeError on the uninitialized typed
property, which Filament/Livewire converts to a 404.
Deferred all $this->record accesses into closures so they only execute at
render time (after mount() has run).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix root cause: recipes_spices.amount_in_percent was decimal(4,2) which
overflows on 100% — widened to decimal(6,2) via migration
- Wrap afterSave() in try-catch so DB/logic errors show as a persistent
danger notification instead of a blank 500 page
- Same for CreateProduct: errors show as notification; also reads
recipe_spices from raw Livewire $this->data (not $data param which
excludes dehydrated(false) fields) so recipes actually save on create
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add "Aus Shopify laden" button on product edit page that pulls description
and variant prices/SKUs/GTINs from Shopify back into SMS
- Add ShopifyService::getProduct() for fetching a single product
- Add 'options' to createProduct() payload so Shopify properly creates variants
(without it Shopify ignores variant option values)
- Add request/response logging to createProduct() for diagnosing transfer issues
- Remove dead code from ProductResource (mutateFormData*, getRecord, getActions)
- Clean up imports in ProductResource
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Filament v3, $arguments is only injected when an action is executed
(clicked), not when it's rendered. Using $arguments in label(), icon(),
color() and visible() closures causes ErrorException: Undefined variable
$arguments on every Livewire update, including on the create page.
Simplified extraItemAction to static label/icon/color; all dynamic
logic (publish vs sync, guard for unsaved items) stays in action().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Products imported before shopify_product_id column was added have null
there, but their variants already carry shopify_variant_id — that's
enough to know the product exists in Shopify. Running shopify:import
once will backfill shopify_product_id for all existing products.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New header action on the product edit page creates the entire product
(title, description, all variants) in Shopify via API. The returned
shopify_product_id and per-variant shopify_variant_ids are stored
locally. After success the page reloads so status badges update.
Button is hidden once the product is already linked to Shopify.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each size row in the product form now shows a Shopify status badge
(green ✓ Shopify / gray Lokal). For saved variants, an action button
appears: 'Zu Shopify' for local-only variants (creates via Shopify
API and stores the returned variant ID), 'Shopify sync' for linked
variants (pushes price/SKU/GTIN to Shopify).
Also fixes:
- gtin missing from ProductSize::$fillable (was silently not saved)
- shopify_product_id added to products table and stored on import
- sizes afterStateHydrated now loads ProductSize directly instead of
via the belongsToMany pivot (gets product_size_id, shopify_variant_id)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
belongsToMany sync() is wrong for products_sizes which is a full model
with its own id and a unique shopify_variant_id. The new approach:
- updateOrCreate per row (safe for repeated saves)
- strips non-digits from gtin, stores null when empty (fixes 500 on
unsignedBigInteger column with empty string in strict SQL mode)
- only deletes local-only sizes (whereNull shopify_variant_id) that
were removed — Shopify-imported sizes are never deleted here
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The recipe name input is dropped from the product form — the version
number alone identifies a recipe. The product title is stored as the
internal name to satisfy the NOT NULL constraint. The history table
no longer shows the name column.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each time the recipe name or spices are changed on the product edit
page, a new version is created instead of overwriting the current one.
Unchanged saves are skipped. A 'Rezept-Versionen' button opens a
sub-page (/produkt/{id}/rezept-versionen) listing all versions with
their spices and timestamps; the active version is highlighted in green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the recipe dropdown with an inline Section containing a name
field and a spice-repeater with percentage inputs. On save, the recipe
is created (version 1) or updated in place; spice pivot rows are synced
via sync(). SumEquals100 rule enforces that percentages add up to 100.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>