32 Commits

Author SHA1 Message Date
Valentin Lab
43993f587c chg: responsive fixes for homepage text, scroll overflow and menu font on small screens (< 430px) 2026-03-27 14:14:25 +01:00
Valentin Lab
de13dee166 fix: add ellipsis on long article labels to prevent overflow on neighbor thumbnails 2026-03-27 13:56:25 +01:00
Valentin Lab
64af20e10a chg: reduce spacing between article thumbnails on small screens (< 430px) 2026-03-27 13:55:37 +01:00
Valentin Lab
2184f1e83c chg: reduce article label font size under thumbnails on small screens (< 430px) 2026-03-27 13:54:58 +01:00
Valentin Lab
8d94c038ad chg: reduce discover button size and hide suffix on small screens (< 430px) 2026-03-27 13:36:36 +01:00
Valentin Lab
f27a7ef8e6 chg: reduce shelve title font size on small screens (< 430px) 2026-03-27 13:12:14 +01:00
Valentin Lab
6e46485d53 fix: show article description on merchandise product pages 2026-03-27 13:12:04 +01:00
Valentin Lab
31815cd618 fix: replace hardcoded nature keys with dynamic foreach in article add-to-basket 2026-03-27 13:11:56 +01:00
Valentin Lab
5947ee256a fix: allow merchandise articles to appear in category pages 2026-03-27 13:11:41 +01:00
Valentin Lab
93f027f815 new: add filtered articles link from category edit form 2026-03-27 13:11:32 +01:00
Valentin Lab
3bfbd629bf fix: exclude invisible articles from category menu visibility 2026-03-27 13:11:22 +01:00
Valentin Lab
493743307a new: add visibility badge for articles in offer form select 2026-03-16 16:44:48 +01:00
Valentin Lab
d8f95c667c new: add warning icon for tariffs without price list in offer form select 2026-03-16 16:41:17 +01:00
Valentin Lab
2563398df2 new: add colored status badges for tariffs in list and offer form select 2026-03-16 16:35:24 +01:00
Valentin Lab
39572c9ea2 new: show active/inactive status toggle on offer edit form 2026-03-16 16:24:44 +01:00
Valentin Lab
55051334ef new: add quick edit links for article, package and tariff on offer form 2026-03-16 16:23:43 +01:00
Valentin Lab
63673117b3 fix: enforce stock limits on basket quantities
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.
2026-02-20 13:05:31 +01:00
Valentin Lab
ef52addc7d fix: show article name and admin link in stock alert email
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.
2026-02-20 13:05:31 +01:00
Valentin Lab
94af725373 new: block order cancellation when invoice has payments
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.
2026-02-20 11:59:45 +01:00
Valentin Lab
fa4aea7358 new: add colored status badges in order lists
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).
2026-02-20 11:55:33 +01:00
Valentin Lab
5325fa1f06 new: send alert email when Paybox payment arrives on cancelled order
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.
2026-02-20 11:52:03 +01:00
Valentin Lab
5c10645af7 new: restore and manage stock on order cancellation
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.
2026-02-20 11:38:21 +01:00
Valentin Lab
7e9c3c6196 fix: prevent error on admin orders list when customer is deleted
Add null check on ``$order->customer`` in ``OrdersDataTable`` to display
"Client supprimé" instead of crashing when the related customer record
no longer exists.
2026-02-20 11:38:13 +01:00
Valentin Lab
e4540f9d88 fix: exclude cancelled orders from dashboard statistics
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.
2026-02-20 11:38:06 +01:00
Valentin Lab
936c9473a7 new: send stock alert email when offer stock crosses threshold
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.
2026-02-13 07:13:56 +01:00
Valentin Lab
cbd8e33f3b new: add "Annulé" status for orders
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.
2026-02-13 06:48:08 +01:00
Valentin Lab
701e424185 chg: reorder checkout sections to show delivery address before billing
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.
2026-02-13 06:46:12 +01:00
Valentin Lab
cd5d72e272 fix: make drag-and-drop on category tree persist correctly
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``.
2026-02-13 06:46:12 +01:00
Valentin Lab
7a246a189a new: add password visibility toggle on all password fields
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.
2026-02-13 06:46:12 +01:00
Valentin Lab
f8a5caec60 chg: dev: update project docs
Add remote server access rules to ``AGENTS.md``. Update workflow
doc with clarification step and fix numbering.
2026-02-13 06:46:05 +01:00
Valentin Lab
9903579b98 fix: correct form ID mismatch preventing admin content saving
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``.
2026-02-13 04:07:37 +01:00
Valentin Lab
552b823b8b fix: prevent error 500 on admin dashboard when order has no customer
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.
2026-02-13 03:54:40 +01:00
47 changed files with 1036 additions and 185 deletions

View File

@@ -52,19 +52,23 @@ migrations or service integrations, include coverage that exercises
failure paths. For granular checks, `./vendor/bin/phpunit --filter failure paths. For granular checks, `./vendor/bin/phpunit --filter
FooTest` is acceptable, but always run the full suite before pushing. FooTest` is acceptable, but always run the full suite before pushing.
## Commit & Pull Request Guidelines ## Pull Request Guidelines
Commits in this repo mix Conventional Commit prefixes (`new:`, `fix:`,
`chg:`); `fix: prevent null totals`. Keep messages in the imperative
mood and reference ticket IDs when available.
Pull requests must describe scope, list schema or configuration Pull requests must describe scope, list schema or configuration
changes, and note any manual follow-up (cron, storage links, changes, and note any manual follow-up (cron, storage links, queues).
queues).
Attach screenshots or terminal logs when touching UI or console Attach screenshots or terminal logs when touching UI or console
output, and ensure CI scripts (when available) pass. output, and ensure CI scripts (when available) pass.
## Remote Server Access
- **BLOCKING**: NEVER execute state-changing commands on remote
servers without explicit per-command user approval. Only READ
operations are allowed by default (logs, status, select queries,
configuration inspection). Write operations include but are not
limited to: `migrate`, `seed`, write queries, file modifications,
service restarts, deployments. No exceptions.
## Environment & Security Notes ## Environment & Security Notes
Copy `.env.example` to `.env` and run `php artisan key:generate` Copy `.env.example` to `.env` and run `php artisan key:generate`

View File

@@ -46,12 +46,12 @@ class CustomerOrdersDataTable extends DataTable
{ {
$datatables $datatables
->editColumn('status', function (Order $order) { ->editColumn('status', function (Order $order) {
return Orders::getStatus($order->status); return Orders::getStatusBadge($order->status);
}) })
->editColumn('created_at', function (Order $order) { ->editColumn('created_at', function (Order $order) {
return $order->created_at->isoFormat('DD/MM/YY HH:mm'); return $order->created_at->isoFormat('DD/MM/YY HH:mm');
}) })
->rawColumns(['action']); ->rawColumns(['status', 'action']);
return parent::modifier($datatables); return parent::modifier($datatables);
} }

View File

@@ -29,18 +29,20 @@ class OrdersDataTable extends DataTable
{ {
$datatables $datatables
->editColumn('status', function (Order $order) { ->editColumn('status', function (Order $order) {
return Orders::getStatus($order->status); return Orders::getStatusBadge($order->status);
}) })
->editColumn('created_at', function (Order $order) { ->editColumn('created_at', function (Order $order) {
return $order->created_at->format('d/m/Y H:i:s'); return $order->created_at->format('d/m/Y H:i:s');
}) })
->editColumn('customer.last_name', function (Order $order) { ->editColumn('customer.last_name', function (Order $order) {
return $order->customer->last_name.' '.$order->customer->first_name; return $order->customer
? $order->customer->last_name.' '.$order->customer->first_name
: 'Client supprimé';
}) })
->editColumn('payment_type', function (Order $order) { ->editColumn('payment_type', function (Order $order) {
return InvoicePayments::getPaymentType($order->payment_type); return InvoicePayments::getPaymentType($order->payment_type);
}) })
->rawColumns(['action']); ->rawColumns(['status', 'action']);
return parent::modifier($datatables); return parent::modifier($datatables);
} }

View File

@@ -22,7 +22,7 @@ class TariffsDataTable extends DataTable
{ {
$datatables $datatables
->editColumn('status', function (Tariff $tariff) { ->editColumn('status', function (Tariff $tariff) {
return Tariffs::getStatus($tariff['status_id']); return Tariffs::getStatusBadge($tariff['status_id']);
}) })
->editColumn('sale_channels2', function (Tariff $tariff) { ->editColumn('sale_channels2', function (Tariff $tariff) {
$html = ''; $html = '';
@@ -32,7 +32,7 @@ class TariffsDataTable extends DataTable
return $html; return $html;
}) })
->rawColumns(['sale_channels2', 'action']); ->rawColumns(['status', 'sale_channels2', 'action']);
return parent::modifier($datatables); return parent::modifier($datatables);
} }

View File

@@ -48,15 +48,15 @@ class CustomerOrdersDataTable extends DataTable
$datatables $datatables
->editColumn('status', function (Order $order) { ->editColumn('status', function (Order $order) {
if ($order->status == 0 && in_array($order->payment_type, [2, 3])) { if ($order->status == 0 && in_array($order->payment_type, [2, 3])) {
return 'En attente de règlement'; return '<span class="badge badge-warning">En attente de règlement</span>';
} }
return Orders::getStatus($order->status); return Orders::getStatusBadge($order->status);
}) })
->editColumn('created_at', function (Order $order) { ->editColumn('created_at', function (Order $order) {
return $order->created_at->isoFormat('DD/MM/YY HH:mm'); return $order->created_at->isoFormat('DD/MM/YY HH:mm');
}) })
->rawColumns(['action']); ->rawColumns(['status', 'action']);
return parent::modifier($datatables); return parent::modifier($datatables);
} }

View File

@@ -29,7 +29,7 @@ class OrdersDataTable extends DataTable
{ {
$datatables $datatables
->editColumn('status', function (Order $order) { ->editColumn('status', function (Order $order) {
return Orders::getStatus($order->status); return Orders::getStatusBadge($order->status);
}) })
->editColumn('created_at', function (Order $order) { ->editColumn('created_at', function (Order $order) {
return $order->created_at->toDateTimeString(); return $order->created_at->toDateTimeString();
@@ -40,7 +40,7 @@ class OrdersDataTable extends DataTable
->editColumn('payment_type', function (Order $order) { ->editColumn('payment_type', function (Order $order) {
return InvoicePayments::getPaymentType($order->payment_type); return InvoicePayments::getPaymentType($order->payment_type);
}) })
->rawColumns(['action']); ->rawColumns(['status', 'action']);
return parent::modifier($datatables); return parent::modifier($datatables);
} }

View File

@@ -26,6 +26,7 @@ class ArticleController extends Controller
'article_natures' => ArticleNatures::getOptions(), 'article_natures' => ArticleNatures::getOptions(),
'categories' => Categories::getOptions(), 'categories' => Categories::getOptions(),
'tags' => Tags::getOptionsFullName(), 'tags' => Tags::getOptionsFullName(),
'filters' => request()->only(['article_nature_id', 'category_id', 'tag_id']),
]; ];
return $dataTable->render('Admin.Shop.Articles.list', $data); return $dataTable->render('Admin.Shop.Articles.list', $data);

View File

@@ -4,6 +4,8 @@ namespace App\Http\Controllers\Admin\Shop;
use App\Datatables\Admin\Shop\OrdersDataTable; use App\Datatables\Admin\Shop\OrdersDataTable;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Repositories\Shop\Invoices;
use App\Repositories\Shop\OfferStocks;
use App\Repositories\Shop\OrderMails; use App\Repositories\Shop\OrderMails;
use App\Repositories\Shop\Orders; use App\Repositories\Shop\Orders;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -36,6 +38,43 @@ class OrderController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$previousStatus = null;
if ($request->has('id')) {
$previousStatus = Orders::get($request->input('id'))->status;
}
$newStatus = $request->input('status');
// Interdire l'annulation si la facture a des paiements
if ($previousStatus != 4 && $newStatus == 4 && $request->has('id')) {
$order = Orders::get($request->input('id'), ['invoice']);
if ($order->invoice) {
$totalPaid = Invoices::getPayments($order->invoice->id);
if ($totalPaid > 0) {
return redirect()->back()->withInput()->with(
'growl',
['Impossible d\'annuler cette commande : la facture a déjà été réglée ('.number_format($totalPaid, 2, ',', ' ').' €). Veuillez d\'abord gérer le remboursement.', 'danger']
);
}
}
}
// Vérifier le stock avant de dés-annuler une commande
if ($previousStatus == 4 && $newStatus != 4) {
$insufficients = OfferStocks::checkStockForOrder($request->input('id'));
if (! empty($insufficients)) {
$messages = [];
foreach ($insufficients as $item) {
$messages[] = $item['name'].' (stock : '.$item['stock'].', requis : '.$item['quantity'].')';
}
return redirect()->back()->withInput()->with(
'growl',
['Impossible de réactiver cette commande, stock insuffisant : '.implode(' ; ', $messages), 'danger']
);
}
}
$order = Orders::store($request->all()); $order = Orders::store($request->all());
if ($order->wasChanged('status')) { if ($order->wasChanged('status')) {
switch ($order->status) { switch ($order->status) {
@@ -45,7 +84,13 @@ class OrderController extends Controller
case 2: case 2:
OrderMails::sendShipping($order->id); OrderMails::sendShipping($order->id);
break; break;
case 4:
OfferStocks::restoreStock($order->id);
break;
default: default:
if ($previousStatus == 4) {
OfferStocks::decreaseStockForOrder($order->id);
}
} }
} }

View File

@@ -12,6 +12,7 @@ use App\Repositories\Shop\Customers;
use App\Repositories\Shop\CustomerAddresses; use App\Repositories\Shop\CustomerAddresses;
use App\Repositories\Shop\Deliveries; use App\Repositories\Shop\Deliveries;
use App\Repositories\Shop\DeliveryTypes; use App\Repositories\Shop\DeliveryTypes;
use App\Repositories\Shop\Offers;
use App\Repositories\Shop\OrderMails; use App\Repositories\Shop\OrderMails;
use App\Repositories\Shop\Orders; use App\Repositories\Shop\Orders;
use App\Repositories\Shop\Paybox; use App\Repositories\Shop\Paybox;
@@ -94,6 +95,25 @@ class OrderController extends Controller
$data['customer_id'] = Customers::getId(); $data['customer_id'] = Customers::getId();
$data['sale_channel_id'] = $data['sale_channel_id'] ?? SaleChannels::getDefaultID(); $data['sale_channel_id'] = $data['sale_channel_id'] ?? SaleChannels::getDefaultID();
$data['basket'] = Baskets::getBasketSummary($data['sale_channel_id'], $data['delivery_type_id'] ?? false); $data['basket'] = Baskets::getBasketSummary($data['sale_channel_id'], $data['delivery_type_id'] ?? false);
// Vérifier le stock avant de valider la commande
$insufficients = [];
foreach ($data['basket']['detail'] as $item) {
$offer = Offers::get($item['offer_id']);
if ($offer && $offer->stock_current !== null && $item['quantity'] > $offer->stock_current) {
$offer->load('article');
$insufficients[] = ($offer->article->name ?? 'Offre #'.$offer->id)
.' (stock : '.(int) $offer->stock_current.', demandé : '.$item['quantity'].')';
}
}
if (! empty($insufficients)) {
return redirect()->back()->withInput()->with(
'growl',
['Stock insuffisant pour : '.implode(' ; ', $insufficients).'. Veuillez ajuster les quantités.', 'danger']
);
}
$order = Orders::saveOrder($data); $order = Orders::saveOrder($data);
if ($order) { if ($order) {

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Mail;
use App\Models\Core\Mail\MailTemplate;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Spatie\MailTemplates\TemplateMailable;
class AlertePaiementAnnule extends TemplateMailable
{
use Queueable, SerializesModels;
public $subject;
public $numero_commande;
public $date_commande;
public $montant;
public $client_nom;
public $client_email;
public $reference_paiement;
protected static $templateModelClass = MailTemplate::class;
public function __construct($order, $reference)
{
$this->numero_commande = $order->ref;
$this->date_commande = $order->created_at->format('d/m/Y H:i');
$this->montant = number_format($order->total_shipped, 2, ',', ' ').' €';
$this->client_nom = $order->customer
? $order->customer->last_name.' '.$order->customer->first_name
: 'Client supprimé';
$this->client_email = $order->customer->email ?? 'inconnu';
$this->reference_paiement = $reference;
}
public function envelope()
{
return new Envelope(
from: new Address('boutique@jardinenvie.com', 'Jardin\'en\'Vie'),
subject: $this->subject,
);
}
}

49
app/Mail/AlerteStock.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
namespace App\Mail;
use App\Models\Core\Mail\MailTemplate;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Spatie\MailTemplates\TemplateMailable;
class AlerteStock extends TemplateMailable
{
use Queueable, SerializesModels;
public $subject;
public $article;
public $offre;
public $stock_restant;
public $seuil;
public $lien_article;
public $lien_offre;
protected static $templateModelClass = MailTemplate::class;
public function __construct($offer)
{
$this->article = $offer->article->name ?? 'Article #'.$offer->article_id;
$this->offre = $offer->id;
$this->stock_restant = $offer->stock_current;
$this->seuil = $offer->minimum_ondemand;
$this->lien_article = url('/Admin/Shop/Articles/edit/'.$offer->article_id);
$this->lien_offre = url('/Admin/Shop/Offers/edit/'.$offer->id);
}
public function envelope()
{
return new Envelope(
from: new Address('boutique@jardinenvie.com', 'Jardin\'en\'Vie'),
subject: $this->subject,
);
}
}

View File

@@ -218,7 +218,7 @@ class Article extends Model implements HasMedia
public function scopeWithAvailableOffers($query, $saleChannelId = false) public function scopeWithAvailableOffers($query, $saleChannelId = false)
{ {
return $query->whereHas('offers', function ($query) use ($saleChannelId) { return $query->visible()->whereHas('offers', function ($query) use ($saleChannelId) {
$query->active()->byStockAvailable(); $query->active()->byStockAvailable();
if ($saleChannelId) { if ($saleChannelId) {

View File

@@ -92,6 +92,11 @@ class Order extends Model
return $query->byStatus(3); return $query->byStatus(3);
} }
public function scopeNotCancelled($query)
{
return $query->where('status', '!=', 4);
}
public function scopeByStatus($query, $status) public function scopeByStatus($query, $status)
{ {
return $query->where('status', $status); return $query->where('status', $status);

View File

@@ -67,17 +67,15 @@ class Categories
$category_target = self::getNode($target_id); $category_target = self::getNode($target_id);
switch ($type) { switch ($type) {
case 'before':
return $category->insertBeforeNode($category_target);
case 'after': case 'after':
$category->afterNode($category_target); return $category->insertAfterNode($category_target);
break;
case 'inside': case 'inside':
$category_target->appendNode($category); return $category_target->prependNode($category);
break;
default: default:
$category->afterNode($category_target); return $category->insertAfterNode($category_target);
} }
return $category->save();
} }
public static function create($data) public static function create($data)

View File

@@ -43,6 +43,7 @@ class Articles
'id' => $offer->id, 'id' => $offer->id,
'name' => $offer->variation->name, 'name' => $offer->variation->name,
'prices' => $offer->tariff->price_lists->first()->price_list_values->toArray(), 'prices' => $offer->tariff->price_lists->first()->price_list_values->toArray(),
'stock' => $offer->stock_current !== null ? (int) $offer->stock_current : null,
]; ];
} }
@@ -66,6 +67,11 @@ class Articles
return $data; return $data;
} }
public static function getVisibilityMap()
{
return Article::pluck('visible', 'id')->toArray();
}
public static function getAll() public static function getAll()
{ {
return Article::orderBy('name', 'asc')->get(); return Article::orderBy('name', 'asc')->get();
@@ -311,8 +317,6 @@ class Articles
case 'merchandise': case 'merchandise':
$model = $model->merchandise(); $model = $model->merchandise();
break; break;
default:
$model = $model->botanic();
} }
return $model; return $model;

View File

@@ -116,6 +116,7 @@ class Baskets
'variation' => $offer->variation->name, 'variation' => $offer->variation->name,
'image' => Articles::getPreviewSrc(ArticleImages::getFullImageByArticle($offer->article)), 'image' => Articles::getPreviewSrc(ArticleImages::getFullImageByArticle($offer->article)),
'latin' => $offer->article->product->specie->latin ?? false, 'latin' => $offer->article->product->specie->latin ?? false,
'stock' => $offer->stock_current !== null ? (int) $offer->stock_current : null,
]; ];
} }
@@ -144,6 +145,13 @@ class Baskets
{ {
$offer = Offers::get($id, ['article', 'variation']); $offer = Offers::get($id, ['article', 'variation']);
if ($offer && $offer->stock_current !== null) {
$quantity = min($quantity, max(0, (int) $offer->stock_current));
if ($quantity <= 0) {
return false;
}
}
return [ return [
'id' => $id, 'id' => $id,
'name' => self::getArticleName($offer), 'name' => self::getArticleName($offer),

View File

@@ -2,19 +2,108 @@
namespace App\Repositories\Shop; namespace App\Repositories\Shop;
use App\Mail\AlerteStock;
use App\Models\Shop\Offer; use App\Models\Shop\Offer;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class OfferStocks class OfferStocks
{ {
public static function decreaseStock($item) public static function decreaseStock($item)
{ {
$offer = Offers::get($item['offer_id']); $offer = Offers::get($item['offer_id']);
$previousStock = $offer->stock_current;
$offer->stock_current = $offer->stock_current - $item['quantity']; $offer->stock_current = $offer->stock_current - $item['quantity'];
if ($offer->stock_current <= 0) { if ($offer->stock_current <= 0) {
$offer->stock_current = 0; $offer->stock_current = 0;
} }
return $offer->save(); $saved = $offer->save();
if ($saved) {
self::checkStockAlert($offer, $previousStock);
}
return $saved;
}
public static function checkStockAlert($offer, $previousStock)
{
$threshold = (float) $offer->minimum_ondemand;
if ($threshold <= 0) {
return;
}
$crossedThreshold = $previousStock > $threshold
&& $offer->stock_current <= $threshold;
if (! $crossedThreshold) {
return;
}
try {
$offer->load('article');
Mail::to('commande@jardinenvie.com')
->send(new AlerteStock($offer));
Log::info('Stock alert email sent', [
'offer_id' => $offer->id,
'article' => $offer->article->name ?? $offer->article_id,
'stock_current' => $offer->stock_current,
'threshold' => $threshold,
]);
} catch (\Throwable $e) {
Log::error('Failed to send stock alert email', [
'offer_id' => $offer->id,
'stock_current' => $offer->stock_current,
'exception' => $e->getMessage(),
]);
}
}
public static function restoreStock($orderId)
{
$details = \App\Models\Shop\OrderDetail::where('order_id', $orderId)->get();
foreach ($details as $detail) {
$offer = Offers::get($detail->offer_id);
if ($offer) {
$offer->stock_current = $offer->stock_current + $detail->quantity;
$offer->save();
}
}
}
public static function checkStockForOrder($orderId)
{
$details = \App\Models\Shop\OrderDetail::where('order_id', $orderId)->get();
$insufficients = [];
foreach ($details as $detail) {
$offer = Offers::get($detail->offer_id);
if ($offer && $offer->stock_current < $detail->quantity) {
$offer->load('article');
$insufficients[] = [
'name' => $offer->article->name ?? 'Offre #'.$offer->id,
'stock' => $offer->stock_current,
'quantity' => $detail->quantity,
];
}
}
return $insufficients;
}
public static function decreaseStockForOrder($orderId)
{
$details = \App\Models\Shop\OrderDetail::where('order_id', $orderId)->get();
foreach ($details as $detail) {
$offer = Offers::get($detail->offer_id);
if ($offer) {
$offer->stock_current = max(0, $offer->stock_current - $detail->quantity);
$offer->save();
}
}
} }
public static function getStockCurrent($id) public static function getStockCurrent($id)

View File

@@ -16,7 +16,11 @@ class Offers
{ {
return [ return [
'articles' => Articles::getOptionsWithNature(), 'articles' => Articles::getOptionsWithNature(),
'article_visibilities' => Articles::getVisibilityMap(),
'tariffs' => Tariffs::getOptions(), 'tariffs' => Tariffs::getOptions(),
'tariff_statuses' => Tariffs::getStatusMap(),
'tariff_status_labels' => Tariffs::getStatuses(),
'tariff_pricelist_counts' => Tariffs::getPriceListCountMap(),
'variations' => Variations::getOptions(), 'variations' => Variations::getOptions(),
]; ];
} }

View File

@@ -3,9 +3,11 @@
namespace App\Repositories\Shop; namespace App\Repositories\Shop;
use App\Mail\Acheminement; use App\Mail\Acheminement;
use App\Mail\AlertePaiementAnnule;
use App\Mail\ConfirmationCommande; use App\Mail\ConfirmationCommande;
use App\Mail\Preparation; use App\Mail\Preparation;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class OrderMails class OrderMails
{ {
@@ -37,4 +39,25 @@ class OrderMails
return Mail::to($order->customer->email)->send($mail); return Mail::to($order->customer->email)->send($mail);
} }
public static function sendCancelledOrderPaymentAlert($orderId, $reference)
{
$order = Orders::get($orderId, ['customer']);
try {
Mail::to('commande@jardinenvie.com')
->send(new AlertePaiementAnnule($order, $reference));
Log::info('Cancelled order payment alert sent', [
'order_id' => $orderId,
'order_ref' => $order->ref,
'reference' => $reference,
]);
} catch (\Throwable $e) {
Log::error('Failed to send cancelled order payment alert', [
'order_id' => $orderId,
'order_ref' => $order->ref,
'exception' => $e->getMessage(),
]);
}
}
} }

View File

@@ -48,36 +48,36 @@ class OrderMetrics
public static function countOfToday() public static function countOfToday()
{ {
return Order::ofToday()->count(); return Order::notCancelled()->ofToday()->count();
} }
public static function countOfLastWeek() public static function countOfLastWeek()
{ {
return Order::ofLastWeek()->count(); return Order::notCancelled()->ofLastWeek()->count();
} }
public static function countOfLastMonth() public static function countOfLastMonth()
{ {
return Order::ofLastMonth()->count(); return Order::notCancelled()->ofLastMonth()->count();
} }
public static function getTotalOfToday() public static function getTotalOfToday()
{ {
return Order::ofToday()->sum('total_taxed'); return Order::notCancelled()->ofToday()->sum('total_taxed');
} }
public static function getTotalOfLastWeek() public static function getTotalOfLastWeek()
{ {
return Order::ofLastWeek()->sum('total_taxed'); return Order::notCancelled()->ofLastWeek()->sum('total_taxed');
} }
public static function getTotalOfLastMonth() public static function getTotalOfLastMonth()
{ {
return Order::ofLastMonth()->sum('total_taxed'); return Order::notCancelled()->ofLastMonth()->sum('total_taxed');
} }
public static function getModel() public static function getModel()
{ {
return Order::query(); return Order::notCancelled();
} }
} }

View File

@@ -126,6 +126,26 @@ class Orders
return self::statuses()[$id] ?? false; return self::statuses()[$id] ?? false;
} }
public static function getStatusBadge($id)
{
$label = self::getStatus($id);
if ($label === false) {
return '';
}
$classes = [
0 => 'badge-warning',
1 => 'badge-info',
2 => 'badge-primary',
3 => 'badge-success',
4 => 'badge-danger',
];
$class = $classes[$id] ?? 'badge-secondary';
return '<span class="badge '.$class.'">'.$label.'</span>';
}
public static function getStatusByName($name) public static function getStatusByName($name)
{ {
$data = array_flip(self::statuses()); $data = array_flip(self::statuses());
@@ -135,7 +155,7 @@ class Orders
public static function statuses() public static function statuses()
{ {
return ['En attente', 'Préparation', 'Expédié', 'Livré']; return ['En attente', 'Préparation', 'Expédié', 'Livré', 'Annulé'];
} }
public static function getPaymentType($id) public static function getPaymentType($id)

View File

@@ -104,7 +104,9 @@ class Paybox
return true; return true;
} }
DB::transaction(function () use ($invoice, $order, $reference, $payload, $existingPayment, &$shouldNotify) { $isCancelled = (int) $order->status === 4;
DB::transaction(function () use ($invoice, $order, $reference, $payload, $existingPayment, &$shouldNotify, $isCancelled) {
$attributes = [ $attributes = [
'payment_type' => 1, 'payment_type' => 1,
'amount' => $invoice->total_shipped, 'amount' => $invoice->total_shipped,
@@ -134,14 +136,24 @@ class Paybox
Invoices::checkPayments($invoice->id); Invoices::checkPayments($invoice->id);
$paidStatus = Orders::getStatusByName('Préparation'); if (! $isCancelled) {
if ($paidStatus !== '' && (int) $order->status !== (int) $paidStatus) { $paidStatus = Orders::getStatusByName('Préparation');
$order->status = $paidStatus; if ($paidStatus !== '' && (int) $order->status !== (int) $paidStatus) {
$order->save(); $order->status = $paidStatus;
$order->save();
}
} }
}); });
if ($shouldNotify) { if ($isCancelled && $shouldNotify) {
Log::warning('Paybox payment received on cancelled order', [
'order_id' => $order->id,
'order_ref' => $order->ref,
'invoice_id' => $invoice->id,
'reference' => $reference,
]);
OrderMails::sendCancelledOrderPaymentAlert($order->id, $reference);
} elseif ($shouldNotify) {
try { try {
OrderMails::sendOrderConfirmed($order->id); OrderMails::sendOrderConfirmed($order->id);
} catch (\Throwable $exception) { } catch (\Throwable $exception) {

View File

@@ -50,11 +50,30 @@ class Tariffs
return self::getStatuses()[$status_id]; return self::getStatuses()[$status_id];
} }
public static function getStatusBadge($status_id)
{
$colors = ['success', 'warning', 'secondary', 'danger'];
$label = self::getStatus($status_id);
$color = $colors[$status_id] ?? 'secondary';
return '<span class="badge badge-'.$color.'">'.$label.'</span>';
}
public static function getStatuses() public static function getStatuses()
{ {
return ['Actif', 'Suspendu', 'Invisible', 'Obsolete']; return ['Actif', 'Suspendu', 'Invisible', 'Obsolete'];
} }
public static function getStatusMap()
{
return Tariff::pluck('status_id', 'id')->toArray();
}
public static function getPriceListCountMap()
{
return Tariff::withCount('price_lists')->pluck('price_lists_count', 'id')->toArray();
}
public static function getModel() public static function getModel()
{ {
return Tariff::query(); return Tariff::query();

View File

@@ -0,0 +1,75 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('mail_templates')->insert([
'mailable' => 'App\\Mail\\AlerteStock',
'subject' => json_encode(['fr' => '[Stock bas] {{article}} — {{stock_restant}} unités restantes'], JSON_UNESCAPED_UNICODE),
'html_template' => json_encode(['fr' => $this->getHtmlTemplate()], JSON_UNESCAPED_UNICODE),
'text_template' => json_encode(['fr' => $this->getTextTemplate()], JSON_UNESCAPED_UNICODE),
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('mail_templates')
->where('mailable', 'App\\Mail\\AlerteStock')
->delete();
}
private function getHtmlTemplate(): string
{
return '<table style="min-width: 100%;" width="100%" cellspacing="0" cellpadding="0">'
.'<tbody><tr><td>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr>'
.'<td style="margin: 0px auto; padding: 32px 0px 24px 4px; vertical-align: top;" align="center">'
.'<a href="https://www.jardinenvie.com/" style="text-decoration: none; color: #000000;">'
.'<img alt="Jardin\'enVie" src="https://boutique.jardinenvie.com/img/logo.png" style="margin: 0px auto; display: block;" width="300" height="138" border="0" /></a></td>'
.'</tr></tbody></table>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr><td style="margin: 0 auto; padding: 0px; vertical-align: top;">'
.'<table style="border-collapse: collapse; margin: 0 auto; width: 512px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody>'
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 40px; color: #000000; font-size: 28px; padding: 20px 0px 10px 0px; font-weight: 800; text-align: center;">'
.'Alerte stock bas</td></tr>'
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 28px; color: #000000; font-size: 16px; text-align: center; padding: 10px 0 30px 0;">'
.'<p>Le stock de l\'article <strong>{{article}}</strong> (offre n&deg;{{offre}}) '
.'a atteint le seuil d\'alerte.</p>'
.'<p style="font-size: 24px; font-weight: bold; color: #c0392b; padding: 10px 0;">{{stock_restant}} unit&eacute;s restantes</p>'
.'<p>Seuil d\'alerte configur&eacute; : {{seuil}} unit&eacute;s</p>'
.'<p style="padding-top: 15px; color: #666;">Pensez &agrave; r&eacute;approvisionner cet article.</p>'
.'</td></tr>'
.'</tbody></table>'
.'</td></tr></tbody></table>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px; text-align: center;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; font-size: 12px; font-weight: 600; line-height: 20px; padding: 16px; color: #999999;">'
.'Jardin\'enVie Artisan Semencier<br />429 route des chaux, 26500 Bourg les Valence - Dr&ocirc;me'
.'</td></tr></tbody></table>'
.'</td></tr></tbody></table>';
}
private function getTextTemplate(): string
{
return "ALERTE STOCK BAS\n\n"
."Article : {{article}} (offre n°{{offre}})\n"
."Stock restant : {{stock_restant}} unités\n"
."Seuil d'alerte : {{seuil}} unités\n\n"
."Pensez à réapprovisionner cet article.\n\n"
."Jardin'enVie Artisan Semencier\n"
.'429 route des chaux, 26500 Bourg les Valence - Drôme';
}
};

View File

@@ -0,0 +1,86 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('mail_templates')->insert([
'mailable' => 'App\\Mail\\AlertePaiementAnnule',
'subject' => json_encode(['fr' => '[URGENT] Paiement reçu sur commande annulée {{numero_commande}}'], JSON_UNESCAPED_UNICODE),
'html_template' => json_encode(['fr' => $this->getHtmlTemplate()], JSON_UNESCAPED_UNICODE),
'text_template' => json_encode(['fr' => $this->getTextTemplate()], JSON_UNESCAPED_UNICODE),
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('mail_templates')
->where('mailable', 'App\\Mail\\AlertePaiementAnnule')
->delete();
}
private function getHtmlTemplate(): string
{
return '<table style="min-width: 100%;" width="100%" cellspacing="0" cellpadding="0">'
.'<tbody><tr><td>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr>'
.'<td style="margin: 0px auto; padding: 32px 0px 24px 4px; vertical-align: top;" align="center">'
.'<a href="https://www.jardinenvie.com/" style="text-decoration: none; color: #000000;">'
.'<img alt="Jardin\'enVie" src="https://boutique.jardinenvie.com/img/logo.png" style="margin: 0px auto; display: block;" width="300" height="138" border="0" /></a></td>'
.'</tr></tbody></table>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr><td style="margin: 0 auto; padding: 0px; vertical-align: top;">'
.'<table style="border-collapse: collapse; margin: 0 auto; width: 512px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody>'
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 40px; color: #c0392b; font-size: 28px; padding: 20px 0px 10px 0px; font-weight: 800; text-align: center;">'
.'&#9888; Paiement sur commande annul&eacute;e</td></tr>'
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 28px; color: #000000; font-size: 16px; text-align: center; padding: 10px 0 30px 0;">'
.'<p>Un paiement Paybox a &eacute;t&eacute; re&ccedil;u sur une commande <strong>annul&eacute;e</strong>.</p>'
.'<p style="font-size: 20px; font-weight: bold; color: #c0392b; padding: 10px 0;">Un remboursement est probablement n&eacute;cessaire.</p>'
.'<table style="margin: 15px auto; text-align: left; border-collapse: collapse;" cellpadding="8">'
.'<tr><td style="font-weight: bold; padding-right: 15px;">Commande :</td><td>{{numero_commande}}</td></tr>'
.'<tr><td style="font-weight: bold; padding-right: 15px;">Date :</td><td>{{date_commande}}</td></tr>'
.'<tr><td style="font-weight: bold; padding-right: 15px;">Montant :</td><td style="font-size: 18px; font-weight: bold; color: #c0392b;">{{montant}}</td></tr>'
.'<tr><td style="font-weight: bold; padding-right: 15px;">Client :</td><td>{{client_nom}}</td></tr>'
.'<tr><td style="font-weight: bold; padding-right: 15px;">Email :</td><td>{{client_email}}</td></tr>'
.'<tr><td style="font-weight: bold; padding-right: 15px;">R&eacute;f. paiement :</td><td>{{reference_paiement}}</td></tr>'
.'</table>'
.'<p style="padding-top: 15px; color: #666;">Veuillez proc&eacute;der au remboursement du client dans les plus brefs d&eacute;lais.</p>'
.'</td></tr>'
.'</tbody></table>'
.'</td></tr></tbody></table>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px; text-align: center;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; font-size: 12px; font-weight: 600; line-height: 20px; padding: 16px; color: #999999;">'
.'Jardin\'enVie Artisan Semencier<br />429 route des chaux, 26500 Bourg les Valence - Dr&ocirc;me'
.'</td></tr></tbody></table>'
.'</td></tr></tbody></table>';
}
private function getTextTemplate(): string
{
return "⚠ PAIEMENT SUR COMMANDE ANNULÉE\n\n"
."Un paiement Paybox a été reçu sur une commande annulée.\n"
."Un remboursement est probablement nécessaire.\n\n"
."Commande : {{numero_commande}}\n"
."Date : {{date_commande}}\n"
."Montant : {{montant}}\n"
."Client : {{client_nom}}\n"
."Email : {{client_email}}\n"
."Réf. paiement : {{reference_paiement}}\n\n"
."Veuillez procéder au remboursement du client dans les plus brefs délais.\n\n"
."Jardin'enVie Artisan Semencier\n"
.'429 route des chaux, 26500 Bourg les Valence - Drôme';
}
};

View File

@@ -0,0 +1,125 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('mail_templates')
->where('mailable', 'App\\Mail\\AlerteStock')
->update([
'html_template' => json_encode(['fr' => $this->getHtmlTemplate()], JSON_UNESCAPED_UNICODE),
'text_template' => json_encode(['fr' => $this->getTextTemplate()], JSON_UNESCAPED_UNICODE),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('mail_templates')
->where('mailable', 'App\\Mail\\AlerteStock')
->update([
'html_template' => json_encode(['fr' => $this->getOldHtmlTemplate()], JSON_UNESCAPED_UNICODE),
'text_template' => json_encode(['fr' => $this->getOldTextTemplate()], JSON_UNESCAPED_UNICODE),
'updated_at' => now(),
]);
}
private function getHtmlTemplate(): string
{
return '<table style="min-width: 100%;" width="100%" cellspacing="0" cellpadding="0">'
.'<tbody><tr><td>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr>'
.'<td style="margin: 0px auto; padding: 32px 0px 24px 4px; vertical-align: top;" align="center">'
.'<a href="https://www.jardinenvie.com/" style="text-decoration: none; color: #000000;">'
.'<img alt="Jardin\'enVie" src="https://boutique.jardinenvie.com/img/logo.png" style="margin: 0px auto; display: block;" width="300" height="138" border="0" /></a></td>'
.'</tr></tbody></table>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr><td style="margin: 0 auto; padding: 0px; vertical-align: top;">'
.'<table style="border-collapse: collapse; margin: 0 auto; width: 512px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody>'
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 40px; color: #000000; font-size: 28px; padding: 20px 0px 10px 0px; font-weight: 800; text-align: center;">'
.'Alerte stock bas</td></tr>'
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 28px; color: #000000; font-size: 16px; text-align: center; padding: 10px 0 30px 0;">'
.'<p>Le stock de l\'article <strong>{{article}}</strong> (offre n&deg;{{offre}}) '
.'a atteint le seuil d\'alerte.</p>'
.'<p style="font-size: 24px; font-weight: bold; color: #c0392b; padding: 10px 0;">{{stock_restant}} unit&eacute;s restantes</p>'
.'<p>Seuil d\'alerte configur&eacute; : {{seuil}} unit&eacute;s</p>'
.'<p style="padding-top: 15px;"><a href="{{lien_article}}" style="display: inline-block; padding: 10px 20px; background-color: #3498db; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: bold;">Voir l\'article</a>'
.'&nbsp;&nbsp;<a href="{{lien_offre}}" style="display: inline-block; padding: 10px 20px; background-color: #27ae60; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: bold;">Voir l\'offre</a></p>'
.'<p style="padding-top: 10px; color: #666;">Pensez &agrave; r&eacute;approvisionner cet article.</p>'
.'</td></tr>'
.'</tbody></table>'
.'</td></tr></tbody></table>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px; text-align: center;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; font-size: 12px; font-weight: 600; line-height: 20px; padding: 16px; color: #999999;">'
.'Jardin\'enVie Artisan Semencier<br />429 route des chaux, 26500 Bourg les Valence - Dr&ocirc;me'
.'</td></tr></tbody></table>'
.'</td></tr></tbody></table>';
}
private function getTextTemplate(): string
{
return "ALERTE STOCK BAS\n\n"
."Article : {{article}} (offre n°{{offre}})\n"
."Stock restant : {{stock_restant}} unités\n"
."Seuil d'alerte : {{seuil}} unités\n\n"
."Voir l'article dans l'admin : {{lien_article}}\n"
."Voir l'offre dans l'admin : {{lien_offre}}\n\n"
."Pensez à réapprovisionner cet article.\n\n"
."Jardin'enVie Artisan Semencier\n"
.'429 route des chaux, 26500 Bourg les Valence - Drôme';
}
private function getOldHtmlTemplate(): string
{
return '<table style="min-width: 100%;" width="100%" cellspacing="0" cellpadding="0">'
.'<tbody><tr><td>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr>'
.'<td style="margin: 0px auto; padding: 32px 0px 24px 4px; vertical-align: top;" align="center">'
.'<a href="https://www.jardinenvie.com/" style="text-decoration: none; color: #000000;">'
.'<img alt="Jardin\'enVie" src="https://boutique.jardinenvie.com/img/logo.png" style="margin: 0px auto; display: block;" width="300" height="138" border="0" /></a></td>'
.'</tr></tbody></table>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr><td style="margin: 0 auto; padding: 0px; vertical-align: top;">'
.'<table style="border-collapse: collapse; margin: 0 auto; width: 512px;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody>'
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 40px; color: #000000; font-size: 28px; padding: 20px 0px 10px 0px; font-weight: 800; text-align: center;">'
.'Alerte stock bas</td></tr>'
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 28px; color: #000000; font-size: 16px; text-align: center; padding: 10px 0 30px 0;">'
.'<p>Le stock de l\'article <strong>{{article}}</strong> (offre n&deg;{{offre}}) '
.'a atteint le seuil d\'alerte.</p>'
.'<p style="font-size: 24px; font-weight: bold; color: #c0392b; padding: 10px 0;">{{stock_restant}} unit&eacute;s restantes</p>'
.'<p>Seuil d\'alerte configur&eacute; : {{seuil}} unit&eacute;s</p>'
.'<p style="padding-top: 15px; color: #666;">Pensez &agrave; r&eacute;approvisionner cet article.</p>'
.'</td></tr>'
.'</tbody></table>'
.'</td></tr></tbody></table>'
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px; text-align: center;" cellspacing="0" cellpadding="0" border="0" align="center">'
.'<tbody><tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; font-size: 12px; font-weight: 600; line-height: 20px; padding: 16px; color: #999999;">'
.'Jardin\'enVie Artisan Semencier<br />429 route des chaux, 26500 Bourg les Valence - Dr&ocirc;me'
.'</td></tr></tbody></table>'
.'</td></tr></tbody></table>';
}
private function getOldTextTemplate(): string
{
return "ALERTE STOCK BAS\n\n"
."Article : {{article}} (offre n°{{offre}})\n"
."Stock restant : {{stock_restant}} unités\n"
."Seuil d'alerte : {{seuil}} unités\n\n"
."Pensez à réapprovisionner cet article.\n\n"
."Jardin'enVie Artisan Semencier\n"
.'429 route des chaux, 26500 Bourg les Valence - Drôme';
}
};

View File

@@ -436,3 +436,63 @@ div.megamenu ul.megamenu li.megamenu.level1
max-width: 100%; max-width: 100%;
} }
} }
/* -- Titres des rayons -- */
.shelve-title {
font-size: 2em;
}
.shelve-article-label {
height: 48px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* -- Responsive: très petites résolutions (< 430px) -- */
@media (max-width: 429.98px) {
.shelve-title {
font-size: 1.4em;
}
.shelve-btn {
font-size: 0.8em;
}
.shelve-btn-suffix {
display: none;
}
.shelve-article-label {
font-size: 0.8em;
height: 36px;
}
.shelve-slide {
padding-left: 0.15rem !important;
padding-right: 0.15rem !important;
}
body {
overflow-x: hidden;
}
.homepage-text {
font-size: 1em !important;
}
.homepage-text h1 {
font-size: 1.3em !important;
}
.homepage-text h5 {
font-size: 0.85em !important;
}
.homepage-text p {
font-size: 0.85em;
}
.homepage-text .home-nav .auto {
min-width: 100% !important;
max-width: 100% !important;
}
#navbarContentMobile .nav-link,
#navbarContentMobile .dropdown-menu a {
font-size: 0.85em;
}
}

View File

@@ -1,3 +1,12 @@
@if ($category['id'] ?? false)
<div class="d-flex justify-content-end mb-3">
<a href="{{ route('Admin.Shop.Articles.index', ['category_id' => $category['id']]) }}" class="btn btn-outline-primary">
Voir les articles de ce rayon
<i class="fa fa-external-link"></i>
</a>
</div>
@endif
<div class="row"> <div class="row">
<div class="col-9"> <div class="col-9">
<div class="row mb-3"> <div class="row mb-3">

View File

@@ -14,57 +14,30 @@
@push('js') @push('js')
<script> <script>
var position = '';
var target_node = '';
$(function() { $(function() {
var $tree = $('#tree1').tree({ var $tree = $('#tree1').tree({
dragAndDrop: true, dragAndDrop: true,
onDragStop: handleMove,
autoOpen: 0 autoOpen: 0
}); });
$tree.on('tree.move', function(e) { $tree.on('tree.move', function(e) {
// e.preventDefault(); var position = e.move_info.position;
var target_node = e.move_info.target_node;
var moved_node = e.move_info.moved_node;
position = e.move_info.position; $.ajax({
target_node = e.move_info.target_node; method: "POST",
url: "{{ route('Admin.Shop.Categories.moveTree') }}",
function getNewParentNode() { data: {
if (position == 'inside') { node_id: moved_node.id,
return target_node; type: position,
} target_id: target_node.id
else { }
// before or after });
return target_node.parent;
}
}
var parent_node = getNewParentNode();
console.log("Parent node", parent_node);
}); });
}); });
function handleMove(node, e) {
console.log(node);
node_id = node.id;
console.log(node_id);
console.log(position);
console.log(target_node);
target_node_id = target_node.id;
$.ajax({
method: "POST",
url: "{{ route('Admin.Shop.Categories.moveTree') }}",
data: { node_id: node.id, type: position, target_id: target_node.id }
});
// console.log(e);
// console.log($('#tree1').tree('getTree'));
}
</script> </script>
@endpush @endpush

View File

@@ -4,7 +4,7 @@
]) ])
@section('content') @section('content')
{{ Form::open(['route' => 'Admin.Shop.Contents.store', 'id' => 'content-form', 'autocomplete' => 'off']) }} {{ Form::open(['route' => 'Admin.Shop.Contents.store', 'id' => 'homepage-form', 'autocomplete' => 'off']) }}
<input type="hidden" name="id" value="{{ $content['id'] }}"> <input type="hidden" name="id" value="{{ $content['id'] }}">
@include('Admin.Shop.Contents.form') @include('Admin.Shop.Contents.form')
</form> </form>

View File

@@ -25,13 +25,21 @@
@foreach ($lastOrders as $order) @foreach ($lastOrders as $order)
<tr> <tr>
<td> <td>
<a href="{{ route('Admin.Shop.Customers.edit', ['id' => $order->customer->id]) }}" @if ($order->customer)
class="alert-link green"> <a href="{{ route('Admin.Shop.Customers.edit', ['id' => $order->customer->id]) }}"
{{ $order->customer->first_name }} class="alert-link green">
{{ $order->customer->last_name }} {{ $order->customer->first_name }}
</a> {{ $order->customer->last_name }}
</a>
@else
<span class="text-muted">Client supprimé</span>
@endif
</td>
<td>
@if ($order->customer)
{{ $order->customer->city }} ({{ substr($order->customer->zipcode, 0, 2) }})
@endif
</td> </td>
<td>{{ $order->customer->city }} ({{ substr($order->customer->zipcode, 0, 2) }})</td>
<td>{{ Carbon\Carbon::parse($order->created_at)->format('d/m/Y H:i') }}</td> <td>{{ Carbon\Carbon::parse($order->created_at)->format('d/m/Y H:i') }}</td>
<td class="text-right font-weight-bold"> <td class="text-right font-weight-bold">
{{ $order->total_shipped }} {{ $order->total_shipped }}

View File

@@ -14,45 +14,66 @@
<div class="col-12"> <div class="col-12">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-12">
@include('components.form.select', [ <div class="d-flex align-items-end">
'name' => 'article_id', <div class="flex-grow-1">
'id_name' => 'article_id', @include('components.form.select', [
'list' => $articles ?? null, 'name' => 'article_id',
'value' => $offer['article_id'] ?? null, 'id_name' => 'article_id',
'with_empty' => '', 'list' => $articles ?? null,
'class' => 'select2 select_article', 'value' => $offer['article_id'] ?? null,
'label' => 'Article', 'with_empty' => '',
'required' => true, 'class' => 'select2 select_article',
]) 'label' => 'Article',
'required' => true,
])
</div>
<a id="edit-article-link" href="#" class="btn btn-sm btn-outline-secondary ml-2 mb-1" title="Modifier l'article" style="display:none;">
<i class="fa fa-external-link"></i>
</a>
</div>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-4"> <div class="col-4">
@include('components.form.select', [ <div class="d-flex align-items-end">
'name' => 'variation_id', <div class="flex-grow-1">
'id_name' => 'variation_id', @include('components.form.select', [
'list' => $variations ?? null, 'name' => 'variation_id',
'value' => $offer['variation_id'] ?? null, 'id_name' => 'variation_id',
'with_empty' => '', 'list' => $variations ?? null,
'class' => 'select2 select_variation', 'value' => $offer['variation_id'] ?? null,
'label' => __('shop.packages.name'), 'with_empty' => '',
'required' => true, 'class' => 'select2 select_variation',
]) 'label' => __('shop.packages.name'),
'required' => true,
])
</div>
<a id="edit-variation-link" href="#" class="btn btn-sm btn-outline-secondary ml-2 mb-1" title="Modifier la déclinaison" style="display:none;">
<i class="fa fa-external-link"></i>
</a>
</div>
</div> </div>
<div class="col-4"> <div class="col-4">
@include('components.form.select', [ <div class="d-flex align-items-end">
'name' => 'tariff_id', <div class="flex-grow-1">
'id_name' => 'tariff_id', @include('components.form.select', [
'list' => $tariffs ?? null, 'name' => 'tariff_id',
'value' => $offer['tariff_id'] ?? null, 'id_name' => 'tariff_id',
'with_empty' => '', 'list' => $tariffs ?? null,
'class' => 'select2 select_tariffs', 'value' => $offer['tariff_id'] ?? null,
'label' => 'Tarif', 'with_empty' => '',
'required' => true, 'class' => 'select2 select_tariffs',
]) 'label' => 'Tarif',
'required' => true,
])
</div>
<a id="edit-tariff-link" href="#" class="btn btn-sm btn-outline-secondary ml-2 mb-1" title="Modifier le tarif" style="display:none;">
<i class="fa fa-external-link"></i>
</a>
</div>
</div> </div>
<div class="col-4"> <div class="col-2">
@include('components.form.input', [ @include('components.form.input', [
'name' => 'weight', 'name' => 'weight',
'value' => $offer['weight'] ?? null, 'value' => $offer['weight'] ?? null,
@@ -60,6 +81,15 @@
'required' => true, 'required' => true,
]) ])
</div> </div>
<div class="col-2">
<input type="hidden" name="status_id" value="0">
@include('components.form.toggle', [
'name' => 'status_id',
'value' => $offer['status_id'] ?? 0,
'label' => 'Actif',
'size' => 'md',
])
</div>
</div> </div>
@component('components.card', ['title' => 'Disponibilité', 'class' => 'mt-5']) @component('components.card', ['title' => 'Disponibilité', 'class' => 'mt-5'])
@@ -122,5 +152,80 @@
initChevron(); initChevron();
initSaveForm('#offer-form'); initSaveForm('#offer-form');
initSelect2(); initSelect2();
$('#status_id').bootstrapToggle();
// Article visibility badges in select2
var articleVisibilities = {!! json_encode($article_visibilities ?? (object)[]) !!};
function formatArticle(item) {
if (!item.id) return item.text;
var visible = articleVisibilities[item.id];
var badge = (visible == 1)
? '<span class="badge badge-success" style="font-size:0.75em;margin-left:4px;">Visible</span>'
: '<span class="badge badge-warning" style="font-size:0.75em;margin-left:4px;">Invisible</span>';
return $('<span style="display:flex;justify-content:space-between;align-items:center;width:100%;">' + item.text + badge + '</span>');
}
$('#article_id').select2('destroy').select2({
placeholder: "{{ __('select_a_value') }}",
allowClear: false,
width: { value: '100%' },
templateResult: formatArticle,
templateSelection: formatArticle
});
// Tariff status badges in select2
var tariffStatuses = {!! json_encode($tariff_statuses ?? (object)[]) !!};
var tariffStatusLabels = {!! json_encode($tariff_status_labels ?? []) !!};
var tariffStatusColors = {0: '#28a745', 1: '#ffc107', 2: '#6c757d', 3: '#dc3545'};
var tariffPLCounts = {!! json_encode($tariff_pricelist_counts ?? (object)[]) !!};
function formatTariff(item) {
if (!item.id) return item.text;
var statusId = tariffStatuses[item.id];
var color = tariffStatusColors[statusId] || '#6c757d';
var label = tariffStatusLabels[statusId] || '';
var plCount = tariffPLCounts[item.id] || 0;
var warning = plCount == 0 ? '<span style="margin-left:4px;cursor:help;" title="Aucune liste de prix"><i class="fas fa-exclamation-triangle text-danger" style="font-size:1.4em;vertical-align:-2px;"></i></span>' : '';
return $('<span style="display:flex;justify-content:space-between;align-items:center;width:100%;">' + item.text + '<span>' + warning + '<span class="badge" style="background:' + color + ';color:#fff;font-size:0.75em;margin-left:4px;">' + label + '</span></span></span>');
}
$('#tariff_id').select2('destroy').select2({
placeholder: "{{ __('select_a_value') }}",
allowClear: false,
width: { value: '100%' },
templateResult: formatTariff,
templateSelection: formatTariff
});
function updateEditLink(selectId, linkId, routeTemplate) {
var val = $('#' + selectId).val();
var $link = $('#' + linkId);
if (val) {
$link.attr('href', routeTemplate.replace('__ID__', val)).show();
} else {
$link.hide();
}
}
var articleRoute = '{{ route('Admin.Shop.Articles.edit', ['id' => '__ID__']) }}';
var variationRoute = '{{ route('Admin.Shop.Variations.edit', ['id' => '__ID__']) }}';
var tariffRoute = '{{ route('Admin.Shop.Tariffs.edit', ['id' => '__ID__']) }}';
// Init on page load
updateEditLink('article_id', 'edit-article-link', articleRoute);
updateEditLink('variation_id', 'edit-variation-link', variationRoute);
updateEditLink('tariff_id', 'edit-tariff-link', tariffRoute);
// Update on change
$('#article_id').on('change', function() {
updateEditLink('article_id', 'edit-article-link', articleRoute);
});
$('#variation_id').on('change', function() {
updateEditLink('variation_id', 'edit-variation-link', variationRoute);
});
$('#tariff_id').on('change', function() {
updateEditLink('tariff_id', 'edit-tariff-link', tariffRoute);
});
</script> </script>
@endpush @endpush

View File

@@ -9,7 +9,8 @@
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div> </div>
<div class="modal-body"> @include('load.form.password_toggle')
<div class="modal-body">
<div class="row" style="padding: 10px 20px;"> <div class="row" style="padding: 10px 20px;">
<div class="col-xs-12 text-center" id="changePasswordMessage"></div> <div class="col-xs-12 text-center" id="changePasswordMessage"></div>
</div> </div>

View File

@@ -1,9 +1,6 @@
@php @php
// Check if article is not visible OR has no offers at all // Check if article is not visible OR has no offers at all
$hasNoOffers = empty($article['offers']['semences'] ?? false) $hasNoOffers = empty($article['offers'] ?? false) || !array_filter($article['offers']);
&& empty($article['offers']['plants'] ?? false)
&& empty($article['offers']['legumes'] ?? false)
&& empty($article['offers']['marchandise'] ?? false);
$shouldShowComingSoon = !($article['visible'] ?? true) || $hasNoOffers; $shouldShowComingSoon = !($article['visible'] ?? true) || $hasNoOffers;
@endphp @endphp
@@ -19,41 +16,16 @@
</div> </div>
@else @else
{{-- Display normal offers for visible articles with available offers --}} {{-- Display normal offers for visible articles with available offers --}}
@if ($article['offers']['semences'] ?? false) @foreach ($article['offers'] as $natureKey => $natureOffers)
@include('Shop.Articles.partials.addBasket', [ @if (!empty($natureOffers))
'data' => $article['offers']['semences'], @include('Shop.Articles.partials.addBasket', [
'title' => 'Semences', 'data' => $natureOffers,
'model' => 'semences', 'title' => ucfirst($natureKey),
'bgClass' => 'bg-green-light', 'model' => $natureKey,
]) 'bgClass' => 'bg-green-light',
@endif ])
@endif
@if ($article['offers']['plants'] ?? false) @endforeach
@include('Shop.Articles.partials.addBasket', [
'data' => $article['offers']['plants'],
'title' => 'Plants',
'model' => 'plants',
'bgClass' => 'bg-green-light',
])
@endif
@if ($article['offers']['legumes'] ?? false)
@include('Shop.Articles.partials.addBasket', [
'data' => $article['offers']['legumes'],
'title' => 'Légumes',
'model' => 'legumes',
'bgClass' => 'bg-green-light',
])
@endif
@if ($article['offers']['marchandise'] ?? false)
@include('Shop.Articles.partials.addBasket', [
'data' => $article['offers']['marchandise'],
'title' => 'Marchandises',
'model' => 'marchandise',
'bgClass' => 'bg-green-light',
])
@endif
@endif @endif
@include('load.basket') @include('load.basket')

View File

@@ -18,8 +18,9 @@
'name' => 'quantity', 'name' => 'quantity',
'class' => 'quantity', 'class' => 'quantity',
'id_name' => $model . '-quantity', 'id_name' => $model . '-quantity',
'value' => (int) $data[0]['prices'][0]['quantity'], 'value' => 1,
'min' => $data[0]['prices'][0]['quantity'], 'min' => 1,
'max' => $data[0]['stock'] ?? false,
'step' => 1, 'step' => 1,
]) ])
</div> </div>
@@ -44,10 +45,35 @@
@push('js') @push('js')
<script> <script>
var {{ $model }}Stocks = {
@foreach ($data as $offer)
{{ $offer['id'] }}: {{ $offer['stock'] !== null ? $offer['stock'] : 'null' }},
@endforeach
};
function update{{ ucfirst($model) }}Max() {
var offerId = $('#{{ $model }}-offer_id').find('option:selected').val();
var stock = {{ $model }}Stocks[offerId];
var input = $('#{{ $model }}-quantity');
if (stock !== null && stock !== undefined) {
input.attr('max', stock);
if (parseInt(input.val()) > stock) {
input.val(stock);
}
} else {
input.removeAttr('max');
}
if (parseInt(input.val()) < 1) {
input.val(1);
}
}
$('#{{ $model }}-quantity').change(function() { $('#{{ $model }}-quantity').change(function() {
update{{ ucfirst($model) }}Max();
setPrice('{{ $model }}'); setPrice('{{ $model }}');
}); });
$('#{{ $model }}-offer_id').change(function() { $('#{{ $model }}-offer_id').change(function() {
update{{ ucfirst($model) }}Max();
setPrice('{{ $model }}'); setPrice('{{ $model }}');
}); });
</script> </script>

View File

@@ -19,9 +19,8 @@
</div> </div>
<div class="col-lg-5 col-xs-12 text-justify"> <div class="col-lg-5 col-xs-12 text-justify">
{!! $article['description']['variety'] ?? null !!} {!! $article['description']['variety'] ?? null !!}
{!! $article['description']['semences'] ?? null !!}
{!! $article['description']['plants'] ?? null !!}
{!! $article['description']['merchandise'] ?? null !!} {!! $article['description']['merchandise'] ?? null !!}
{!! $article['description']['description'] ?? null !!}
@if ($article['description']['plus'] ?? false) @if ($article['description']['plus'] ?? false)
<h3>Spécificités</h3> <h3>Spécificités</h3>

View File

@@ -53,7 +53,12 @@
$('.basket-quantity').change(function() { $('.basket-quantity').change(function() {
var offer_id = $(this).data('id'); var offer_id = $(this).data('id');
var quantity = $(this).val(); var quantity = parseInt($(this).val()) || 1;
var min = parseInt($(this).attr('min')) || 1;
var max = parseInt($(this).attr('max'));
if (quantity < min) quantity = min;
if (max && quantity > max) quantity = max;
$(this).val(quantity);
var $row = $(this).closest('.row'); var $row = $(this).closest('.row');
updateBasket(offer_id, quantity, function() { updateBasket(offer_id, quantity, function() {
calculatePrice($row); calculatePrice($row);

View File

@@ -18,6 +18,8 @@
'value' => $item['quantity'], 'value' => $item['quantity'],
'class' => 'basket-quantity', 'class' => 'basket-quantity',
'data_id' => $item['id'], 'data_id' => $item['id'],
'min' => 1,
'max' => $item['stock'] ?? false,
]) ])
</div> </div>
<div class="col-4 text-right" style="font-size: 2em;" id="basket_total-{{ $item['id'] }}"> <div class="col-4 text-right" style="font-size: 2em;" id="basket_total-{{ $item['id'] }}">

View File

@@ -2,12 +2,12 @@
<div class="mb-5 bg-green-light shadow2"> <div class="mb-5 bg-green-light shadow2">
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<h1 class="p-2 green" style="font-size: 2em;">{{ $shelve['name'] }}</h1> <h1 class="p-2 green shelve-title">{{ $shelve['name'] }}</h1>
</div> </div>
<div class="col-6 text-right"> <div class="col-6 text-right">
<a href="{{ route('Shop.Categories.show', ['id' => $shelve['id']]) }}" <a href="{{ route('Shop.Categories.show', ['id' => $shelve['id']]) }}"
class="mt-2 mr-2 btn btn-green-dark"> class="mt-2 mr-2 btn btn-green-dark shelve-btn">
Découvrir la sélection Découvrir<span class="shelve-btn-suffix"> la sélection</span>
</a> </a>
<!-- <!--
<a class="mt-2 green-dark btn" href="{{ route('Shop.Categories.show', ['id' => $shelve['id']]) }}">Tout <a class="mt-2 green-dark btn" href="{{ route('Shop.Categories.show', ['id' => $shelve['id']]) }}">Tout
@@ -18,11 +18,11 @@
<div class="row"> <div class="row">
<div class="col-11 mx-auto shelve_slider_{{ $shelve['id'] }} slider"> <div class="col-11 mx-auto shelve_slider_{{ $shelve['id'] }} slider">
@foreach ($shelve['articles'] as $name => $article) @foreach ($shelve['articles'] as $name => $article)
<div class="text-center pr-2 pl-2"> <div class="text-center pr-2 pl-2 shelve-slide">
<a class="green" href="{{ route('Shop.Articles.show', ['id' => $article['id']]) }}"> <a class="green" href="{{ route('Shop.Articles.show', ['id' => $article['id']]) }}">
<img data-lazy="{{ App\Repositories\Shop\Articles::getPreviewSrc($article['image'] ?? false) }}" <img data-lazy="{{ App\Repositories\Shop\Articles::getPreviewSrc($article['image'] ?? false) }}"
class="d-block w-100 rounded" alt="{{ $name }}" /> class="d-block w-100 rounded" alt="{{ $name }}" />
<div style="height: 48px;"> <div class="shelve-article-label">
{{ $name }} {{ $name }}
</div> </div>
</a> </a>

View File

@@ -1,14 +1,4 @@
<div id="registred"> <div id="registred">
<x-layout.collapse id="invoice_addresses" title="Adresse de facturation" class="rounded-lg mb-3" uncollapsed=true>
@include('Shop.Customers.partials.addresses', [
'addresses' => $customer['invoice_addresses'] ?? [],
'prefix' => 'invoices',
'inputName' => 'invoice[invoice_address_id]',
'with_name' => true,
'selected' => $customer['invoice_address_id'] ?? null,
])
</x-layout.collapse>
<x-layout.collapse id="delivery_mode" title="Mode de livraison" class="rounded-lg mb-3" uncollapsed=true> <x-layout.collapse id="delivery_mode" title="Mode de livraison" class="rounded-lg mb-3" uncollapsed=true>
@include('Shop.Orders.partials.deliveries') @include('Shop.Orders.partials.deliveries')
</x-layout.collapse> </x-layout.collapse>
@@ -25,6 +15,16 @@
@include('Shop.Orders.partials.shipping') @include('Shop.Orders.partials.shipping')
</x-layout.collapse> </x-layout.collapse>
<x-layout.collapse id="invoice_addresses" title="Adresse de facturation" class="rounded-lg mb-3" uncollapsed=true>
@include('Shop.Customers.partials.addresses', [
'addresses' => $customer['invoice_addresses'] ?? [],
'prefix' => 'invoices',
'inputName' => 'invoice[invoice_address_id]',
'with_name' => true,
'selected' => $customer['invoice_address_id'] ?? null,
])
</x-layout.collapse>
<x-layout.collapse id="payment" title="Paiement" class="rounded-lg mb-3" uncollapsed=true> <x-layout.collapse id="payment" title="Paiement" class="rounded-lg mb-3" uncollapsed=true>
@include('Shop.Orders.partials.payments') @include('Shop.Orders.partials.payments')
</x-layout.collapse> </x-layout.collapse>

View File

@@ -10,6 +10,7 @@
<div class="alert alert-info"> <div class="alert alert-info">
<p>{{ __('boilerplate::auth.firstlogin.intro') }}</p> <p>{{ __('boilerplate::auth.firstlogin.intro') }}</p>
</div> </div>
@include('load.form.password_toggle')
<div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}"> <div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}">
{{ Form::label('password', __('boilerplate::auth.fields.password')) }} {{ Form::label('password', __('boilerplate::auth.fields.password')) }}
{{ Form::input('password', 'password', Request::old('password'), ['class' => 'form-control', 'autofocus']) }} {{ Form::input('password', 'password', Request::old('password'), ['class' => 'form-control', 'autofocus']) }}

View File

@@ -15,9 +15,9 @@
<div class="input-group form-group {{ $errors->has('password') ? 'has-error' : '' }}"> <div class="input-group form-group {{ $errors->has('password') ? 'has-error' : '' }}">
{{ Form::password('password', ['class' => 'form-control', 'placeholder' => __('boilerplate::auth.fields.password'), 'required']) }} {{ Form::password('password', ['class' => 'form-control', 'placeholder' => __('boilerplate::auth.fields.password'), 'required']) }}
<div class="input-group-append"> <div class="input-group-append">
<div class="btn btn-outline-secondary"> <button class="btn btn-outline-secondary password-toggle" type="button" tabindex="-1">
<i class="fa fa-lock"></i> <i class="fa fa-eye"></i>
</div> </button>
</div> </div>
</div> </div>
{!! $errors->first('password', '<p class="text-danger"><strong>:message</strong></p>') !!} {!! $errors->first('password', '<p class="text-danger"><strong>:message</strong></p>') !!}
@@ -46,4 +46,5 @@
--> -->
</div> </div>
</div> </div>
@include('load.form.password_toggle')
{!! Form::close() !!} {!! Form::close() !!}

View File

@@ -68,6 +68,8 @@
</div> </div>
</form> </form>
@include('load.form.password_toggle')
@push('js') @push('js')
<script> <script>
$('#use_for_delivery').click(function() { $('#use_for_delivery').click(function() {

View File

@@ -1,3 +1,5 @@
@include('load.form.password_toggle')
<div class="row mb-3 mt-3"> <div class="row mb-3 mt-3">
<label for="new-password" class="col-md-6 control-label text-right">Mot de passe actuel</label> <label for="new-password" class="col-md-6 control-label text-right">Mot de passe actuel</label>
<div class="col-md-6"> <div class="col-md-6">

View File

@@ -14,6 +14,7 @@
{{ Form::email('email', old('email', $email), ['class' => 'form-control', 'placeholder' => __('boilerplate::auth.fields.email'), 'required', 'autofocus']) }} {{ Form::email('email', old('email', $email), ['class' => 'form-control', 'placeholder' => __('boilerplate::auth.fields.email'), 'required', 'autofocus']) }}
{!! $errors->first('email','<p class="text-danger"><strong>:message</strong></p>') !!} {!! $errors->first('email','<p class="text-danger"><strong>:message</strong></p>') !!}
</div> </div>
@include('load.form.password_toggle')
<div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}"> <div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}">
{{ Form::password('password', ['class' => 'form-control', 'placeholder' => __('boilerplate::auth.fields.password'), 'required']) }} {{ Form::password('password', ['class' => 'form-control', 'placeholder' => __('boilerplate::auth.fields.password'), 'required']) }}
{!! $errors->first('password','<p class="text-danger"><strong>:message</strong></p>') !!} {!! $errors->first('password','<p class="text-danger"><strong>:message</strong></p>') !!}

View File

@@ -7,7 +7,7 @@
@if (!empty($text)) @if (!empty($text))
<div class="row m-0 mb-3"> <div class="row m-0 mb-3">
<div class="col-12 p-3 green-dark" style="font-size: 1.2em;">{!! $text !!}</div> <div class="col-12 p-3 green-dark homepage-text" style="font-size: 1.2em;">{!! $text !!}</div>
</div> </div>
@endif @endif

View File

@@ -0,0 +1,44 @@
@if (!defined('LOAD_PASSWORD_TOGGLE'))
@push('js')
<script>
function initPasswordToggle(context) {
var $ctx = $(context || document);
$ctx.find('input[type="password"]').each(function() {
var $input = $(this);
if ($input.closest('.input-group').find('.password-toggle').length) {
return;
}
if (!$input.parent().hasClass('input-group')) {
$input.wrap('<div class="input-group"></div>');
}
var $btn = $('<div class="input-group-append">' +
'<button class="btn btn-outline-secondary password-toggle" type="button" tabindex="-1">' +
'<i class="fa fa-eye"></i>' +
'</button></div>');
$input.after($btn);
});
}
$(function() {
initPasswordToggle();
$(document).on('shown.bs.modal', function(e) {
initPasswordToggle(e.target);
});
$(document).on('click', '.password-toggle', function() {
var $btn = $(this);
var $input = $btn.closest('.input-group').find('input');
if ($input.attr('type') === 'password') {
$input.attr('type', 'text');
$btn.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
} else {
$input.attr('type', 'password');
$btn.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
}
});
});
</script>
@endpush
@php(define('LOAD_PASSWORD_TOGGLE', true))
@endif