No stock validation existed in the ordering flow, allowing customers
to order more items than available.
Cap quantity to ``stock_current`` in ``Baskets::getBasketData()`` when
adding to cart. Add ``min=1`` and ``max=stock`` attributes on the
basket quantity input, with JS clamping in the change handler.
Verify stock again in ``Shop\OrderController::store()`` before saving
the order as a race-condition safeguard.
Fix ``AlerteStock`` to use ``name`` instead of non-existent ``title``
field on articles, so the article name actually appears in the email.
Add ``lien_article`` variable with a direct link to the admin article
edit page. Update the DB template with a button and text link.
When changing an order status to « Annulé », check if the related
invoice has any validated payments via ``Invoices::getPayments()``.
If the total paid is greater than zero, the cancellation is refused
with a growl error showing the amount already paid.
Add ``getStatusBadge()`` to ``Orders`` returning Bootstrap badge HTML
per status: warning (En attente), info (Préparation), primary
(Expédié), success (Livré), danger (Annulé). Applied to all four
order DataTables (admin, admin customer, shop, shop customer).
When a Paybox callback confirms payment on an order with status 4
(Annulé), the payment is still recorded but the order status is no
longer forced to « Préparation ». Instead, an alert email is sent to
``commande@jardinenvie.com`` warning that a refund is likely needed.
New ``AlertePaiementAnnule`` mailable with DB template providing order
ref, amount, customer info and payment reference. New method
``OrderMails::sendCancelledOrderPaymentAlert()`` handles the dispatch.
Add ``restoreStock()``, ``decreaseStockForOrder()``, and
``checkStockForOrder()`` to ``OfferStocks``. When an order is cancelled
(status 4), stock is restored. When un-cancelling, stock availability is
checked first—insufficient stock blocks the transition with an error
message—then decremented.
Add null check on ``$order->customer`` in ``OrdersDataTable`` to display
"Client supprimé" instead of crashing when the related customer record
no longer exists.
The ``scopeNotCancelled`` scope on ``Order`` filters out orders with
status 4 (Annulé). All ``OrderMetrics`` methods now chain
``notCancelled()`` so that dashboard counts and totals ignore cancelled
orders.
When a purchase causes an offer's ``stock_current`` to drop to or below
its ``minimum_ondemand`` threshold, an email is sent to
``commande@jardinenvie.com`` using an editable mail template (Spatie
``MailTemplate``).
The check runs in ``OfferStocks::decreaseStock()`` after updating stock.
Only threshold-crossing events trigger the alert (not every low-stock
sale). Failures are caught and logged to avoid disrupting the order
flow.
Add ``'Annulé'`` at index 4 in ``Orders::statuses()``, allowing
administrators to mark orders as cancelled from the admin edit page.
This is a label-only change; side-effects (dashboard stats, stock
restoration, Paybox guard) are documented in ``admin.org`` for
client review.
Move the delivery mode and delivery address sections before the
billing address in ``registered.blade.php``. The new order is:
Mode de livraison → Adresse de livraison → Adresse de facturation →
Paiement, which better matches the natural checkout flow.
Two issues prevented the shelf tree reordering from working:
- The JS used ``onDragStop`` (only fires for drags outside the
tree) instead of the ``tree.move`` event to send the AJAX
request. Moved the POST into the ``tree.move`` handler.
- The ``inside`` case used ``appendNode`` (last child), but
jqTree sends ``inside`` when dropping before the first child.
Switched to ``prependNode`` so the node lands first.
- Added missing ``before`` case with ``insertBeforeNode``.
Reusable ``password_toggle.blade.php`` partial that wraps every
``input[type=password]`` with an eye icon button. Clicking it
toggles between hidden and visible text. Handles Bootstrap modals
via ``shown.bs.modal`` event. Applied on login, register, password
change (shop + admin), password reset, and first login pages.
The edit view used ``id='content-form'`` while the shared
``form.blade.php`` calls ``initSaveForm('#homepage-form')``.
The jQuery selector never found the form, so clicking Save
did nothing. Aligned the edit form ID to ``homepage-form``.
The ``latestOrders`` partial accessed ``$order->customer->id``
without checking for null. Orders whose customer has been deleted
caused the admin dashboard to crash on load.
The ``basketTotal`` partial expects ``$sale_channel`` as a top-level
view variable. The cart page passed it correctly, but the checkout
page and the AJAX basket refresh only passed it nested inside
``$basket``.
Replaces the simple address radio list in the checkout page with the
richer ``Shop.Customers.partials.addresses`` partial already used on
the profile edit page. Customers can now choose among existing
addresses, add a new one on-the-fly, or delete an address directly
from the checkout flow.
The logo ``src`` was ``/storage/photos/shares/logo.png`` — a relative
path to a file that doesn't exist. Email clients cannot resolve
relative URLs. Replaces with the absolute URL to the actual logo at
``https://boutique.jardinenvie.com/img/logo.png`` across all 4 mail
templates.
Each rule (length, lowercase, uppercase, number, special character)
shows a live check/cross icon as the user types. Also aligns
``handlePasswordChange`` server-side validation with the boilerplate
``Password`` rule (was only enforcing min 8 chars).
The mobile category menu (``#navbarContentMobile``) could not scroll
when its content exceeded the viewport height, because no overflow
or height constraint was set on the collapse container.
Add ``max-height: calc(100vh - 60px)``, ``overflow-y: auto`` and
``-webkit-overflow-scrolling: touch`` to allow touch-scrolling
through the full category list on mobile devices.
- Replace ``<br>`` with ``\n`` in ``InvoicePDF::makeAddress()`` so
dompdf renders line breaks instead of showing raw HTML tags.
- Translate ``order number`` to ``Numéro de commande`` in the custom
fields passed to the invoice builder.
The amount-in-words was already in English because the container
lacked French ICU data (``icu-data-full``); that was fixed at
runtime, not in code.
The password change form on the profile page (``Mes coordonnées``)
was scaffolded but never wired to any backend logic. The fields
``current-password``, ``new-password`` and ``new-password_confirmation``
were silently ignored by ``Customers::storeFull()``.
- Add ``handlePasswordChange()`` in ``CustomerController`` that
validates current password, confirmation match, and 8-char minimum
before hashing and saving.
- Remove ``required`` attribute from password fields so the form can
submit for profile-only updates without filling password fields.
- Strip password fields from request data before passing to
``storeFull()`` to avoid Eloquent mass-assignment noise.
The email template had "Carte de crédit" hardcoded regardless of the
actual payment method. The address blocks were also missing the
``address2`` and ``name`` fields.
- Add ``mode_paiement``, ``livraison_nom``, ``facturation_nom``,
``livraison_adresse2``, ``facturation_adresse2`` to
``ConfirmationCommande`` Mailable
- Migration to replace hardcoded payment label with
``{{mode_paiement}}`` and add ``address2`` fields in DB template
- Migration to add ``name`` fields before each address block
When an order has status 0 ("En attente") and payment type is check
or wire transfer, the customer-facing order list now displays
"En attente de règlement" instead of the generic "En attente".
Display a tailored confirmation message when the customer pays by
check or wire transfer, including a warning about the 30-day
cancellation policy. The payment type is passed as a query parameter
so the message survives page reloads.
- Add ``getOrderConfirmedByCheckContent()`` and
``getOrderConfirmedByWireContent()`` to ``Contents`` repository
- Flash ``payment_type`` through redirect query parameter
- Add migration inserting content rows (id 10, 11)
- Update confirmed view with green checkmark and warning icon
``getArticlesToSell()`` returned ``false`` when no articles matched,
causing ``collect(false)`` to produce ``[false]``. The view then
iterated over that single-element array and tried to access array
offsets on a boolean value.