2 Commits

Author SHA1 Message Date
Valentin Lab
2fe818e8b4 fix: make 'Rayons' title adaptable to screen width 2025-12-13 20:55:07 +01:00
Valentin Lab
0ec13e802e new: make the menu visible on mobile 2025-12-13 20:10:42 +01:00
80 changed files with 284 additions and 1756 deletions

View File

@@ -52,23 +52,19 @@ 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.
## Pull Request Guidelines ## Commit & 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, queues). changes, and note any manual follow-up (cron, storage links,
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::getStatusBadge($order->status); return Orders::getStatus($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(['status', 'action']); ->rawColumns(['action']);
return parent::modifier($datatables); return parent::modifier($datatables);
} }

View File

@@ -29,20 +29,18 @@ class OrdersDataTable extends DataTable
{ {
$datatables $datatables
->editColumn('status', function (Order $order) { ->editColumn('status', function (Order $order) {
return Orders::getStatusBadge($order->status); return Orders::getStatus($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 return $order->customer->last_name.' '.$order->customer->first_name;
? $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(['status', 'action']); ->rawColumns(['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::getStatusBadge($tariff['status_id']); return Tariffs::getStatus($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(['status', 'sale_channels2', 'action']); ->rawColumns(['sale_channels2', 'action']);
return parent::modifier($datatables); return parent::modifier($datatables);
} }

View File

@@ -47,16 +47,12 @@ 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])) { return Orders::getStatus($order->status);
return '<span class="badge badge-warning">En attente de règlement</span>';
}
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(['status', 'action']); ->rawColumns(['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::getStatusBadge($order->status); return Orders::getStatus($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(['status', 'action']); ->rawColumns(['action']);
return parent::modifier($datatables); return parent::modifier($datatables);
} }

View File

@@ -26,7 +26,6 @@ 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);
@@ -64,17 +63,6 @@ class ArticleController extends Controller
return view('Admin.Shop.Articles.edit', $data); return view('Admin.Shop.Articles.edit', $data);
} }
public function duplicate($id)
{
$data = Articles::getFull($id);
// Prepare for creation: blank id/slug, tweak name to indicate copy
$data['article']['id'] = null;
$data['article']['slug'] = null;
$data['article']['name'] = ($data['article']['name'] ?? '').' (copie)';
return view('Admin.Shop.Articles.create', $data);
}
public function destroy($id) public function destroy($id)
{ {
return Articles::destroy($id); return Articles::destroy($id);

View File

@@ -4,8 +4,6 @@ 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;
@@ -38,43 +36,6 @@ 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) {
@@ -84,13 +45,7 @@ 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

@@ -68,10 +68,8 @@ class BasketController extends Controller
public function getBasketTotal($deliveryId = false, $deliveryTypeId = false) public function getBasketTotal($deliveryId = false, $deliveryTypeId = false)
{ {
$basket = Baskets::getBasketTotal($deliveryId, $deliveryTypeId);
$data = [ $data = [
'basket' => $basket, 'basket' => Baskets::getBasketTotal($deliveryId, $deliveryTypeId),
'sale_channel' => $basket['sale_channel'] ?? null,
]; ];
return view('Shop.Baskets.partials.basketTotal', $data); return view('Shop.Baskets.partials.basketTotal', $data);

View File

@@ -8,7 +8,6 @@ use App\Repositories\Shop\CustomerAddresses;
use App\Repositories\Shop\Customers; use App\Repositories\Shop\Customers;
use App\Repositories\Shop\Offers; use App\Repositories\Shop\Offers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@@ -118,82 +117,9 @@ class CustomerController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$data = $request->all(); $data = $request->all();
$validator = Validator::make($data, [
'phone' => 'required|max:30',
], [
'phone.required' => __('Le numéro de téléphone est obligatoire.'),
]);
if ($validator->fails()) {
return redirect()->route('Shop.Customers.edit')
->withInput()
->withErrors($validator->errors(), 'registration');
}
$passwordError = $this->handlePasswordChange($request);
if ($passwordError) {
return redirect()->route('Shop.Customers.edit')
->with('growl', [$passwordError, 'danger']);
}
unset($data['current-password'], $data['new-password'], $data['new-password_confirmation']);
$customer = Customers::storeFull($data); $customer = Customers::storeFull($data);
$growl = $request->filled('new-password') return redirect()->route('Shop.Customers.edit');
? [__('Profil et mot de passe mis à jour.'), 'success']
: [__('Profil mis à jour.'), 'success'];
return redirect()->route('Shop.Customers.edit')->with('growl', $growl);
}
protected function handlePasswordChange(Request $request)
{
if (! $request->filled('new-password')) {
return null;
}
$customer = Customers::get(Customers::getId());
if (! $customer) {
return __('Impossible de modifier le mot de passe.');
}
if (! Hash::check($request->input('current-password'), $customer->password)) {
return __('Le mot de passe actuel est incorrect.');
}
if ($request->input('new-password') !== $request->input('new-password_confirmation')) {
return __('Les mots de passe ne correspondent pas.');
}
$newPassword = $request->input('new-password');
if (strlen($newPassword) < 8) {
return __('Le mot de passe doit contenir au moins 8 caractères.');
}
if (! preg_match('/[a-z]/', $newPassword)) {
return __('Le mot de passe doit contenir au moins une lettre minuscule.');
}
if (! preg_match('/[A-Z]/', $newPassword)) {
return __('Le mot de passe doit contenir au moins une lettre majuscule.');
}
if (! preg_match('/[0-9]/', $newPassword)) {
return __('Le mot de passe doit contenir au moins un chiffre.');
}
if (! preg_match('/[^A-Za-z0-9]/', $newPassword)) {
return __('Le mot de passe doit contenir au moins un caractère spécial.');
}
$customer->password = Hash::make($request->input('new-password'));
$customer->save();
return null;
} }
public function storeAddress(Request $request) public function storeAddress(Request $request)
@@ -245,7 +171,6 @@ class CustomerController extends Controller
$html = view('Shop.Customers.partials.address_item', [ $html = view('Shop.Customers.partials.address_item', [
'address' => $address->toArray(), 'address' => $address->toArray(),
'prefix' => $prefix, 'prefix' => $prefix,
'inputName' => $request->input('input_name'),
'with_name' => true, 'with_name' => true,
'selected' => $address->id, 'selected' => $address->id,
])->render(); ])->render();

View File

@@ -9,10 +9,8 @@ use App\Repositories\Core\User\ShopCart;
use App\Repositories\Shop\Baskets; use App\Repositories\Shop\Baskets;
use App\Repositories\Shop\Contents; use App\Repositories\Shop\Contents;
use App\Repositories\Shop\Customers; use App\Repositories\Shop\Customers;
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;
@@ -59,21 +57,8 @@ class OrderController extends Controller
$deliveries = $deliveries ? $deliveries->values() : collect(); $deliveries = $deliveries ? $deliveries->values() : collect();
$customerData = $customer ? $customer->toArray() : false; $customerData = $customer ? $customer->toArray() : false;
if ($customerData) { if ($customerData && $defaultSaleChannelId) {
$customerData['delivery_address_id'] = optional(CustomerAddresses::getDeliveryAddress($customerId))->id; $customerData['default_sale_channel_id'] = $defaultSaleChannelId;
$customerData['invoice_address_id'] = optional(CustomerAddresses::getInvoiceAddress($customerId))->id;
if (! $customerData['delivery_address_id'] && ! empty($customerData['delivery_addresses'])) {
$customerData['delivery_address_id'] = $customerData['delivery_addresses'][0]['id'] ?? null;
}
if (! $customerData['invoice_address_id'] && ! empty($customerData['invoice_addresses'])) {
$customerData['invoice_address_id'] = $customerData['invoice_addresses'][0]['id'] ?? null;
}
if ($defaultSaleChannelId) {
$customerData['default_sale_channel_id'] = $defaultSaleChannelId;
}
} }
$data = [ $data = [
@@ -95,25 +80,6 @@ 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) {
@@ -122,9 +88,7 @@ class OrderController extends Controller
} }
OrderMails::sendOrderConfirmed($order->id); OrderMails::sendOrderConfirmed($order->id);
return redirect()->route('Shop.Orders.confirmed', [ return redirect()->route('Shop.Orders.confirmed');
'payment_type' => $data['payment_type'],
]);
} }
return view('Shop.Orders.order'); return view('Shop.Orders.order');
@@ -133,18 +97,9 @@ class OrderController extends Controller
public function confirmed() public function confirmed()
{ {
ShopCart::clear(); ShopCart::clear();
$paymentType = request('payment_type');
$content = Contents::getOrderConfirmedContent(); $content = Contents::getOrderConfirmedContent();
$paymentLabel = match ($paymentType) {
'2' => 'chèque',
'3' => 'virement',
default => null,
};
return view('Shop.Orders.confirmed', [ return view('Shop.Orders.confirmed', ['content' => $content]);
'content' => $content,
'payment_label' => $paymentLabel,
]);
} }
public function getPdf($uuid) public function getPdf($uuid)

View File

@@ -20,7 +20,6 @@ class RegisterCustomer extends FormRequest
'last_name' => 'required|max:255', 'last_name' => 'required|max:255',
'first_name' => 'required|max:255', 'first_name' => 'required|max:255',
'email' => 'required|email|max:255|unique:shop_customers,email,NULL,id,deleted_at,NULL', 'email' => 'required|email|max:255|unique:shop_customers,email,NULL,id,deleted_at,NULL',
'phone' => 'required|max:30',
'password' => ['required', 'confirmed', new Password()], 'password' => ['required', 'confirmed', new Password()],
]; ];
} }

View File

@@ -1,51 +0,0 @@
<?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,
);
}
}

View File

@@ -1,49 +0,0 @@
<?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

@@ -3,7 +3,6 @@
namespace App\Mail; namespace App\Mail;
use App\Models\Core\Mail\MailTemplate; use App\Models\Core\Mail\MailTemplate;
use App\Repositories\Shop\Orders;
use App\Repositories\Shop\Traits\MailCustomers; use App\Repositories\Shop\Traits\MailCustomers;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
@@ -33,44 +32,29 @@ class ConfirmationCommande extends TemplateMailable
public $facturation_ville; public $facturation_ville;
public $livraison_nom;
public $livraison_adresse; public $livraison_adresse;
public $livraison_adresse2;
public $livraison_cp; public $livraison_cp;
public $livraison_ville; public $livraison_ville;
public $facturation_nom;
public $facturation_adresse2;
public $mode_paiement;
protected static $templateModelClass = MailTemplate::class; protected static $templateModelClass = MailTemplate::class;
public function __construct($order) public function __construct($order)
{ {
$facturation_address = $order->invoice->address; $facturation_address = $order->invoice->address;
$delivery_address = $order->delivery_address; $delivery_address = $order->delivery_address;
$this->prenom = $order->customer->first_name; $this->prenom = $order->customer->first_name;
$this->nom = $order->customer->last_name; $this->nom = $order->customer->last_name;
$this->facturation_nom = $facturation_address->name;
$this->facturation_adresse = $facturation_address->address; $this->facturation_adresse = $facturation_address->address;
$this->facturation_adresse2 = $facturation_address->address2;
$this->facturation_cp = $facturation_address->zipcode; $this->facturation_cp = $facturation_address->zipcode;
$this->facturation_ville = $facturation_address->city; $this->facturation_ville = $facturation_address->city;
$this->livraison_nom = $delivery_address->name;
$this->livraison_adresse = $delivery_address->address; $this->livraison_adresse = $delivery_address->address;
$this->livraison_adresse2 = $delivery_address->address2;
$this->livraison_cp = $delivery_address->zipcode; $this->livraison_cp = $delivery_address->zipcode;
$this->livraison_ville = $delivery_address->city; $this->livraison_ville = $delivery_address->city;
$this->societe = $order->customer->company; $this->societe = $order->customer->company;
$this->email = $order->customer->email; $this->email = $order->customer->email;
$this->numero_commande = $order->ref; $this->numero_commande = $order->ref;
$this->date_commande = $order->created_at; $this->date_commande = $order->created_at;
$this->mode_paiement = Orders::getPaymentType($order->payment_type);
} }
} }

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->visible()->whereHas('offers', function ($query) use ($saleChannelId) { return $query->whereHas('offers', function ($query) use ($saleChannelId) {
$query->active()->byStockAvailable(); $query->active()->byStockAvailable();
if ($saleChannelId) { if ($saleChannelId) {

View File

@@ -92,11 +92,6 @@ 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,15 +67,17 @@ 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':
return $category->insertAfterNode($category_target); $category->afterNode($category_target);
break;
case 'inside': case 'inside':
return $category_target->prependNode($category); $category_target->appendNode($category);
break;
default: default:
return $category->insertAfterNode($category_target); $category->afterNode($category_target);
} }
return $category->save();
} }
public static function create($data) public static function create($data)

View File

@@ -31,11 +31,7 @@ class ArticleImages
public static function getFullImagesByArticle($article) public static function getFullImagesByArticle($article)
{ {
if (! $article) { $images = count($article->images) ? $article->images : collect([]);
return collect([]);
}
$images = count($article->images ?? []) ? $article->images : collect([]);
switch ($article->product_type) { switch ($article->product_type) {
case 'App\Models\Botanic\Variety': case 'App\Models\Botanic\Variety':
$variety = $article->product ?? false; $variety = $article->product ?? false;

View File

@@ -43,7 +43,6 @@ 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,
]; ];
} }
@@ -67,11 +66,6 @@ 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();
@@ -184,18 +178,10 @@ class Articles
$articles = self::getArticlesWithOffers($options); $articles = self::getArticlesWithOffers($options);
$searchOrder = $options['ids'] ?? false ? array_flip($options['ids']->toArray()) : false; $searchOrder = $options['ids'] ?? false ? array_flip($options['ids']->toArray()) : false;
foreach ($articles as $article) { foreach ($articles as $article) {
// Skip articles without an offer/tariff/price list for the resolved sale channel
if (!isset($article->offers[0]) || ! $article->offers[0]->tariff) {
continue;
}
$price_lists = $article->offers[0]->tariff->price_lists->toArray(); $price_lists = $article->offers[0]->tariff->price_lists->toArray();
if (! count($price_lists)) { if (! count($price_lists)) {
continue; continue;
} }
if (empty($price_lists[0]['price_list_values'][0] ?? null)) {
continue;
}
if (! is_array($data[$article->name] ?? false)) { if (! is_array($data[$article->name] ?? false)) {
$data[$article->name] = self::getDataForSale($article); $data[$article->name] = self::getDataForSale($article);
if ($searchOrder) { if ($searchOrder) {
@@ -210,7 +196,7 @@ class Articles
ksort($data); ksort($data);
} }
return $data ?? []; return $data ?? false;
} }
public static function getDataForSale($article) public static function getDataForSale($article)
@@ -317,6 +303,8 @@ 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,7 +116,6 @@ 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,
]; ];
} }
@@ -145,13 +144,6 @@ 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

@@ -41,16 +41,6 @@ class Contents
return self::get(5)->text ?? 'Votre commande a été confirmée'; return self::get(5)->text ?? 'Votre commande a été confirmée';
} }
public static function getOrderConfirmedByCheckContent()
{
return self::get(10)->text ?? 'Votre commande a bien été enregistrée, elle vous sera expédiée dès réception de votre chèque.';
}
public static function getOrderConfirmedByWireContent()
{
return self::get(11)->text ?? 'Votre commande a bien été enregistrée, elle vous sera expédiée dès réception de votre virement.';
}
public static function getPayboxConfirmedContent() public static function getPayboxConfirmedContent()
{ {
return self::get(6)->text ?? 'Merci pour votre règlement. Votre commande sera traitée sous peu.'; return self::get(6)->text ?? 'Merci pour votre règlement. Votre commande sera traitée sous peu.';

View File

@@ -19,7 +19,7 @@ class InvoicePDF
$invoice = Invoices::getFull($id); $invoice = Invoices::getFull($id);
$customFields = []; $customFields = [];
if ($orderRef = optional($invoice->order)->ref) { if ($orderRef = optional($invoice->order)->ref) {
$customFields['Numéro de commande'] = $orderRef; $customFields['order number'] = $orderRef;
} }
$customer = new Party([ $customer = new Party([
@@ -61,7 +61,7 @@ class InvoicePDF
trim(($address->zipcode ?? '').' '.($address->city ?? '')), trim(($address->zipcode ?? '').' '.($address->city ?? '')),
]); ]);
return implode("\n", $lines); return implode('<br>', $lines);
} }
public static function makeItems($details) public static function makeItems($details)

View File

@@ -2,108 +2,19 @@
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;
} }
$saved = $offer->save(); return $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,11 +16,7 @@ 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,11 +3,9 @@
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
{ {
@@ -39,25 +37,4 @@ 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::notCancelled()->ofToday()->count(); return Order::ofToday()->count();
} }
public static function countOfLastWeek() public static function countOfLastWeek()
{ {
return Order::notCancelled()->ofLastWeek()->count(); return Order::ofLastWeek()->count();
} }
public static function countOfLastMonth() public static function countOfLastMonth()
{ {
return Order::notCancelled()->ofLastMonth()->count(); return Order::ofLastMonth()->count();
} }
public static function getTotalOfToday() public static function getTotalOfToday()
{ {
return Order::notCancelled()->ofToday()->sum('total_taxed'); return Order::ofToday()->sum('total_taxed');
} }
public static function getTotalOfLastWeek() public static function getTotalOfLastWeek()
{ {
return Order::notCancelled()->ofLastWeek()->sum('total_taxed'); return Order::ofLastWeek()->sum('total_taxed');
} }
public static function getTotalOfLastMonth() public static function getTotalOfLastMonth()
{ {
return Order::notCancelled()->ofLastMonth()->sum('total_taxed'); return Order::ofLastMonth()->sum('total_taxed');
} }
public static function getModel() public static function getModel()
{ {
return Order::notCancelled(); return Order::query();
} }
} }

View File

@@ -126,26 +126,6 @@ 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());
@@ -155,7 +135,7 @@ class Orders
public static function statuses() public static function statuses()
{ {
return ['En attente', 'Préparation', 'Expédié', 'Livré', 'Annulé']; return ['En attente', 'Préparation', 'Expédié', 'Livré'];
} }
public static function getPaymentType($id) public static function getPaymentType($id)

View File

@@ -104,9 +104,7 @@ class Paybox
return true; return true;
} }
$isCancelled = (int) $order->status === 4; DB::transaction(function () use ($invoice, $order, $reference, $payload, $existingPayment, &$shouldNotify) {
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,
@@ -136,24 +134,14 @@ class Paybox
Invoices::checkPayments($invoice->id); Invoices::checkPayments($invoice->id);
if (! $isCancelled) { $paidStatus = Orders::getStatusByName('Préparation');
$paidStatus = Orders::getStatusByName('Préparation'); if ($paidStatus !== '' && (int) $order->status !== (int) $paidStatus) {
if ($paidStatus !== '' && (int) $order->status !== (int) $paidStatus) { $order->status = $paidStatus;
$order->status = $paidStatus; $order->save();
$order->save();
}
} }
}); });
if ($isCancelled && $shouldNotify) { if ($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,30 +50,11 @@ 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

@@ -1,36 +0,0 @@
<?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('shop_contents')->insert([
[
'id' => 10,
'text' => '<p>Votre commande a bien été enregistrée, elle vous sera expédiée dès réception de votre chèque.</p><p class="mt-3 text-warning"><i class="fa fa-exclamation-triangle mr-1"></i> Sans réception de votre paiement au bout de 30 jours, votre commande sera annulée.</p>',
'created_at' => now(),
'updated_at' => now(),
],
[
'id' => 11,
'text' => '<p>Votre commande a bien été enregistrée, elle vous sera expédiée dès réception de votre virement.</p><p class="mt-3 text-warning"><i class="fa fa-exclamation-triangle mr-1"></i> Sans réception de votre paiement au bout de 30 jours, votre commande sera annulée.</p>',
'created_at' => now(),
'updated_at' => now(),
],
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('shop_contents')->whereIn('id', [10, 11])->delete();
}
};

View File

@@ -1,87 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$this->transformTemplate(function ($html) {
// Replace hardcoded "Carte de crédit" with the template variable
$html = str_replace(
'Carte de cr&eacute;dit',
'{{mode_paiement}}',
$html
);
// Add address2 to delivery address
$html = str_replace(
'{{livraison_adresse}}<br />{{livraison_cp}} {{livraison_ville}}',
'{{livraison_adresse}}{{#livraison_adresse2}}<br />{{livraison_adresse2}}{{/livraison_adresse2}}<br />{{livraison_cp}} {{livraison_ville}}',
$html
);
// Add address2 to billing address
$html = str_replace(
'{{facturation_adresse}}<br />{{facturation_cp}} {{facturation_ville}}',
'{{facturation_adresse}}{{#facturation_adresse2}}<br />{{facturation_adresse2}}{{/facturation_adresse2}}<br />{{facturation_cp}} {{facturation_ville}}',
$html
);
return $html;
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$this->transformTemplate(function ($html) {
$html = str_replace(
'{{mode_paiement}}',
'Carte de cr&eacute;dit',
$html
);
$html = str_replace(
'{{livraison_adresse}}{{#livraison_adresse2}}<br />{{livraison_adresse2}}{{/livraison_adresse2}}<br />{{livraison_cp}} {{livraison_ville}}',
'{{livraison_adresse}}<br />{{livraison_cp}} {{livraison_ville}}',
$html
);
$html = str_replace(
'{{facturation_adresse}}{{#facturation_adresse2}}<br />{{facturation_adresse2}}{{/facturation_adresse2}}<br />{{facturation_cp}} {{facturation_ville}}',
'{{facturation_adresse}}<br />{{facturation_cp}} {{facturation_ville}}',
$html
);
return $html;
});
}
private function transformTemplate(callable $transform): void
{
$template = DB::table('mail_templates')
->where('mailable', 'App\\Mail\\ConfirmationCommande')
->first();
if (! $template) {
return;
}
$translations = json_decode($template->html_template, true);
foreach ($translations as $lang => $html) {
$translations[$lang] = $transform($html);
}
DB::table('mail_templates')
->where('id', $template->id)
->update(['html_template' => json_encode($translations, JSON_UNESCAPED_UNICODE)]);
}
};

View File

@@ -1,74 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$this->transformTemplate(function ($html) {
// Add name before delivery address
$html = str_replace(
'{{livraison_adresse}}',
'{{#livraison_nom}}{{livraison_nom}}<br />{{/livraison_nom}}{{livraison_adresse}}',
$html
);
// Add name before billing address
$html = str_replace(
'{{facturation_adresse}}',
'{{#facturation_nom}}{{facturation_nom}}<br />{{/facturation_nom}}{{facturation_adresse}}',
$html
);
return $html;
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$this->transformTemplate(function ($html) {
$html = str_replace(
'{{#livraison_nom}}{{livraison_nom}}<br />{{/livraison_nom}}{{livraison_adresse}}',
'{{livraison_adresse}}',
$html
);
$html = str_replace(
'{{#facturation_nom}}{{facturation_nom}}<br />{{/facturation_nom}}{{facturation_adresse}}',
'{{facturation_adresse}}',
$html
);
return $html;
});
}
private function transformTemplate(callable $transform): void
{
$template = DB::table('mail_templates')
->where('mailable', 'App\\Mail\\ConfirmationCommande')
->first();
if (! $template) {
return;
}
$translations = json_decode($template->html_template, true);
foreach ($translations as $lang => $html) {
$translations[$lang] = $transform($html);
}
DB::table('mail_templates')
->where('id', $template->id)
->update(['html_template' => json_encode($translations, JSON_UNESCAPED_UNICODE)]);
}
};

View File

@@ -1,56 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private $oldSrc = '/storage/photos/shares/logo.png';
private $newSrc = 'https://boutique.jardinenvie.com/img/logo.png';
/**
* Run the migrations.
*/
public function up(): void
{
$this->replaceLogoInAllTemplates($this->oldSrc, $this->newSrc);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$this->replaceLogoInAllTemplates($this->newSrc, $this->oldSrc);
}
private function replaceLogoInAllTemplates(string $from, string $to): void
{
$templates = DB::table('mail_templates')->get();
foreach ($templates as $template) {
$translations = json_decode($template->html_template, true);
if (! $translations) {
continue;
}
$changed = false;
foreach ($translations as $lang => $html) {
$updated = str_replace($from, $to, $html);
if ($updated !== $html) {
$translations[$lang] = $updated;
$changed = true;
}
}
if ($changed) {
DB::table('mail_templates')
->where('id', $template->id)
->update(['html_template' => json_encode($translations, JSON_UNESCAPED_UNICODE)]);
}
}
}
};

View File

@@ -1,75 +0,0 @@
<?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

@@ -1,86 +0,0 @@
<?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

@@ -1,125 +0,0 @@
<?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

@@ -1,9 +0,0 @@
<?php
return [
'confirmdelete' => 'Do you confirm the deletion?',
'deletesuccess' => 'Deleted successfully.',
'mail_the_selection' => 'Mail the selection',
'mail_the_complete_list' => 'Mail the complete list',
];

View File

@@ -1,9 +0,0 @@
<?php
return [
'confirmdelete' => 'Confirmez-vous la suppression ?',
'deletesuccess' => 'La suppression a été effectuée.',
'mail_the_selection' => 'Envoyer la sélection',
'mail_the_complete_list' => 'Envoyer toute la liste',
];

View File

@@ -112,33 +112,3 @@ body {
.bg-darker { .bg-darker {
background-color: rgba(0,0,0,0.05)!important; background-color: rgba(0,0,0,0.05)!important;
} }
/* Header action buttons aligned with page title */
.content-header .form-buttons {
margin-left: 12px;
}
.content-header .form-buttons .btn {
height: 32px;
display: inline-flex;
align-items: center;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1.1;
}
@media (max-width: 575.98px) {
.content-header .form-buttons {
margin-left: 0;
margin-top: 8px;
}
.content-header .form-buttons .btn {
height: 28px;
padding-top: 2px;
padding-bottom: 2px;
padding-left: 8px;
padding-right: 8px;
font-size: 0.75rem;
}
}

View File

@@ -397,11 +397,6 @@ div.megamenu ul.megamenu li.megamenu.level1
} }
@media (max-width: 991.98px){ @media (max-width: 991.98px){
#navbarContentMobile {
max-height: calc(100vh - 60px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
#navbarContentMobile .navbar-nav { #navbarContentMobile .navbar-nav {
flex-direction: column; flex-direction: column;
} }
@@ -436,63 +431,3 @@ 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

@@ -5,7 +5,5 @@
]) ])
@section('content') @section('content')
@include('Admin.Shop.Articles.form', [ @include('Admin.Shop.Articles.form')
'cancel_url' => route('Admin.Shop.Articles.index'),
])
@endsection @endsection

View File

@@ -5,13 +5,5 @@
]) ])
@section('content') @section('content')
@php @include('Admin.Shop.Articles.form')
$duplicateUrl = \Route::has('Admin.Shop.Articles.duplicate')
? route('Admin.Shop.Articles.duplicate', $article['id'] ?? null)
: null;
@endphp
@include('Admin.Shop.Articles.form', [
'duplicate_url' => $duplicateUrl,
'cancel_url' => route('Admin.Shop.Articles.index'),
])
@endsection @endsection

View File

@@ -5,29 +5,10 @@
'files' => true, 'files' => true,
]) }} ]) }}
<input type="hidden" name="id" id="id" value="{{ $article['id'] ?? null }}"> <input type="hidden" name="id" id="id" value="{{ $article['id'] ?? null }}">
@php
$articlePublicUrl = null;
if (!empty($article['slug'] ?? null)) {
$articlePublicUrl = route('Shop.Articles.slug', ['slug' => $article['slug']]);
} elseif (!empty($article['id'] ?? null)) {
$articlePublicUrl = route('Shop.Articles.show', ['id' => $article['id']]);
}
@endphp
@if ($articlePublicUrl)
<div class="d-flex justify-content-end mb-3">
<a href="{{ $articlePublicUrl }}" class="btn btn-outline-primary" target="_blank" rel="noopener">
Voir la page publique
<i class="fa fa-external-link"></i>
</a>
</div>
@endif
@include('Admin.Shop.Articles.partials.characteristics') @include('Admin.Shop.Articles.partials.characteristics')
{{ Form::close() }} {{ Form::close() }}
<x-save :cancel-url="$cancel_url ?? null" :duplicate-url="$duplicate_url ?? null" /> <x-save />
@include('load.form.appender') @include('load.form.appender')
@include('load.form.editor') @include('load.form.editor')

View File

@@ -2,7 +2,7 @@
@component('components.layout.box-collapse', [ @component('components.layout.box-collapse', [
'id' => 'product_description_box', 'id' => 'product_description_box',
'title' => 'Informations héritées', 'title' => 'Informations héritées',
'collapsed' => $collapsed ?? true, 'collapsed' => $collapsed ?? false,
]) ])
@foreach ($article['inherited'] as $inherited) @foreach ($article['inherited'] as $inherited)
@component('components.card', ['title' => $inherited['name'], 'class' => 'mb-3']) @component('components.card', ['title' => $inherited['name'], 'class' => 'mb-3'])

View File

@@ -1,12 +1,3 @@
@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,30 +14,57 @@
@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) {
var position = e.move_info.position;
var target_node = e.move_info.target_node;
var moved_node = e.move_info.moved_node;
$.ajax({ $tree.on('tree.move', function(e) {
method: "POST", // e.preventDefault();
url: "{{ route('Admin.Shop.Categories.moveTree') }}",
data: { position = e.move_info.position;
node_id: moved_node.id, target_node = e.move_info.target_node;
type: position,
target_id: target_node.id function getNewParentNode() {
} if (position == 'inside') {
}); return target_node;
}
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' => 'homepage-form', 'autocomplete' => 'off']) }} {{ Form::open(['route' => 'Admin.Shop.Contents.store', 'id' => 'content-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,21 +25,13 @@
@foreach ($lastOrders as $order) @foreach ($lastOrders as $order)
<tr> <tr>
<td> <td>
@if ($order->customer) <a href="{{ route('Admin.Shop.Customers.edit', ['id' => $order->customer->id]) }}"
<a href="{{ route('Admin.Shop.Customers.edit', ['id' => $order->customer->id]) }}" class="alert-link green">
class="alert-link green"> {{ $order->customer->first_name }}
{{ $order->customer->first_name }} {{ $order->customer->last_name }}
{{ $order->customer->last_name }} </a>
</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

@@ -1,79 +1,49 @@
{{ Form::open(['route' => 'Admin.Shop.Offers.store', 'id' => 'offer-form', 'autocomplete' => 'off']) }} {{ Form::open(['route' => 'Admin.Shop.Offers.store', 'id' => 'offer-form', 'autocomplete' => 'off']) }}
<input type="hidden" name="id" value="{{ $offer['id'] ?? false }}"> <input type="hidden" name="id" value="{{ $offer['id'] ?? false }}">
@if (($offer['id'] ?? false) && ($offer['article_id'] ?? false))
<div class="d-flex justify-content-end mb-3">
<a href="{{ route('Shop.Articles.show', ['id' => $offer['article_id']]) }}" class="btn btn-outline-primary" target="_blank" rel="noopener">
Voir la page publique de l'article
<i class="fa fa-external-link"></i>
</a>
</div>
@endif
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-8">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-12">
<div class="d-flex align-items-end"> @include('components.form.select', [
<div class="flex-grow-1"> 'name' => 'article_id',
@include('components.form.select', [ 'id_name' => 'article_id',
'name' => 'article_id', 'list' => $articles ?? null,
'id_name' => 'article_id', 'value' => $offer['article_id'] ?? null,
'list' => $articles ?? null, 'with_empty' => '',
'value' => $offer['article_id'] ?? null, 'class' => 'select2 select_article',
'with_empty' => '', 'label' => 'Article',
'class' => 'select2 select_article', 'required' => true,
'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">
<div class="d-flex align-items-end"> @include('components.form.select', [
<div class="flex-grow-1"> 'name' => 'variation_id',
@include('components.form.select', [ 'id_name' => 'variation_id',
'name' => 'variation_id', 'list' => $variations ?? null,
'id_name' => 'variation_id', 'value' => $offer['variation_id'] ?? null,
'list' => $variations ?? null, 'with_empty' => '',
'value' => $offer['variation_id'] ?? null, 'class' => 'select2 select_variation',
'with_empty' => '', 'label' => __('shop.packages.name'),
'class' => 'select2 select_variation', 'required' => true,
'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">
<div class="d-flex align-items-end"> @include('components.form.select', [
<div class="flex-grow-1"> 'name' => 'tariff_id',
@include('components.form.select', [ 'id_name' => 'tariff_id',
'name' => 'tariff_id', 'list' => $tariffs ?? null,
'id_name' => 'tariff_id', 'value' => $offer['tariff_id'] ?? null,
'list' => $tariffs ?? null, 'with_empty' => '',
'value' => $offer['tariff_id'] ?? null, 'class' => 'select2 select_tariffs',
'with_empty' => '', 'label' => 'Tarif',
'class' => 'select2 select_tariffs', 'required' => true,
'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-2"> <div class="col-4">
@include('components.form.input', [ @include('components.form.input', [
'name' => 'weight', 'name' => 'weight',
'value' => $offer['weight'] ?? null, 'value' => $offer['weight'] ?? null,
@@ -81,15 +51,6 @@
'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'])
@@ -135,6 +96,13 @@
</div> </div>
@endcomponent @endcomponent
</div> </div>
<div class="col-4">
@component('components.card', ['title' => 'Previsualisation'])
<div id="preview-article"></div>
<div id="preview-variation"></div>
<div id="preview-tariff"></div>
@endcomponent
</div>
</div> </div>
@@ -149,83 +117,59 @@
{!! JsValidator::formRequest('App\Http\Requests\Admin\Shop\StoreOfferPost', '#offer-form') !!} {!! JsValidator::formRequest('App\Http\Requests\Admin\Shop\StoreOfferPost', '#offer-form') !!}
<script> <script>
function handleArticle() {
$('.select_article').change(function() {
previewArticle($(this).val());
})
}
function previewArticle(id) {
var url = '{{ route('Admin.Shop.Offers.previewArticle') }}/' + id;
$('#preview-article').load(url, function() {
initChevron();
});
}
function handleVariation() {
$('.select_variation').change(function() {
previewVariation($(this).val());
})
}
function previewVariation(id) {
var url = '{{ route('Admin.Shop.Offers.previewVariation') }}/' + id;
$('#preview-variation').load(url, function() {
initChevron();
});
}
function handleTariff() {
$('.select_tariffs').change(function() {
previewTariff($(this).val());
})
}
function previewTariff(id) {
var url = '{{ route('Admin.Shop.Offers.previewTariff') }}/' + id;
$('#preview-tariff').load(url, function() {
initChevron();
});
}
function initPreview() {
previewArticle("{{ $offer['article_id'] ?? null }}");
previewVariation("{{ $offer['variation_id'] ?? null }}");
previewTariff("{{ $offer['tariff_id'] ?? null }}");
}
handleArticle();
handleVariation();
handleTariff();
initChevron(); initChevron();
initSaveForm('#offer-form'); initSaveForm('#offer-form');
initSelect2(); initSelect2();
$('#status_id').bootstrapToggle(); @if ($offer['id'] ?? false)
initPreview();
// Article visibility badges in select2 @endif
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,8 +9,7 @@
<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>
@include('load.form.password_toggle') <div class="modal-body">
<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,6 +1,9 @@
@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'] ?? false) || !array_filter($article['offers']); $hasNoOffers = empty($article['offers']['semences'] ?? false)
&& 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
@@ -16,16 +19,41 @@
</div> </div>
@else @else
{{-- Display normal offers for visible articles with available offers --}} {{-- Display normal offers for visible articles with available offers --}}
@foreach ($article['offers'] as $natureKey => $natureOffers) @if ($article['offers']['semences'] ?? false)
@if (!empty($natureOffers)) @include('Shop.Articles.partials.addBasket', [
@include('Shop.Articles.partials.addBasket', [ 'data' => $article['offers']['semences'],
'data' => $natureOffers, 'title' => 'Semences',
'title' => ucfirst($natureKey), 'model' => 'semences',
'model' => $natureKey, 'bgClass' => 'bg-green-light',
'bgClass' => 'bg-green-light', ])
]) @endif
@endif
@endforeach @if ($article['offers']['plants'] ?? false)
@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,9 +18,8 @@
'name' => 'quantity', 'name' => 'quantity',
'class' => 'quantity', 'class' => 'quantity',
'id_name' => $model . '-quantity', 'id_name' => $model . '-quantity',
'value' => 1, 'value' => (int) $data[0]['prices'][0]['quantity'],
'min' => 1, 'min' => $data[0]['prices'][0]['quantity'],
'max' => $data[0]['stock'] ?? false,
'step' => 1, 'step' => 1,
]) ])
</div> </div>
@@ -45,35 +44,10 @@
@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,8 +19,9 @@
</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>
@@ -49,16 +50,7 @@
<div class="col-lg-3 col-xs-12"> <div class="col-lg-3 col-xs-12">
@if (auth('web')->check() && !empty($article['available_sale_channels'])) @if (auth('web')->check() && !empty($article['available_sale_channels']))
<div id="article-admin-offers" class="alert alert-info p-2 mb-3"> <div id="article-admin-offers" class="alert alert-info p-2 mb-3">
<div class="d-flex justify-content-between align-items-center"> <strong class="d-block">Offres :</strong>
<strong class="d-block mb-0">Offres :</strong>
<a href="{{ route('Admin.Shop.Articles.edit', $article['id']) }}" class="text-dark d-inline-flex align-items-center gap-1" style="font-size: 0.95rem;" title="Ouvrir la fiche article en admin" target="_blank" rel="noopener">
<svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9" />
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" />
</svg>
<span class="sr-only">Éditer l'article</span>
</a>
</div>
<ul class="list-unstyled mb-0 small"> <ul class="list-unstyled mb-0 small">
@php @php
$currentSaleChannelId = $article['current_sale_channel']['id'] ?? null; $currentSaleChannelId = $article['current_sale_channel']['id'] ?? null;
@@ -94,7 +86,7 @@
$tariffId = $channel['tariff_id'] ?? null; $tariffId = $channel['tariff_id'] ?? null;
@endphp @endphp
@if ($tariffId) @if ($tariffId)
<a href="{{ route('Admin.Shop.Tariffs.edit', $tariffId) }}" target="_blank" rel="noopener" title="Ouvrir le tarif" class="ml-2 text-nowrap text-right {{ $nameClass }} text-decoration-none text-reset d-inline-block admin-link-group admin-price-link"> <a href="{{ route('Admin.Shop.Tariffs.edit', $tariffId) }}" target="_blank" rel="noopener" class="ml-2 text-nowrap text-right {{ $nameClass }} text-decoration-none text-reset d-inline-block admin-link-group admin-price-link">
{{ number_format($priceTaxed, 2, ',', ' ') }} TTC {{ number_format($priceTaxed, 2, ',', ' ') }} TTC
@if (! empty($quantity)) @if (! empty($quantity))
<span class="d-block text-muted" style="font-size: 0.85em;">Qté min. {{ $quantity }}</span> <span class="d-block text-muted" style="font-size: 0.85em;">Qté min. {{ $quantity }}</span>
@@ -121,7 +113,7 @@
$stockClass = $offer['stock_current'] > 0 ? 'text-success' : 'text-danger'; $stockClass = $offer['stock_current'] > 0 ? 'text-success' : 'text-danger';
@endphp @endphp
<li class="small {{ $offerClass }}" style="font-size: 0.85em;"> <li class="small {{ $offerClass }}" style="font-size: 0.85em;">
<a href="{{ route('Admin.Shop.Offers.edit', $offer['id']) }}" target="_blank" rel="noopener" title="Ouvrir l'offre" class="text-decoration-none {{ $offerClass }} admin-link-group admin-offer-link"> <a href="{{ route('Admin.Shop.Offers.edit', $offer['id']) }}" target="_blank" rel="noopener" class="text-decoration-none {{ $offerClass }} admin-link-group admin-offer-link">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div> <div>
<span style="opacity: 0.5;">{{ $isSelectedOffer ? '▸' : '○' }}</span> <span style="opacity: 0.5;">{{ $isSelectedOffer ? '▸' : '○' }}</span>

View File

@@ -53,12 +53,7 @@
$('.basket-quantity').change(function() { $('.basket-quantity').change(function() {
var offer_id = $(this).data('id'); var offer_id = $(this).data('id');
var quantity = parseInt($(this).val()) || 1; var quantity = $(this).val();
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,8 +18,6 @@
'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

@@ -1,7 +1,7 @@
<div class="row mt-3 address-row" data-address-id="{{ $address['id'] }}"> <div class="row mt-3 address-row" data-address-id="{{ $address['id'] }}">
<div class="col-1"> <div class="col-1">
@php @php
$inputName = $inputName ?? (isset($prefix) && $prefix ? $prefix.'[address_id]' : 'address_id'); $inputName = isset($prefix) && $prefix ? $prefix.'[address_id]' : 'address_id';
$currentValue = $selected ?? null; $currentValue = $selected ?? null;
@endphp @endphp
<x-form.radios.icheck name="{{ $inputName }}" val="{{ $address['id'] }}" <x-form.radios.icheck name="{{ $inputName }}" val="{{ $address['id'] }}"

View File

@@ -3,7 +3,6 @@
@include('Shop.Customers.partials.address_item', [ @include('Shop.Customers.partials.address_item', [
'address' => $address, 'address' => $address,
'prefix' => $prefix ?? null, 'prefix' => $prefix ?? null,
'inputName' => $inputName ?? null,
'with_name' => $with_name ?? false, 'with_name' => $with_name ?? false,
'selected' => $selected ?? null, 'selected' => $selected ?? null,
]) ])
@@ -45,7 +44,6 @@
<script> <script>
(function() { (function() {
var prefix = '{{ $prefix }}'; var prefix = '{{ $prefix }}';
var inputName = '{{ $inputName ?? '' }}';
var $formContainer = $('#add_address_container_{{ $prefix }}'); var $formContainer = $('#add_address_container_{{ $prefix }}');
var $list = $('#addresses_list_{{ $prefix }}'); var $list = $('#addresses_list_{{ $prefix }}');
var storeUrl = '{{ route('Shop.Customers.address.store') }}'; var storeUrl = '{{ route('Shop.Customers.address.store') }}';
@@ -71,7 +69,7 @@
$.ajax({ $.ajax({
url: storeUrl, url: storeUrl,
method: 'POST', method: 'POST',
data: data + '&prefix=' + prefix + (inputName ? '&input_name=' + inputName : ''), data: data + '&prefix=' + prefix,
success: function(response) { success: function(response) {
if (response.html) { if (response.html) {
$list.append(response.html); $list.append(response.html);

View File

@@ -57,7 +57,6 @@
'name' => 'phone', 'name' => 'phone',
'value' => $customer['phone'] ?? (old('phone') ?? ''), 'value' => $customer['phone'] ?? (old('phone') ?? ''),
'label' => 'Téléphone', 'label' => 'Téléphone',
'required' => true,
]) ])
</div> </div>
</div> </div>

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 shelve-title">{{ $shelve['name'] }}</h1> <h1 class="p-2 green" style="font-size: 2em;">{{ $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 shelve-btn"> class="mt-2 mr-2 btn btn-green-dark">
Découvrir<span class="shelve-btn-suffix"> la sélection</span> Découvrir la sélection
</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 shelve-slide"> <div class="text-center pr-2 pl-2">
<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 class="shelve-article-label"> <div style="height: 48px;">
{{ $name }} {{ $name }}
</div> </div>
</a> </a>

View File

@@ -4,20 +4,8 @@
@section('content') @section('content')
<div class="row"> <div class="row">
<div class="col-12 text-center py-5"> <div class="col-12">
<i class="fa fa-check-circle text-success" style="font-size: 5rem;"></i> {!! $content !!}
<div class="mt-4" style="font-size: 1.2rem;">
{!! $content !!}
</div>
@if($payment_label ?? false)
<div class="mt-3" style="font-size: 1.1rem;">
Votre commande a bien été enregistrée, elle vous sera expédiée dès réception de votre {{ $payment_label }}.
</div>
<div class="mt-3" style="font-size: 1.1rem;">
<i class="fa fa-exclamation-triangle text-warning mr-1"></i>
Sans réception de votre paiement au bout de 30 jours, votre commande sera annulée.
</div>
@endif
</div> </div>
</div> </div>
@endsection @endsection

View File

@@ -40,10 +40,7 @@
<div class="col-sm-12 col-lg-4"> <div class="col-sm-12 col-lg-4">
<x-card class='shadow'> <x-card class='shadow'>
<div id="basketTotal"> <div id="basketTotal">
@include('Shop.Baskets.partials.basketTotal', [ @include('Shop.Baskets.partials.basketTotal', ['basket' => $basket])
'basket' => $basket,
'sale_channel' => $basket['sale_channel'] ?? null,
])
</div> </div>
</x-card> </x-card>
</div> </div>

View File

@@ -1,30 +1,26 @@
<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.Orders.partials.addresses', [
'addresses' => $customer['invoice_addresses'] ?? false,
'prefix' => 'invoice',
'name' => 'invoice[invoice_address_id]',
])
</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>
<x-layout.collapse id="delivery_addresses" title="Adresse de livraison" class="rounded-lg mb-3 d-none" <x-layout.collapse id="delivery_addresses" title="Adresse de livraison" class="rounded-lg mb-3 d-none"
uncollapsed=true> uncollapsed=true>
@include('Shop.Customers.partials.addresses', [ @include('Shop.Orders.partials.addresses', [
'addresses' => $customer['delivery_addresses'] ?? [], 'addresses' => $customer['delivery_addresses'] ?? false,
'prefix' => 'deliveries', 'prefix' => 'delivery',
'inputName' => 'delivery_address_id', 'name' => 'delivery_address_id',
'with_name' => true,
'selected' => $customer['delivery_address_id'] ?? null,
]) ])
@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,7 +10,6 @@
<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">
<button class="btn btn-outline-secondary password-toggle" type="button" tabindex="-1"> <div class="btn btn-outline-secondary">
<i class="fa fa-eye"></i> <i class="fa fa-lock"></i>
</button> </div>
</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,5 +46,4 @@
--> -->
</div> </div>
</div> </div>
@include('load.form.password_toggle')
{!! Form::close() !!} {!! Form::close() !!}

View File

@@ -1,62 +0,0 @@
@php
$passwordInputId = $passwordInputId ?? 'password';
@endphp
<ul class="list-unstyled small mt-1 mb-0 password-rules" data-input="#{{ $passwordInputId }}" style="display: none;">
<li data-rule="length"><i class="fa fa-fw fa-times"></i> Au moins 8 caractères</li>
<li data-rule="lowercase"><i class="fa fa-fw fa-times"></i> Au moins une lettre minuscule</li>
<li data-rule="uppercase"><i class="fa fa-fw fa-times"></i> Au moins une lettre majuscule</li>
<li data-rule="number"><i class="fa fa-fw fa-times"></i> Au moins un chiffre</li>
<li data-rule="special"><i class="fa fa-fw fa-times"></i> Au moins un caractère spécial</li>
</ul>
@once
@push('css')
<style>
.password-rules li {
color: #dc3545;
transition: all 0.3s;
}
.password-rules li.valid {
display: none;
}
</style>
@endpush
@push('js')
<script>
$(function() {
$('.password-rules').each(function() {
var $rules = $(this);
var inputSelector = $rules.data('input');
var $input = $(inputSelector);
if (!$input.length) return;
var checks = {
length: function(v) { return v.length >= 8; },
lowercase: function(v) { return /[a-z]/.test(v); },
uppercase: function(v) { return /[A-Z]/.test(v); },
number: function(v) { return /[0-9]/.test(v); },
special: function(v) { return /[^A-Za-z0-9]/.test(v); }
};
$input.on('input keyup', function() {
var val = $(this).val();
if (val.length === 0) {
$rules.hide();
return;
}
$rules.find('li').each(function() {
var rule = $(this).data('rule');
if (checks[rule]) {
$(this).toggleClass('valid', checks[rule](val));
}
});
$rules.show();
});
});
});
</script>
@endpush
@endonce

View File

@@ -11,12 +11,10 @@
<label>Mot de passe *</label> <label>Mot de passe *</label>
{{ Form::password('password', [ {{ Form::password('password', [
'class' => 'form-control', 'class' => 'form-control',
'id' => 'password',
'placeholder' => __('boilerplate::auth.fields.password'), 'placeholder' => __('boilerplate::auth.fields.password'),
'required', 'required',
]) }} ]) }}
{!! $errors->registration->first('password', '<p class="text-danger"><strong>:message</strong></p>') !!} {!! $errors->registration->first('password', '<p class="text-danger"><strong>:message</strong></p>') !!}
@include('Shop.auth.partials.password_rules', ['passwordInputId' => 'password'])
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
@@ -68,8 +66,6 @@
</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,5 +1,3 @@
@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">
@@ -15,8 +13,7 @@
<label for="new-password" class="col-md-6 control-label text-right">Nouveau mot de passe</label> <label for="new-password" class="col-md-6 control-label text-right">Nouveau mot de passe</label>
<div class="col-md-6"> <div class="col-md-6">
<input id="new-password" type="password" class="form-control" name="new-password"> <input id="new-password" type="password" class="form-control" name="new-password" required>
@include('Shop.auth.partials.password_rules', ['passwordInputId' => 'new-password'])
</div> </div>
</div> </div>
@@ -24,6 +21,6 @@
<label for="new-password-confirm" class="col-md-6 control-label text-right">Confirmez votre mot de passe</label> <label for="new-password-confirm" class="col-md-6 control-label text-right">Confirmez votre mot de passe</label>
<div class="col-md-6"> <div class="col-md-6">
<input id="new-password-confirm" type="password" class="form-control" name="new-password_confirmation"> <input id="new-password-confirm" type="password" class="form-control" name="new-password_confirmation" required>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,6 @@
{{ 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 homepage-text" style="font-size: 1.2em;">{!! $text !!}</div> <div class="col-12 p-3 green-dark" style="font-size: 1.2em;">{!! $text !!}</div>
</div> </div>
@endif @endif

View File

@@ -1,19 +0,0 @@
<ul class="navbar-nav w-100">
@foreach ($categories as $menu)
<li class="nav-item dropdown megamenu p-2 col
@if (in_array($menu['id'], [$category['id'] ?? false, $category['parent_id'] ?? false, $category['parent']['parent_id'] ?? false])) active @endif">
@if ($menu['children'] ?? false)
<a id="megamenu_{{ $menu['id'] }}" href="{{ route('Shop.Categories.show', ['id' => $menu['id']]) }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="nav-link dropdown-toggle text-uppercase">
{{ $menu['name'] }}
</a>
<div aria-labelledby="megamenu_{{ $menu['id'] }}" class="dropdown-menu border-0 p-0 m-0">
@include('Shop.layout.partials.megamenu')
</div>
@else
<a href="{{ route('Shop.Categories.show', ['id' => $menu['id']]) }}" class="nav-link text-uppercase text-white">
{{ $menu['name'] }}
</a>
@endif
</li>
@endforeach
</ul>

View File

@@ -15,20 +15,7 @@
@isset($trigger) data-trigger="{{ $trigger }}" @endisset @isset($trigger) data-trigger="{{ $trigger }}" @endisset
@isset($container) data-container="{{ $container }}" @endisset @isset($container) data-container="{{ $container }}" @endisset
@isset($html) data-html="true" @endisset @isset($html) data-html="true" @endisset
@isset($metadata) {{ $metadata }} @endisset @isset($metadata) {{ $metadata }} @endisset>
@isset($attr)
@if (is_array($attr))
@foreach ($attr as $key => $value)
@if ($value === true)
{{ $key }}
@elseif ($value !== false && $value !== null)
{{ $key }}="{{ $value }}"
@endif
@endforeach
@else
{{ $attr }}
@endif
@endisset>
<i class="fa fa-fw {{ $icon ?? '' }}"></i> <i class="fa fa-fw {{ $icon ?? '' }}"></i>
{{ $txt ?? '' }} {{ $txt ?? '' }}
</button> </button>

View File

@@ -1,6 +0,0 @@
@include('components.form.button', [
'class' => 'btn-info duplicate ' . ($class ?? ''),
'icon' => 'fa-copy',
'txt' => __('Dupliquer'),
'attr' => ['data-url' => $duplicate_url ?? $duplicateUrl ?? null],
])

View File

@@ -1,33 +1,7 @@
@php
$cancelUrl = $cancel_url ?? $cancelUrl ?? null;
$duplicateUrl = $duplicate_url ?? $duplicateUrl ?? null;
@endphp
@push('header-actions')
<div class="form-buttons d-flex align-items-center ml-3">
@include('components.form.buttons.button-cancel', [
'class' => 'btn-sm mr-2',
'url' => $cancelUrl,
])
@if($duplicateUrl)
@include('components.form.buttons.button-duplicate', [
'class' => 'btn-sm mr-2',
'duplicate_url' => $duplicateUrl,
])
@endif
@include('components.form.buttons.button-save', [
'class' => 'btn-sm',
])
</div>
@endpush
<div class="row pt-0 pb-3"> <div class="row pt-0 pb-3">
<div class="col-12"> <div class="col-12">
<div class="text-right form-buttons"> <div class="text-right form-buttons">
@include('components.form.buttons.button-cancel', ['url' => $cancelUrl]) @include('components.form.buttons.button-cancel')
@if($duplicateUrl)
@include('components.form.buttons.button-duplicate', ['duplicate_url' => $duplicateUrl])
@endif
@include('components.form.buttons.button-save') @include('components.form.buttons.button-save')
</div> </div>
</div> </div>

View File

@@ -1,18 +1,15 @@
<div class="content-header pt-2 pb-1"> <div class="content-header pt-2 pb-1">
<div class="container-fluid"> <div class="container-fluid">
<div class="row mb-2 align-items-center"> <div class="row mb-2 align-items-end">
<div class="col-sm-8"> <div class="col-sm-6">
<div class="d-flex align-items-center flex-wrap"> <h1 class="m-0 text-dark">
<h1 class="m-0 text-dark d-flex align-items-center flex-grow-1"> {{ $title ?? null}}
{{ $title ?? null}} @isset($subtitle)
@isset($subtitle) <small class="font-weight-light ml-1 text-md">{{ $subtitle }}</small>
<small class="font-weight-light ml-1 text-md">{{ $subtitle }}</small> @endisset
@endisset </h1>
</h1>
@stack('header-actions')
</div>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-6">
<ol class="breadcrumb float-sm-right text-sm"> <ol class="breadcrumb float-sm-right text-sm">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a href="{{ route('boilerplate.dashboard') }}"> <a href="{{ route('boilerplate.dashboard') }}">

View File

@@ -94,18 +94,6 @@
@stack('scripts') @stack('scripts')
@stack('js') @stack('js')
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.form-buttons .duplicate').forEach(function(btn) {
btn.addEventListener('click', function() {
var url = this.dataset.url || this.getAttribute('data-url');
if (url) {
window.location = url;
}
});
});
});
</script>
</body> </body>

View File

@@ -1,44 +0,0 @@
@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

View File

@@ -14,5 +14,4 @@ Route::prefix('Articles')->name('Articles.')->group(function () {
Route::get('getProductImages/{product_id?}/{model?}', 'ArticleController@getProductImages')->name('getProductImages'); Route::get('getProductImages/{product_id?}/{model?}', 'ArticleController@getProductImages')->name('getProductImages');
Route::post('toggleVisible', 'ArticleController@toggleVisible')->name('toggleVisible'); Route::post('toggleVisible', 'ArticleController@toggleVisible')->name('toggleVisible');
Route::post('toggleHomepage', 'ArticleController@toggleHomepage')->name('toggleHomepage'); Route::post('toggleHomepage', 'ArticleController@toggleHomepage')->name('toggleHomepage');
Route::get('duplicate/{id}', 'ArticleController@duplicate')->name('duplicate');
}); });

View File

@@ -1,8 +1,8 @@
<?php <?php
Route::middleware('auth')->prefix('Admin')->namespace('Admin')->name('Admin.')->group(function () { Route::middleware('auth')->prefix('Admin')->namespace('Admin')->name('Admin.')->group(function () {
Route::get('{period?}', 'HomeController@index')->name('home');
include __DIR__.'/Botanic/route.php'; include __DIR__.'/Botanic/route.php';
include __DIR__.'/Core/route.php'; include __DIR__.'/Core/route.php';
include __DIR__.'/Shop/route.php'; include __DIR__.'/Shop/route.php';
Route::get('{period?}', 'HomeController@index')->name('home');
}); });

View File

@@ -1,8 +1,5 @@
<?php <?php
Route::prefix('Offres')->name('Offers.')->group(function () { Route::prefix('Offres')->name('Offers.')->group(function () {
// Public offer pages are not exposed; keep the route returning 404 to avoid leaking data. Route::get('show/{id}', 'OfferController@show')->name('show');
Route::get('show/{id}', function () {
abort(404);
})->name('show');
}); });