26 Commits

Author SHA1 Message Date
Valentin Lab
f6eb686fcd fix: display « tarif appliqué » on checkout page
The ``basketTotal`` partial expects ``$sale_channel`` as a top-level
view variable.  The cart page passed it correctly, but the checkout
page and the AJAX basket refresh only passed it nested inside
``$basket``.
2026-02-09 10:00:39 +01:00
Valentin Lab
2771a09a90 chg: use rich address partial in checkout with add/delete support
Replaces the simple address radio list in the checkout page with the
richer ``Shop.Customers.partials.addresses`` partial already used on
the profile edit page.  Customers can now choose among existing
addresses, add a new one on-the-fly, or delete an address directly
from the checkout flow.
2026-02-09 09:51:39 +01:00
Valentin Lab
4f3ab05757 fix: use absolute URL for logo in all mail templates
The logo ``src`` was ``/storage/photos/shares/logo.png`` — a relative
path to a file that doesn't exist.  Email clients cannot resolve
relative URLs.  Replaces with the absolute URL to the actual logo at
``https://boutique.jardinenvie.com/img/logo.png`` across all 4 mail
templates.
2026-02-09 09:37:49 +01:00
Valentin Lab
bf8e948ff3 new: display dynamic password rules checklist on registration and profile edit
Each rule (length, lowercase, uppercase, number, special character)
shows a live check/cross icon as the user types.  Also aligns
``handlePasswordChange`` server-side validation with the boilerplate
``Password`` rule (was only enforcing min 8 chars).
2026-02-09 09:29:29 +01:00
Valentin Lab
3d4496b253 fix: make phone number mandatory on registration and profile edit 2026-02-09 09:16:37 +01:00
Valentin Lab
b763915211 fix: enable scrolling in mobile navigation menu
The mobile category menu (``#navbarContentMobile``) could not scroll
when its content exceeded the viewport height, because no overflow
or height constraint was set on the collapse container.

Add ``max-height: calc(100vh - 60px)``, ``overflow-y: auto`` and
``-webkit-overflow-scrolling: touch`` to allow touch-scrolling
through the full category list on mobile devices.
2026-02-09 09:12:07 +01:00
Valentin Lab
1bf920c123 fix: correct PDF invoice address separator and translate label
- Replace ``<br>`` with ``\n`` in ``InvoicePDF::makeAddress()`` so
  dompdf renders line breaks instead of showing raw HTML tags.
- Translate ``order number`` to ``Numéro de commande`` in the custom
  fields passed to the invoice builder.

The amount-in-words was already in English because the container
lacked French ICU data (``icu-data-full``); that was fixed at
runtime, not in code.
2026-02-09 08:53:38 +01:00
Valentin Lab
ed3909782b fix: implement password change for shop customers
The password change form on the profile page (``Mes coordonnées``)
was scaffolded but never wired to any backend logic. The fields
``current-password``, ``new-password`` and ``new-password_confirmation``
were silently ignored by ``Customers::storeFull()``.

- Add ``handlePasswordChange()`` in ``CustomerController`` that
  validates current password, confirmation match, and 8-char minimum
  before hashing and saving.
- Remove ``required`` attribute from password fields so the form can
  submit for profile-only updates without filling password fields.
- Strip password fields from request data before passing to
  ``storeFull()`` to avoid Eloquent mass-assignment noise.
2026-02-09 08:36:29 +01:00
Valentin Lab
4fbbe991d9 fix: order confirmation email shows wrong payment method and incomplete address
The email template had "Carte de crédit" hardcoded regardless of the
actual payment method. The address blocks were also missing the
``address2`` and ``name`` fields.

- Add ``mode_paiement``, ``livraison_nom``, ``facturation_nom``,
  ``livraison_adresse2``, ``facturation_adresse2`` to
  ``ConfirmationCommande`` Mailable
- Migration to replace hardcoded payment label with
  ``{{mode_paiement}}`` and add ``address2`` fields in DB template
- Migration to add ``name`` fields before each address block
2026-02-09 07:23:50 +01:00
Valentin Lab
41d3294f74 chg: show "en attente de règlement" for check/wire orders in customer order list
When an order has status 0 ("En attente") and payment type is check
or wire transfer, the customer-facing order list now displays
"En attente de règlement" instead of the generic "En attente".
2026-02-09 06:53:55 +01:00
Valentin Lab
9c1f3dfed2 new: show payment-specific confirmation for check/wire orders
Display a tailored confirmation message when the customer pays by
check or wire transfer, including a warning about the 30-day
cancellation policy. The payment type is passed as a query parameter
so the message survives page reloads.

- Add ``getOrderConfirmedByCheckContent()`` and
  ``getOrderConfirmedByWireContent()`` to ``Contents`` repository
- Flash ``payment_type`` through redirect query parameter
- Add migration inserting content rows (id 10, 11)
- Update confirmed view with green checkmark and warning icon
2026-02-09 06:47:18 +01:00
Valentin Lab
e774113110 fix: prevent error 500 on search with no results
``getArticlesToSell()`` returned ``false`` when no articles matched,
causing ``collect(false)`` to produce ``[false]``. The view then
iterated over that single-element array and tried to access array
offsets on a boolean value.
2026-02-09 05:00:27 +01:00
Valentin Lab
1f4177cdb3 fix: missing translation for article's deletion confirmation modal 2025-12-14 21:43:46 +01:00
Valentin Lab
66c035ef9a new: add `duplicate` button for articles 2025-12-14 21:25:42 +01:00
Valentin Lab
fefd6209ac new: add tooltip to the existing links towards "offre" and "tarif" 2025-12-13 22:26:40 +01:00
Valentin Lab
f5ec254c0e new: add a direct link toward article's admin edit form from the public article page 2025-12-13 22:18:42 +01:00
Valentin Lab
a43e82f3d9 new: remove the "previsualisation" side pane from the "offre" admin edit form 2025-12-13 22:07:16 +01:00
Valentin Lab
65460fd9f1 new: add a link to public article page from article and offres admin edit form 2025-12-13 22:04:56 +01:00
Valentin Lab
d9ae84310d fix: avoid sharing route to JSON data to offers
No paths leads there, and there are no reason to keep this route.
2025-12-13 22:02:46 +01:00
Valentin Lab
2fc091d754 chg: make the herited info pane closed by default in admin edit form for articles 2025-12-13 21:46:45 +01:00
Valentin Lab
7887e2d532 new: make all forms have a cancel/save button on the top also 2025-12-13 21:43:40 +01:00
Valentin Lab
f92e175731 fix: prevent error 500 upon displaying empty 'Rayons' 2025-12-13 21:43:40 +01:00
Valentin Lab
6bb910bb54 fix: make 'Rayons' title adaptable to screen width 2025-12-13 21:43:40 +01:00
Valentin Lab
1db3725fb2 new: make the menu visible on mobile 2025-12-13 21:43:40 +01:00
Valentin Lab
ebdf0c0d8e fix: repair tinymce implementation 2025-12-13 20:10:19 +01:00
Valentin Lab
22ebcb102f fix: prevent error message about missing css 2025-12-13 20:09:56 +01:00
52 changed files with 854 additions and 152 deletions

View File

@@ -57,6 +57,7 @@ RUN chmod +x artisan
RUN ./artisan vendor:publish --tag=public --force ## creates public/vendor/jsvalidation
RUN ./artisan vendor:publish --tag=boilerplate-public --force --ansi ## creates public/vendor/boilerplate
RUN ./artisan vendor:publish --tag=datatables-buttons --force --ansi ## creates public/vendor/datatables/buttons
RUN ./artisan vendor:publish --tag=lfm_public --force --ansi
## XXXvlab: 2025-09-25 these migration files are breaking first
## install, but we had to resolve to not install from scratch and use

View File

@@ -47,6 +47,10 @@ class CustomerOrdersDataTable extends DataTable
{
$datatables
->editColumn('status', function (Order $order) {
if ($order->status == 0 && in_array($order->payment_type, [2, 3])) {
return 'En attente de règlement';
}
return Orders::getStatus($order->status);
})
->editColumn('created_at', function (Order $order) {

View File

@@ -63,6 +63,17 @@ class ArticleController extends Controller
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)
{
return Articles::destroy($id);

View File

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

View File

@@ -8,6 +8,7 @@ use App\Repositories\Shop\CustomerAddresses;
use App\Repositories\Shop\Customers;
use App\Repositories\Shop\Offers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
@@ -117,9 +118,82 @@ class CustomerController extends Controller
public function store(Request $request)
{
$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);
return redirect()->route('Shop.Customers.edit');
$growl = $request->filled('new-password')
? [__('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)
@@ -171,6 +245,7 @@ class CustomerController extends Controller
$html = view('Shop.Customers.partials.address_item', [
'address' => $address->toArray(),
'prefix' => $prefix,
'inputName' => $request->input('input_name'),
'with_name' => true,
'selected' => $address->id,
])->render();

View File

@@ -9,6 +9,7 @@ use App\Repositories\Core\User\ShopCart;
use App\Repositories\Shop\Baskets;
use App\Repositories\Shop\Contents;
use App\Repositories\Shop\Customers;
use App\Repositories\Shop\CustomerAddresses;
use App\Repositories\Shop\Deliveries;
use App\Repositories\Shop\DeliveryTypes;
use App\Repositories\Shop\OrderMails;
@@ -57,8 +58,21 @@ class OrderController extends Controller
$deliveries = $deliveries ? $deliveries->values() : collect();
$customerData = $customer ? $customer->toArray() : false;
if ($customerData && $defaultSaleChannelId) {
$customerData['default_sale_channel_id'] = $defaultSaleChannelId;
if ($customerData) {
$customerData['delivery_address_id'] = optional(CustomerAddresses::getDeliveryAddress($customerId))->id;
$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 = [
@@ -88,7 +102,9 @@ class OrderController extends Controller
}
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');
@@ -97,9 +113,18 @@ class OrderController extends Controller
public function confirmed()
{
ShopCart::clear();
$paymentType = request('payment_type');
$content = Contents::getOrderConfirmedContent();
$paymentLabel = match ($paymentType) {
'2' => 'chèque',
'3' => 'virement',
default => null,
};
return view('Shop.Orders.confirmed', ['content' => $content]);
return view('Shop.Orders.confirmed', [
'content' => $content,
'payment_label' => $paymentLabel,
]);
}
public function getPdf($uuid)

View File

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

View File

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

View File

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

View File

@@ -178,10 +178,18 @@ class Articles
$articles = self::getArticlesWithOffers($options);
$searchOrder = $options['ids'] ?? false ? array_flip($options['ids']->toArray()) : false;
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();
if (! count($price_lists)) {
continue;
}
if (empty($price_lists[0]['price_list_values'][0] ?? null)) {
continue;
}
if (! is_array($data[$article->name] ?? false)) {
$data[$article->name] = self::getDataForSale($article);
if ($searchOrder) {
@@ -196,7 +204,7 @@ class Articles
ksort($data);
}
return $data ?? false;
return $data ?? [];
}
public static function getDataForSale($article)

View File

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

View File

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

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

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

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

9
resources/lang/en/fg.php Normal file
View File

@@ -0,0 +1,9 @@
<?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',
];

9
resources/lang/fr/fg.php Normal file
View File

@@ -0,0 +1,9 @@
<?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,3 +112,33 @@ body {
.bg-darker {
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

@@ -311,6 +311,52 @@ div.megamenu ul.megamenu li.megamenu.level1
}
.category-title {
font-size: 2em;
}
.category-description {
font-size: 1.2em;
}
.breadcrumb-title {
font-size: 1.6em;
}
.breadcrumb-current {
font-size: 1em;
}
@media (max-width: 767.98px){
.category-title {
font-size: 1.5em;
}
.category-description {
font-size: 1.05em;
}
.breadcrumb-title {
font-size: 1.4em;
}
.breadcrumb-current {
font-size: 0.95em;
}
}
@media (max-width: 575.98px){
.category-title {
font-size: 1.35em;
}
.category-description {
font-size: 0.95em;
}
.breadcrumb-title {
font-size: 1.2em;
}
.breadcrumb-current {
font-size: 0.9em;
}
}
@font-face {
font-family: 'noto_sanscondensed';
src: url('/fonts/notosans-condensed/notosans-condensed-webfont.eot');
@@ -348,4 +394,45 @@ div.megamenu ul.megamenu li.megamenu.level1
.dropdown-menu > li:hover > .submenu{
display: block;
}
}
}
@media (max-width: 991.98px){
#navbarContentMobile {
max-height: calc(100vh - 60px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
#navbarContentMobile .navbar-nav {
flex-direction: column;
}
#navbarContentMobile .navbar-nav .col {
flex: 0 0 100%;
max-width: 100%;
}
#navbarContentMobile .dropdown-menu {
display: block;
position: static;
float: none;
box-shadow: none;
background: transparent;
}
#navbarContentMobile .dropdown-toggle::after {
display: none;
}
#navbarContentMobile .dropdown-menu .container,
#navbarContentMobile .dropdown-menu .row {
margin: 0;
}
#navbarContentMobile .dropdown-menu .shadow {
box-shadow: none !important;
}
.category-card .card-body {
font-size: 0.75rem;
}
/* Supprimer les grandes marges du container en affichage mobile/tablette */
.container {
max-width: 100%;
}
}

View File

@@ -32,12 +32,14 @@ $(document).on('click', '.dropdown-menu', function (e) {
// make it as accordion for smaller screens
if ($(window).width() < 992) {
$('.dropdown-menu a').click(function(e) {
e.preventDefault();
if ($(this).next('.submenu').length) {
$(this).next('.submenu').toggle();
var $submenu = $(this).next('.submenu');
if ($submenu.length) {
e.preventDefault();
$submenu.toggle();
}
$('.dropdown').on('hide.bs.dropdown', function () {
$(this).find('.submenu').hide();
});
});
$('.dropdown').on('hide.bs.dropdown', function () {
$(this).find('.submenu').hide();
});
}

View File

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

View File

@@ -5,5 +5,13 @@
])
@section('content')
@include('Admin.Shop.Articles.form')
@php
$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

View File

@@ -5,10 +5,29 @@
'files' => true,
]) }}
<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')
{{ Form::close() }}
<x-save />
<x-save :cancel-url="$cancel_url ?? null" :duplicate-url="$duplicate_url ?? null" />
@include('load.form.appender')
@include('load.form.editor')

View File

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

View File

@@ -1,8 +1,17 @@
{{ Form::open(['route' => 'Admin.Shop.Offers.store', 'id' => 'offer-form', 'autocomplete' => 'off']) }}
<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="col-8">
<div class="col-12">
<div class="row mb-3">
<div class="col-12">
@include('components.form.select', [
@@ -96,13 +105,6 @@
</div>
@endcomponent
</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>
@@ -117,59 +119,8 @@
{!! JsValidator::formRequest('App\Http\Requests\Admin\Shop\StoreOfferPost', '#offer-form') !!}
<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();
initSaveForm('#offer-form');
initSelect2();
@if ($offer['id'] ?? false)
initPreview();
@endif
</script>
@endpush

View File

@@ -50,7 +50,16 @@
<div class="col-lg-3 col-xs-12">
@if (auth('web')->check() && !empty($article['available_sale_channels']))
<div id="article-admin-offers" class="alert alert-info p-2 mb-3">
<strong class="d-block">Offres :</strong>
<div class="d-flex justify-content-between align-items-center">
<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">
@php
$currentSaleChannelId = $article['current_sale_channel']['id'] ?? null;
@@ -86,7 +95,7 @@
$tariffId = $channel['tariff_id'] ?? null;
@endphp
@if ($tariffId)
<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">
<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">
{{ number_format($priceTaxed, 2, ',', ' ') }} € TTC
@if (! empty($quantity))
<span class="d-block text-muted" style="font-size: 0.85em;">Qté min. {{ $quantity }}</span>
@@ -113,7 +122,7 @@
$stockClass = $offer['stock_current'] > 0 ? 'text-success' : 'text-danger';
@endphp
<li class="small {{ $offerClass }}" style="font-size: 0.85em;">
<a href="{{ route('Admin.Shop.Offers.edit', $offer['id']) }}" target="_blank" rel="noopener" class="text-decoration-none {{ $offerClass }} admin-link-group admin-offer-link">
<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">
<div class="d-flex justify-content-between align-items-start">
<div>
<span style="opacity: 0.5;">{{ $isSelectedOffer ? '▸' : '○' }}</span>

View File

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

View File

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

View File

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

View File

@@ -4,8 +4,20 @@
@section('content')
<div class="row">
<div class="col-12">
{!! $content !!}
<div class="col-12 text-center py-5">
<i class="fa fa-check-circle text-success" style="font-size: 5rem;"></i>
<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>
@endsection

View File

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

View File

@@ -1,9 +1,11 @@
<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]',
@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>
@@ -13,10 +15,12 @@
<x-layout.collapse id="delivery_addresses" title="Adresse de livraison" class="rounded-lg mb-3 d-none"
uncollapsed=true>
@include('Shop.Orders.partials.addresses', [
'addresses' => $customer['delivery_addresses'] ?? false,
'prefix' => 'delivery',
'name' => 'delivery_address_id',
@include('Shop.Customers.partials.addresses', [
'addresses' => $customer['delivery_addresses'] ?? [],
'prefix' => 'deliveries',
'inputName' => 'delivery_address_id',
'with_name' => true,
'selected' => $customer['delivery_address_id'] ?? null,
])
@include('Shop.Orders.partials.shipping')
</x-layout.collapse>

View File

@@ -1,8 +1,6 @@
<h1 style="font-size: 1.5em;">
<h1 class="breadcrumb-title">
@foreach($breadcrumb ?? [] as $parent)
<a href="{{ route('Shop.Categories.show', ['id' => $parent['id']]) }}" style="text-decoration: none; color: inherit;">{{ $parent['name'] }}</a> /
<a href="{{ route('Shop.Categories.show', ['id' => $parent['id']]) }}" class="breadcrumb-link">{{ $parent['name'] }}</a> /
@endforeach
<span style="font-size: 1.4em;">
{{ $category['name'] }}
</span>
<span class="breadcrumb-current">{{ $category['name'] }}</span>
</h1>

View File

@@ -1,7 +1,7 @@
<div class="row">
<div class="col-8">
<h1 style="font-size: 2em;">{{ $category['name'] }}</h1>
<h3 style="font-size: 1.2em;">{!! $category['description'] !!}</h3>
<h1 class="category-title">{{ $category['name'] }}</h1>
<h3 class="category-description">{!! $category['description'] !!}</h3>
</div>
<div class="col-4">
@include('Shop.layout.partials.category_add')
@@ -12,4 +12,4 @@
<div class="col-12">
@include('Shop.layout.partials.category_articles')
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<div class="row">
<div class="row mx-n1">
@if ($articles ?? false)
@foreach ($articles as $product_name => $article)
<div class="col-lg-3 col-xs-12 mb-3">
<div class="category-card col-6 col-md-4 col-lg-3 mb-2 px-1">
@include('Shop.Articles.partials.article')
</div>
@endforeach
@@ -46,4 +46,3 @@
});
</script>
@endpush

View File

@@ -0,0 +1,62 @@
@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,10 +11,12 @@
<label>Mot de passe *</label>
{{ Form::password('password', [
'class' => 'form-control',
'id' => 'password',
'placeholder' => __('boilerplate::auth.fields.password'),
'required',
]) }}
{!! $errors->registration->first('password', '<p class="text-danger"><strong>:message</strong></p>') !!}
@include('Shop.auth.partials.password_rules', ['passwordInputId' => 'password'])
</div>
</div>
<div class="col-6">

View File

@@ -13,7 +13,8 @@
<label for="new-password" class="col-md-6 control-label text-right">Nouveau mot de passe</label>
<div class="col-md-6">
<input id="new-password" type="password" class="form-control" name="new-password" required>
<input id="new-password" type="password" class="form-control" name="new-password">
@include('Shop.auth.partials.password_rules', ['passwordInputId' => 'new-password'])
</div>
</div>
@@ -21,6 +22,6 @@
<label for="new-password-confirm" class="col-md-6 control-label text-right">Confirmez votre mot de passe</label>
<div class="col-md-6">
<input id="new-password-confirm" type="password" class="form-control" name="new-password_confirmation" required>
<input id="new-password-confirm" type="password" class="form-control" name="new-password_confirmation">
</div>
</div>

View File

@@ -1,18 +1,28 @@
<div class="row bg-light">
<div class="row bg-light align-items-center">
<div class="col-sm-12 col-lg-5">
<div class="col-6 col-lg-5 d-flex align-items-center">
<a href="/"><img src="/img/logo.png" height="52" alt="Jardin'Envie"></a>
<span class="green ml-3">Variétés Paysannes de la Semence à l'Assiette</span>
<span class="green ml-3 d-none d-md-inline">Variétés Paysannes de la Semence à l'Assiette</span>
</div>
<div class="col-sm-12 col-lg-4 pt-2">
<div class="col-12 col-lg-4 pt-2 order-3 order-lg-2">
@include('Shop.layout.partials.search')
</div>
<div class="col-sm-12 col-lg-3 pt-2 text-right">
<div class="col-6 col-lg-3 pt-2 text-right order-2 order-lg-3 d-flex justify-content-end align-items-center">
@include('Shop.layout.partials.header-catalog')
@include('Shop.layout.partials.header-profile')
@include('Shop.layout.partials.header-basket')
</div>
</div>
<div class="row d-lg-none bg-green-dark">
<div class="col-12 p-0">
<div class="collapse" id="navbarContentMobile">
<nav class="navbar navbar-dark p-0">
@include('Shop.layout.partials.sections-menu-list')
</nav>
</div>
</div>
</div>

View File

@@ -10,6 +10,13 @@
<div class="input-group-append">
<span class="input-group-text"><i class="btn btn-sm fa fa-search"></i></span>
</div>
<div class="input-group-append d-lg-none">
<button class="navbar-toggler navbar-light" type="button" data-toggle="collapse"
data-target="#navbarContentMobile" aria-controls="navbarContentMobile" aria-expanded="false"
aria-label="Menu catégories">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</form>

View File

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

@@ -1,27 +1,9 @@
<div class="row mb-3 bg-green-dark">
<div class="row mb-3 bg-green-dark d-none d-lg-block">
<div class="col-12 pl-0 pr-0">
<nav class="navbar navbar-expand-lg p-0">
<div class="collapse navbar-collapse" id="navbarContent">
<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>
<nav class="navbar navbar-expand-lg navbar-dark p-0">
<div class="navbar-collapse show" id="navbarContent">
@include('Shop.layout.partials.sections-menu-list')
</div>
</nav>
</div>
</div>
</div>

View File

@@ -15,7 +15,20 @@
@isset($trigger) data-trigger="{{ $trigger }}" @endisset
@isset($container) data-container="{{ $container }}" @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>
{{ $txt ?? '' }}
</button>

View File

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

View File

@@ -1,7 +1,33 @@
@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="col-12">
<div class="text-right form-buttons">
@include('components.form.buttons.button-cancel')
@include('components.form.buttons.button-cancel', ['url' => $cancelUrl])
@if($duplicateUrl)
@include('components.form.buttons.button-duplicate', ['duplicate_url' => $duplicateUrl])
@endif
@include('components.form.buttons.button-save')
</div>
</div>

View File

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

View File

@@ -94,6 +94,18 @@
@stack('scripts')
@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>

View File

@@ -39,7 +39,7 @@
''); // Empty tags
},
skin: "oxide",
content_css: 'oxide',
content_css: 'default',
language: '{{ App::getLocale() }}',
file_picker_callback: function(callback, value, meta) {
var x = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName(

View File

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

View File

@@ -1,8 +1,8 @@
<?php
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__.'/Core/route.php';
include __DIR__.'/Shop/route.php';
Route::get('{period?}', 'HomeController@index')->name('home');
});

View File

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