Compare commits
80 Commits
1.0.0-rc.5
...
43993f587c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43993f587c | ||
|
|
de13dee166 | ||
|
|
64af20e10a | ||
|
|
2184f1e83c | ||
|
|
8d94c038ad | ||
|
|
f27a7ef8e6 | ||
|
|
6e46485d53 | ||
|
|
31815cd618 | ||
|
|
5947ee256a | ||
|
|
93f027f815 | ||
|
|
3bfbd629bf | ||
|
|
493743307a | ||
|
|
d8f95c667c | ||
|
|
2563398df2 | ||
|
|
39572c9ea2 | ||
|
|
55051334ef | ||
|
|
63673117b3 | ||
|
|
ef52addc7d | ||
|
|
94af725373 | ||
|
|
fa4aea7358 | ||
|
|
5325fa1f06 | ||
|
|
5c10645af7 | ||
|
|
7e9c3c6196 | ||
|
|
e4540f9d88 | ||
|
|
936c9473a7 | ||
|
|
cbd8e33f3b | ||
|
|
701e424185 | ||
|
|
cd5d72e272 | ||
|
|
7a246a189a | ||
|
|
f8a5caec60 | ||
|
|
9903579b98 | ||
|
|
552b823b8b | ||
|
|
f6eb686fcd | ||
|
|
2771a09a90 | ||
|
|
4f3ab05757 | ||
|
|
bf8e948ff3 | ||
|
|
3d4496b253 | ||
|
|
b763915211 | ||
|
|
1bf920c123 | ||
|
|
ed3909782b | ||
|
|
4fbbe991d9 | ||
|
|
41d3294f74 | ||
|
|
9c1f3dfed2 | ||
|
|
e774113110 | ||
|
|
1f4177cdb3 | ||
|
|
66c035ef9a | ||
|
|
fefd6209ac | ||
|
|
f5ec254c0e | ||
|
|
a43e82f3d9 | ||
|
|
65460fd9f1 | ||
|
|
d9ae84310d | ||
|
|
2fc091d754 | ||
|
|
7887e2d532 | ||
|
|
f92e175731 | ||
|
|
6bb910bb54 | ||
|
|
1db3725fb2 | ||
|
|
ebdf0c0d8e | ||
|
|
22ebcb102f | ||
|
|
cc3d4d3e32 | ||
|
|
ef1964d472 | ||
|
|
abb32e32b9 | ||
|
|
8c29459489 | ||
|
|
accb052f5c | ||
|
|
d5f095b5e5 | ||
|
|
fd628f3f95 | ||
|
|
a10f0b35d9 | ||
|
|
858421a9eb | ||
|
|
158bc4fd57 | ||
|
|
b7e3eefed6 | ||
|
|
67e4346c68 | ||
|
|
9ce62e82e5 | ||
|
|
7e93219774 | ||
|
|
29f46b7287 | ||
|
|
1f02c932a0 | ||
|
|
7d8bd8c372 | ||
|
|
f4bd4ddf24 | ||
|
|
1f7098d55b | ||
|
|
1867e75177 | ||
|
|
d502882052 | ||
|
|
a5b2196b32 |
78
AGENTS.md
Normal file
78
AGENTS.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
OpenSem builds on Laravel 9.
|
||||
|
||||
Core application code lives in `app/`, while HTTP routes reside in
|
||||
`routes/` and Blade views in `resources/views/`. Reusable front-end
|
||||
assets (JS, SCSS, images) sit under `resources/` and are compiled into
|
||||
`public/` via Laravel Mix.
|
||||
|
||||
Database blueprints are versioned in `database/migrations/` with seeds
|
||||
in `database/seeders/`.
|
||||
|
||||
Tests are organised in `tests/Unit/` and `tests/Feature/`; keep large
|
||||
fixtures in `tests/Fixtures/` to avoid polluting source directories.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `composer install` — install PHP dependencies defined in
|
||||
`composer.json`.
|
||||
|
||||
- `php artisan serve` — start a local HTTP server on port 8000.
|
||||
|
||||
- `npm install && npm run dev` — install Node tooling and build UI
|
||||
assets for development.
|
||||
|
||||
- `npm run prod` — generate minified production assets in `public/`.
|
||||
|
||||
- `php artisan migrate --seed` — apply database schema and load
|
||||
default data for demo instances.
|
||||
|
||||
- `./build.sh` — builds a `.tar.xz` that contains the production and
|
||||
deployement ready source to be deployed.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Follow PSR-12 with four-space indentation and `snake_case` database
|
||||
columns. Controllers, models, and Livewire components use StudlyCase
|
||||
class names; private methods remain `camelCase`. Run `composer run
|
||||
inspect` before opening a PR to execute `phpcs` and `phpstan`. For
|
||||
front-end changes, keep Blade sections in lowercase kebab IDs (for
|
||||
example, `@section('order-summary')`).
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Use PHPUnit via `php artisan test`; target deterministic tests with
|
||||
clear Arrange/Act/Assert blocks. Feature tests should mirror top-level
|
||||
route names (e.g., `OrdersTest.php`). Unit tests belong in
|
||||
`tests/Unit/` and should stub external services. When adding
|
||||
migrations or service integrations, include coverage that exercises
|
||||
failure paths. For granular checks, `./vendor/bin/phpunit --filter
|
||||
FooTest` is acceptable, but always run the full suite before pushing.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
Pull requests must describe scope, list schema or configuration
|
||||
changes, and note any manual follow-up (cron, storage links, queues).
|
||||
|
||||
Attach screenshots or terminal logs when touching UI or console
|
||||
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
|
||||
|
||||
Copy `.env.example` to `.env` and run `php artisan key:generate`
|
||||
before local work. Never commit `.env`, `storage/`, or database dumps
|
||||
containing sensitive data. Use the Docker resources in `docker/` only
|
||||
for reproducible environments; keep secrets in your host overrides,
|
||||
not in version control.
|
||||
@@ -43,6 +43,7 @@ COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir -p /app/bootstrap/cache \
|
||||
/app/storage/media-library/temp \
|
||||
/app/storage/framework/cache \
|
||||
/app/storage/framework/views \
|
||||
/app/storage/framework/sessions \
|
||||
@@ -56,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
|
||||
@@ -84,8 +86,6 @@ RUN apk add --no-cache xz
|
||||
# bring PHP app with vendor
|
||||
COPY --from=phpdeps /app /app
|
||||
|
||||
# ensure required runtime dirs exist (empty is fine)
|
||||
RUN mkdir -p storage/framework/{cache,views,sessions} bootstrap/cache
|
||||
# create artifact (use tar + xz so we don't depend on GNU tar -J)
|
||||
RUN mkdir -p /out \
|
||||
&& tar -C /app -cf /out/app.tar \
|
||||
|
||||
30
README.md
30
README.md
@@ -1,10 +1,19 @@
|
||||
## A propos de OpenSem
|
||||
|
||||
OpenSem est une solution de commerce électronique et un ERP développé pour les besoins exprimés.
|
||||
OpenSem est une solution de commerce électronique et un ERP développé
|
||||
pour les besoins exprimés.
|
||||
|
||||
Développée par Ludovic Candellier en étroite relation avec
|
||||
Jardin'Envie.
|
||||
|
||||
L'application est écrite en PHP et est basée sur Laravel.
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
Laravel is a web application framework with expressive, elegant
|
||||
syntax. We believe development must be an enjoyable and creative
|
||||
experience to be truly fulfilling. Laravel takes the pain out of
|
||||
development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
@@ -14,12 +23,21 @@ Laravel is a web application framework with expressive, elegant syntax. We belie
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
Laravel is accessible, powerful, and provides tools required for
|
||||
large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
Laravel has the most extensive and thorough
|
||||
[documentation](https://laravel.com/docs) and video tutorial library
|
||||
of all modern web application frameworks, making it a breeze to get
|
||||
started with the framework.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 1500 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can
|
||||
help. Laracasts contains over 1500 video tutorials on a range of
|
||||
topics including Laravel, modern PHP, unit testing, and
|
||||
JavaScript. Boost your skills by digging into our comprehensive video
|
||||
library.
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
The Laravel framework is open-sourced software licensed under the [MIT
|
||||
license](https://opensource.org/licenses/MIT).
|
||||
|
||||
@@ -46,12 +46,12 @@ class CustomerOrdersDataTable extends DataTable
|
||||
{
|
||||
$datatables
|
||||
->editColumn('status', function (Order $order) {
|
||||
return Orders::getStatus($order->status);
|
||||
return Orders::getStatusBadge($order->status);
|
||||
})
|
||||
->editColumn('created_at', function (Order $order) {
|
||||
return $order->created_at->isoFormat('DD/MM/YY HH:mm');
|
||||
})
|
||||
->rawColumns(['action']);
|
||||
->rawColumns(['status', 'action']);
|
||||
|
||||
return parent::modifier($datatables);
|
||||
}
|
||||
|
||||
@@ -29,18 +29,20 @@ class OrdersDataTable extends DataTable
|
||||
{
|
||||
$datatables
|
||||
->editColumn('status', function (Order $order) {
|
||||
return Orders::getStatus($order->status);
|
||||
return Orders::getStatusBadge($order->status);
|
||||
})
|
||||
->editColumn('created_at', function (Order $order) {
|
||||
return $order->created_at->format('d/m/Y H:i:s');
|
||||
})
|
||||
->editColumn('customer.last_name', function (Order $order) {
|
||||
return $order->customer->last_name.' '.$order->customer->first_name;
|
||||
return $order->customer
|
||||
? $order->customer->last_name.' '.$order->customer->first_name
|
||||
: 'Client supprimé';
|
||||
})
|
||||
->editColumn('payment_type', function (Order $order) {
|
||||
return InvoicePayments::getPaymentType($order->payment_type);
|
||||
})
|
||||
->rawColumns(['action']);
|
||||
->rawColumns(['status', 'action']);
|
||||
|
||||
return parent::modifier($datatables);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class TariffsDataTable extends DataTable
|
||||
{
|
||||
$datatables
|
||||
->editColumn('status', function (Tariff $tariff) {
|
||||
return Tariffs::getStatus($tariff['status_id']);
|
||||
return Tariffs::getStatusBadge($tariff['status_id']);
|
||||
})
|
||||
->editColumn('sale_channels2', function (Tariff $tariff) {
|
||||
$html = '';
|
||||
@@ -32,7 +32,7 @@ class TariffsDataTable extends DataTable
|
||||
|
||||
return $html;
|
||||
})
|
||||
->rawColumns(['sale_channels2', 'action']);
|
||||
->rawColumns(['status', 'sale_channels2', 'action']);
|
||||
|
||||
return parent::modifier($datatables);
|
||||
}
|
||||
|
||||
@@ -47,12 +47,16 @@ class CustomerOrdersDataTable extends DataTable
|
||||
{
|
||||
$datatables
|
||||
->editColumn('status', function (Order $order) {
|
||||
return Orders::getStatus($order->status);
|
||||
if ($order->status == 0 && in_array($order->payment_type, [2, 3])) {
|
||||
return '<span class="badge badge-warning">En attente de règlement</span>';
|
||||
}
|
||||
|
||||
return Orders::getStatusBadge($order->status);
|
||||
})
|
||||
->editColumn('created_at', function (Order $order) {
|
||||
return $order->created_at->isoFormat('DD/MM/YY HH:mm');
|
||||
})
|
||||
->rawColumns(['action']);
|
||||
->rawColumns(['status', 'action']);
|
||||
|
||||
return parent::modifier($datatables);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class OrdersDataTable extends DataTable
|
||||
{
|
||||
$datatables
|
||||
->editColumn('status', function (Order $order) {
|
||||
return Orders::getStatus($order->status);
|
||||
return Orders::getStatusBadge($order->status);
|
||||
})
|
||||
->editColumn('created_at', function (Order $order) {
|
||||
return $order->created_at->toDateTimeString();
|
||||
@@ -40,7 +40,7 @@ class OrdersDataTable extends DataTable
|
||||
->editColumn('payment_type', function (Order $order) {
|
||||
return InvoicePayments::getPaymentType($order->payment_type);
|
||||
})
|
||||
->rawColumns(['action']);
|
||||
->rawColumns(['status', 'action']);
|
||||
|
||||
return parent::modifier($datatables);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Botanic;
|
||||
|
||||
use App\Datatables\Botanic\SpeciesDataTable;
|
||||
use App\Repositories\Botanic\Genres;
|
||||
use App\Repositories\Botanic\Species;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -21,7 +20,7 @@ class SpecieController extends Controller
|
||||
|
||||
public function create()
|
||||
{
|
||||
$data = Genres::init();
|
||||
$data = Species::init();
|
||||
|
||||
return view('Admin.Botanic.Species.create', $data);
|
||||
}
|
||||
@@ -36,7 +35,7 @@ class SpecieController extends Controller
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$data = Genres::init();
|
||||
$data = Species::init();
|
||||
$data['specie'] = Species::getFull($id);
|
||||
|
||||
return view('Admin.Botanic.Species.edit', $data);
|
||||
|
||||
@@ -26,6 +26,7 @@ class ArticleController extends Controller
|
||||
'article_natures' => ArticleNatures::getOptions(),
|
||||
'categories' => Categories::getOptions(),
|
||||
'tags' => Tags::getOptionsFullName(),
|
||||
'filters' => request()->only(['article_nature_id', 'category_id', 'tag_id']),
|
||||
];
|
||||
|
||||
return $dataTable->render('Admin.Shop.Articles.list', $data);
|
||||
@@ -63,6 +64,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);
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace App\Http\Controllers\Admin\Shop;
|
||||
|
||||
use App\Datatables\Admin\Shop\OrdersDataTable;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Repositories\Shop\Invoices;
|
||||
use App\Repositories\Shop\OfferStocks;
|
||||
use App\Repositories\Shop\OrderMails;
|
||||
use App\Repositories\Shop\Orders;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -36,6 +38,43 @@ class OrderController extends Controller
|
||||
|
||||
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());
|
||||
if ($order->wasChanged('status')) {
|
||||
switch ($order->status) {
|
||||
@@ -45,7 +84,13 @@ class OrderController extends Controller
|
||||
case 2:
|
||||
OrderMails::sendShipping($order->id);
|
||||
break;
|
||||
case 4:
|
||||
OfferStocks::restoreStock($order->id);
|
||||
break;
|
||||
default:
|
||||
if ($previousStatus == 4) {
|
||||
OfferStocks::decreaseStockForOrder($order->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,10 @@ class PriceListValueController extends Controller
|
||||
|
||||
public function addPrice($index)
|
||||
{
|
||||
$data['index'] = $index;
|
||||
$data = [
|
||||
'index' => $index,
|
||||
'taxes' => Taxes::getOptions(),
|
||||
];
|
||||
|
||||
return view('Admin.Shop.PriceListValues.partials.row_price', $data);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,6 +8,8 @@ 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;
|
||||
|
||||
class CustomerController extends Controller
|
||||
@@ -81,14 +83,33 @@ class CustomerController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$customerId = $data['id'] ?? Customers::getId();
|
||||
$requestedDefaultSaleChannelId = $data['default_sale_channel_id'] ?? null;
|
||||
$hasDefaultSaleChannelColumn = Schema::hasColumn('shop_customers', 'default_sale_channel_id');
|
||||
|
||||
if (! $hasDefaultSaleChannelColumn) {
|
||||
unset($data['default_sale_channel_id']);
|
||||
}
|
||||
|
||||
$customer = Customers::store($data);
|
||||
|
||||
if ($customer) {
|
||||
Customers::guard()->setUser($customer->fresh(['sale_channels']));
|
||||
if (array_key_exists('default_sale_channel_id', $data)) {
|
||||
session(['shop.default_sale_channel_id' => $data['default_sale_channel_id']]);
|
||||
Baskets::refreshPrices((int) $data['default_sale_channel_id']);
|
||||
}
|
||||
if (! $customer) {
|
||||
return response()->json([
|
||||
'error' => 1,
|
||||
'message' => __('Impossible de mettre à jour votre profil pour le moment.'),
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($hasDefaultSaleChannelColumn && $requestedDefaultSaleChannelId !== null) {
|
||||
Customers::setDefaultSaleChannel($customerId, $requestedDefaultSaleChannelId);
|
||||
}
|
||||
|
||||
$freshCustomer = Customers::get($customerId, ['sale_channels']);
|
||||
Customers::guard()->setUser($freshCustomer);
|
||||
|
||||
if ($requestedDefaultSaleChannelId !== null) {
|
||||
session(['shop.default_sale_channel_id' => $requestedDefaultSaleChannelId]);
|
||||
Baskets::refreshPrices((int) $requestedDefaultSaleChannelId);
|
||||
}
|
||||
|
||||
return response()->json(['error' => 0]);
|
||||
@@ -97,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)
|
||||
@@ -151,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();
|
||||
|
||||
@@ -9,8 +9,10 @@ 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\Offers;
|
||||
use App\Repositories\Shop\OrderMails;
|
||||
use App\Repositories\Shop\Orders;
|
||||
use App\Repositories\Shop\Paybox;
|
||||
@@ -57,8 +59,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 = [
|
||||
@@ -80,6 +95,25 @@ class OrderController extends Controller
|
||||
$data['customer_id'] = Customers::getId();
|
||||
$data['sale_channel_id'] = $data['sale_channel_id'] ?? SaleChannels::getDefaultID();
|
||||
$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);
|
||||
|
||||
if ($order) {
|
||||
@@ -88,7 +122,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 +133,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)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Requests\Admin\Shop;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreArticlePost extends FormRequest
|
||||
{
|
||||
@@ -13,8 +14,13 @@ class StoreArticlePost extends FormRequest
|
||||
|
||||
public function rules()
|
||||
{
|
||||
$articleId = $this->input('id');
|
||||
|
||||
return [
|
||||
'ref' => 'required|unique:shop_articles',
|
||||
'ref' => [
|
||||
'required',
|
||||
Rule::unique('shop_articles', 'ref')->ignore($articleId)->whereNull('deleted_at'),
|
||||
],
|
||||
'product_type' => 'required',
|
||||
'product_id' => 'required',
|
||||
'article_nature_id' => 'required',
|
||||
|
||||
@@ -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()],
|
||||
];
|
||||
}
|
||||
|
||||
51
app/Mail/AlertePaiementAnnule.php
Normal file
51
app/Mail/AlertePaiementAnnule.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Core\Mail\MailTemplate;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Spatie\MailTemplates\TemplateMailable;
|
||||
|
||||
class AlertePaiementAnnule extends TemplateMailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $subject;
|
||||
|
||||
public $numero_commande;
|
||||
|
||||
public $date_commande;
|
||||
|
||||
public $montant;
|
||||
|
||||
public $client_nom;
|
||||
|
||||
public $client_email;
|
||||
|
||||
public $reference_paiement;
|
||||
|
||||
protected static $templateModelClass = MailTemplate::class;
|
||||
|
||||
public function __construct($order, $reference)
|
||||
{
|
||||
$this->numero_commande = $order->ref;
|
||||
$this->date_commande = $order->created_at->format('d/m/Y H:i');
|
||||
$this->montant = number_format($order->total_shipped, 2, ',', ' ').' €';
|
||||
$this->client_nom = $order->customer
|
||||
? $order->customer->last_name.' '.$order->customer->first_name
|
||||
: 'Client supprimé';
|
||||
$this->client_email = $order->customer->email ?? 'inconnu';
|
||||
$this->reference_paiement = $reference;
|
||||
}
|
||||
|
||||
public function envelope()
|
||||
{
|
||||
return new Envelope(
|
||||
from: new Address('boutique@jardinenvie.com', 'Jardin\'en\'Vie'),
|
||||
subject: $this->subject,
|
||||
);
|
||||
}
|
||||
}
|
||||
49
app/Mail/AlerteStock.php
Normal file
49
app/Mail/AlerteStock.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Core\Mail\MailTemplate;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Spatie\MailTemplates\TemplateMailable;
|
||||
|
||||
class AlerteStock extends TemplateMailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $subject;
|
||||
|
||||
public $article;
|
||||
|
||||
public $offre;
|
||||
|
||||
public $stock_restant;
|
||||
|
||||
public $seuil;
|
||||
|
||||
public $lien_article;
|
||||
|
||||
public $lien_offre;
|
||||
|
||||
protected static $templateModelClass = MailTemplate::class;
|
||||
|
||||
public function __construct($offer)
|
||||
{
|
||||
$this->article = $offer->article->name ?? 'Article #'.$offer->article_id;
|
||||
$this->offre = $offer->id;
|
||||
$this->stock_restant = $offer->stock_current;
|
||||
$this->seuil = $offer->minimum_ondemand;
|
||||
$this->lien_article = url('/Admin/Shop/Articles/edit/'.$offer->article_id);
|
||||
$this->lien_offre = url('/Admin/Shop/Offers/edit/'.$offer->id);
|
||||
}
|
||||
|
||||
public function envelope()
|
||||
{
|
||||
return new Envelope(
|
||||
from: new Address('boutique@jardinenvie.com', 'Jardin\'en\'Vie'),
|
||||
subject: $this->subject,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ class Article extends Model implements HasMedia
|
||||
|
||||
public function scopeWithAvailableOffers($query, $saleChannelId = false)
|
||||
{
|
||||
return $query->whereHas('offers', function ($query) use ($saleChannelId) {
|
||||
return $query->visible()->whereHas('offers', function ($query) use ($saleChannelId) {
|
||||
$query->active()->byStockAvailable();
|
||||
|
||||
if ($saleChannelId) {
|
||||
|
||||
@@ -92,6 +92,11 @@ class Order extends Model
|
||||
return $query->byStatus(3);
|
||||
}
|
||||
|
||||
public function scopeNotCancelled($query)
|
||||
{
|
||||
return $query->where('status', '!=', 4);
|
||||
}
|
||||
|
||||
public function scopeByStatus($query, $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
|
||||
@@ -11,9 +11,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
if (config('app.env') === 'production') {
|
||||
\URL::forceScheme('https');
|
||||
}
|
||||
Schema::defaultStringLength(191);
|
||||
View::composer('Shop.layout.layout', LayoutComposer::class);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ class Varieties
|
||||
{
|
||||
$images = $data['images'] ?? false;
|
||||
$tags = $data['tags'] ?? false;
|
||||
if (! array_key_exists('plus', $data) || $data['plus'] === null) {
|
||||
$data['plus'] = '';
|
||||
}
|
||||
unset($data['images']);
|
||||
unset($data['tags']);
|
||||
$variety = self::store($data);
|
||||
|
||||
@@ -67,17 +67,15 @@ class Categories
|
||||
$category_target = self::getNode($target_id);
|
||||
|
||||
switch ($type) {
|
||||
case 'before':
|
||||
return $category->insertBeforeNode($category_target);
|
||||
case 'after':
|
||||
$category->afterNode($category_target);
|
||||
break;
|
||||
return $category->insertAfterNode($category_target);
|
||||
case 'inside':
|
||||
$category_target->appendNode($category);
|
||||
break;
|
||||
return $category_target->prependNode($category);
|
||||
default:
|
||||
$category->afterNode($category_target);
|
||||
return $category->insertAfterNode($category_target);
|
||||
}
|
||||
|
||||
return $category->save();
|
||||
}
|
||||
|
||||
public static function create($data)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Repositories\Core;
|
||||
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media as MediaModel;
|
||||
|
||||
class Medias
|
||||
{
|
||||
public static function getImage($model, $conversion = 'normal', $collection = 'images')
|
||||
@@ -79,13 +81,9 @@ class Medias
|
||||
|
||||
public static function getImageSrc($image)
|
||||
{
|
||||
if (! $image) {
|
||||
return null;
|
||||
}
|
||||
$id = $image['id'];
|
||||
$filename = self::getFilename($image);
|
||||
$media = self::resolveMedia($image);
|
||||
|
||||
return "/storage/{$id}/{$filename}";
|
||||
return $media ? $media->getUrl() : null;
|
||||
}
|
||||
|
||||
public static function getThumbSrc($image)
|
||||
@@ -110,6 +108,12 @@ class Medias
|
||||
|
||||
public static function getSrcByType($image, $type)
|
||||
{
|
||||
$media = self::resolveMedia($image);
|
||||
|
||||
if ($media) {
|
||||
return $type ? $media->getUrl($type) : $media->getUrl();
|
||||
}
|
||||
|
||||
return $image ? '/storage/'.$image['id'].'/conversions/'.self::getFilename($image, $type) : false;
|
||||
}
|
||||
|
||||
@@ -124,4 +128,48 @@ class Medias
|
||||
{
|
||||
return str_replace(['#', '/', '\\', ' '], '-', $name);
|
||||
}
|
||||
|
||||
protected static function resolveMedia($image): ?MediaModel
|
||||
{
|
||||
if ($image instanceof MediaModel) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
if (is_null($image)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($image)) {
|
||||
return self::hydrateMedia($image);
|
||||
}
|
||||
|
||||
if (is_object($image)) {
|
||||
if ($image instanceof \ArrayAccess) {
|
||||
return self::hydrateMedia((array) $image);
|
||||
}
|
||||
|
||||
$array = method_exists($image, 'toArray') ? $image->toArray() : (array) $image;
|
||||
|
||||
return self::hydrateMedia($array);
|
||||
}
|
||||
|
||||
$id = data_get($image, 'id');
|
||||
|
||||
return $id ? MediaModel::query()->withoutGlobalScopes()->find($id) : null;
|
||||
}
|
||||
|
||||
protected static function hydrateMedia(array $attributes): ?MediaModel
|
||||
{
|
||||
$id = data_get($attributes, 'id');
|
||||
|
||||
if (! $id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$media = new MediaModel();
|
||||
$media->forceFill($attributes);
|
||||
$media->exists = true;
|
||||
|
||||
return $media;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,21 +27,34 @@ class ArticleTags
|
||||
|
||||
switch ($article->product_type) {
|
||||
case 'App\Models\Botanic\Variety':
|
||||
$data += $article->product->tags->toArray();
|
||||
if ($article->product->specie ?? false) {
|
||||
$data += $article->product->specie->tags->toArray();
|
||||
$variety = $article->product;
|
||||
if ($variety && $variety->tags) {
|
||||
$data = array_merge($data, $variety->tags->toArray());
|
||||
}
|
||||
if ($variety && $variety->specie && $variety->specie->tags) {
|
||||
$data = array_merge($data, $variety->specie->tags->toArray());
|
||||
}
|
||||
break;
|
||||
case 'App\Models\Botanic\Specie':
|
||||
$data += $article->product->tags->toArray();
|
||||
$specie = $article->product;
|
||||
if ($specie && $specie->tags) {
|
||||
$data = array_merge($data, $specie->tags->toArray());
|
||||
}
|
||||
break;
|
||||
case 'App\Models\Shop\Merchandise':
|
||||
$data += $article->product->tags->toArray();
|
||||
$data += $article->product->producer->tags->toArray();
|
||||
$merchandise = $article->product;
|
||||
if ($merchandise && $merchandise->tags) {
|
||||
$data = array_merge($data, $merchandise->tags->toArray());
|
||||
}
|
||||
if ($merchandise && $merchandise->producer && $merchandise->producer->tags) {
|
||||
$data = array_merge($data, $merchandise->producer->tags->toArray());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
$data += $article->tags->toArray();
|
||||
if ($article->tags) {
|
||||
$data = array_merge($data, $article->tags->toArray());
|
||||
}
|
||||
|
||||
foreach ($data as $tag) {
|
||||
if (! isset($tags[$tag['group']][$tag['name']])) {
|
||||
|
||||
@@ -6,6 +6,8 @@ use App\Models\Shop\Article;
|
||||
use App\Repositories\Botanic\Species;
|
||||
use App\Repositories\Botanic\Varieties;
|
||||
use App\Repositories\Shop\SaleChannels;
|
||||
use App\Repositories\Shop\Customers;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Repositories\Core\Comments;
|
||||
use App\Traits\Model\Basic;
|
||||
use App\Traits\Repository\Imageable;
|
||||
@@ -17,7 +19,7 @@ class Articles
|
||||
|
||||
public static function autocomplete($str)
|
||||
{
|
||||
$data = Article::byAutocomplete($str)->orderBy('name')->limit(20)->pluck('name', 'id');
|
||||
$data = Article::byAutocomplete($str)->visible()->orderBy('name')->limit(20)->pluck('name', 'id');
|
||||
$export = [];
|
||||
foreach ($data as $key => $name) {
|
||||
$export[] = ['value' => $key, 'text' => $name];
|
||||
@@ -41,6 +43,7 @@ class Articles
|
||||
'id' => $offer->id,
|
||||
'name' => $offer->variation->name,
|
||||
'prices' => $offer->tariff->price_lists->first()->price_list_values->toArray(),
|
||||
'stock' => $offer->stock_current !== null ? (int) $offer->stock_current : null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -64,6 +67,11 @@ class Articles
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function getVisibilityMap()
|
||||
{
|
||||
return Article::pluck('visible', 'id')->toArray();
|
||||
}
|
||||
|
||||
public static function getAll()
|
||||
{
|
||||
return Article::orderBy('name', 'asc')->get();
|
||||
@@ -72,12 +80,31 @@ class Articles
|
||||
public static function getArticleToSell($id, $saleChannelId = false)
|
||||
{
|
||||
$saleChannelId = $saleChannelId ?: SaleChannels::getDefaultID();
|
||||
$sessionSaleChannelId = session('shop.default_sale_channel_id');
|
||||
$customer = Customers::getAuth();
|
||||
$hasDefaultSaleChannelColumn = Schema::hasColumn('shop_customers', 'default_sale_channel_id');
|
||||
$customerDefaultSaleChannelId = ($customer && $hasDefaultSaleChannelColumn)
|
||||
? $customer->default_sale_channel_id
|
||||
: null;
|
||||
$customerSaleChannelIds = [];
|
||||
|
||||
if ($customer) {
|
||||
$customer->loadMissing('sale_channels:id');
|
||||
$customerSaleChannelIds = $customer->sale_channels->pluck('id')->toArray();
|
||||
}
|
||||
$data = self::getArticle($id);
|
||||
$data['offers'] = self::getOffersGroupedByNature($id, $saleChannelId);
|
||||
|
||||
$currentSaleChannel = $saleChannelId ? SaleChannels::get($saleChannelId) : null;
|
||||
$data['current_sale_channel'] = $currentSaleChannel ? $currentSaleChannel->toArray() : null;
|
||||
$data['available_sale_channels'] = Offers::getSaleChannelsForArticle($id);
|
||||
$data['debug_sale_channel'] = [
|
||||
'session_default_sale_channel_id' => $sessionSaleChannelId,
|
||||
'customer_default_sale_channel_id' => $customerDefaultSaleChannelId,
|
||||
'customer_linked_sale_channel_ids' => $customerSaleChannelIds,
|
||||
'resolved_sale_channel_id' => $saleChannelId,
|
||||
'has_default_sale_channel_column' => $hasDefaultSaleChannelColumn,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
@@ -114,8 +141,11 @@ class Articles
|
||||
$data['specie'] = $article->product ? $article->product->description : '';
|
||||
break;
|
||||
case 'App\Models\Shop\Merchandise':
|
||||
$data['merchandise'] = $article->product ? $article->product->description : '';
|
||||
$data['producer'] = $article->product->producer->description;
|
||||
$merchandise = $article->product;
|
||||
$data['merchandise'] = $merchandise ? ($merchandise->description ?? '') : '';
|
||||
if ($merchandise && $merchandise->producer) {
|
||||
$data['producer'] = $merchandise->producer->description ?? '';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
@@ -154,10 +184,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) {
|
||||
@@ -172,7 +210,7 @@ class Articles
|
||||
ksort($data);
|
||||
}
|
||||
|
||||
return $data ?? false;
|
||||
return $data ?? [];
|
||||
}
|
||||
|
||||
public static function getDataForSale($article)
|
||||
@@ -279,8 +317,6 @@ class Articles
|
||||
case 'merchandise':
|
||||
$model = $model->merchandise();
|
||||
break;
|
||||
default:
|
||||
$model = $model->botanic();
|
||||
}
|
||||
|
||||
return $model;
|
||||
|
||||
@@ -116,6 +116,7 @@ class Baskets
|
||||
'variation' => $offer->variation->name,
|
||||
'image' => Articles::getPreviewSrc(ArticleImages::getFullImageByArticle($offer->article)),
|
||||
'latin' => $offer->article->product->specie->latin ?? false,
|
||||
'stock' => $offer->stock_current !== null ? (int) $offer->stock_current : null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -144,6 +145,13 @@ class Baskets
|
||||
{
|
||||
$offer = Offers::get($id, ['article', 'variation']);
|
||||
|
||||
if ($offer && $offer->stock_current !== null) {
|
||||
$quantity = min($quantity, max(0, (int) $offer->stock_current));
|
||||
if ($quantity <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'name' => self::getArticleName($offer),
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -179,6 +179,24 @@ class Customers
|
||||
return $customer->sale_channels()->sync($saleChannels);
|
||||
}
|
||||
|
||||
public static function setDefaultSaleChannel($customerId, $saleChannelId)
|
||||
{
|
||||
if (! $customerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customer = self::get($customerId);
|
||||
|
||||
if (! $customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customer->default_sale_channel_id = $saleChannelId ?: null;
|
||||
$customer->save();
|
||||
|
||||
return $customer->fresh(['sale_channels']);
|
||||
}
|
||||
|
||||
public static function create($data)
|
||||
{
|
||||
$data['uuid'] = Str::uuid();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,19 +2,108 @@
|
||||
|
||||
namespace App\Repositories\Shop;
|
||||
|
||||
use App\Mail\AlerteStock;
|
||||
use App\Models\Shop\Offer;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class OfferStocks
|
||||
{
|
||||
public static function decreaseStock($item)
|
||||
{
|
||||
$offer = Offers::get($item['offer_id']);
|
||||
$previousStock = $offer->stock_current;
|
||||
$offer->stock_current = $offer->stock_current - $item['quantity'];
|
||||
if ($offer->stock_current <= 0) {
|
||||
$offer->stock_current = 0;
|
||||
}
|
||||
|
||||
return $offer->save();
|
||||
$saved = $offer->save();
|
||||
|
||||
if ($saved) {
|
||||
self::checkStockAlert($offer, $previousStock);
|
||||
}
|
||||
|
||||
return $saved;
|
||||
}
|
||||
|
||||
public static function checkStockAlert($offer, $previousStock)
|
||||
{
|
||||
$threshold = (float) $offer->minimum_ondemand;
|
||||
if ($threshold <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$crossedThreshold = $previousStock > $threshold
|
||||
&& $offer->stock_current <= $threshold;
|
||||
|
||||
if (! $crossedThreshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$offer->load('article');
|
||||
Mail::to('commande@jardinenvie.com')
|
||||
->send(new AlerteStock($offer));
|
||||
Log::info('Stock alert email sent', [
|
||||
'offer_id' => $offer->id,
|
||||
'article' => $offer->article->name ?? $offer->article_id,
|
||||
'stock_current' => $offer->stock_current,
|
||||
'threshold' => $threshold,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to send stock alert email', [
|
||||
'offer_id' => $offer->id,
|
||||
'stock_current' => $offer->stock_current,
|
||||
'exception' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function restoreStock($orderId)
|
||||
{
|
||||
$details = \App\Models\Shop\OrderDetail::where('order_id', $orderId)->get();
|
||||
|
||||
foreach ($details as $detail) {
|
||||
$offer = Offers::get($detail->offer_id);
|
||||
if ($offer) {
|
||||
$offer->stock_current = $offer->stock_current + $detail->quantity;
|
||||
$offer->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function checkStockForOrder($orderId)
|
||||
{
|
||||
$details = \App\Models\Shop\OrderDetail::where('order_id', $orderId)->get();
|
||||
$insufficients = [];
|
||||
|
||||
foreach ($details as $detail) {
|
||||
$offer = Offers::get($detail->offer_id);
|
||||
if ($offer && $offer->stock_current < $detail->quantity) {
|
||||
$offer->load('article');
|
||||
$insufficients[] = [
|
||||
'name' => $offer->article->name ?? 'Offre #'.$offer->id,
|
||||
'stock' => $offer->stock_current,
|
||||
'quantity' => $detail->quantity,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $insufficients;
|
||||
}
|
||||
|
||||
public static function decreaseStockForOrder($orderId)
|
||||
{
|
||||
$details = \App\Models\Shop\OrderDetail::where('order_id', $orderId)->get();
|
||||
|
||||
foreach ($details as $detail) {
|
||||
$offer = Offers::get($detail->offer_id);
|
||||
if ($offer) {
|
||||
$offer->stock_current = max(0, $offer->stock_current - $detail->quantity);
|
||||
$offer->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getStockCurrent($id)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Repositories\Shop;
|
||||
|
||||
use App\Models\Shop\Offer;
|
||||
use App\Models\Shop\PriceList;
|
||||
use App\Models\Shop\PriceListValue;
|
||||
use App\Models\Shop\SaleChannel;
|
||||
use App\Traits\Model\Basic;
|
||||
@@ -15,7 +16,11 @@ class Offers
|
||||
{
|
||||
return [
|
||||
'articles' => Articles::getOptionsWithNature(),
|
||||
'article_visibilities' => Articles::getVisibilityMap(),
|
||||
'tariffs' => Tariffs::getOptions(),
|
||||
'tariff_statuses' => Tariffs::getStatusMap(),
|
||||
'tariff_status_labels' => Tariffs::getStatuses(),
|
||||
'tariff_pricelist_counts' => Tariffs::getPriceListCountMap(),
|
||||
'variations' => Variations::getOptions(),
|
||||
];
|
||||
}
|
||||
@@ -182,27 +187,72 @@ class Offers
|
||||
|
||||
$offers = Offer::query()
|
||||
->byArticle($articleId)
|
||||
->with('article')
|
||||
->with([
|
||||
'article',
|
||||
'tariff:id,status_id',
|
||||
'variation',
|
||||
])
|
||||
->get();
|
||||
|
||||
return $channels->map(function ($channel) use ($offers) {
|
||||
$priceValue = null;
|
||||
$candidateOffer = null;
|
||||
$allOffersForChannel = [];
|
||||
|
||||
foreach ($offers as $offer) {
|
||||
$priceCandidate = self::getPrice($offer->id, 1, $channel->id);
|
||||
|
||||
if ($priceCandidate && (float) $priceCandidate->price_taxed > 0) {
|
||||
$priceValue = $priceCandidate;
|
||||
break;
|
||||
// Get price list name
|
||||
$priceListName = null;
|
||||
if ($priceCandidate) {
|
||||
$priceListModel = PriceList::find($priceCandidate->price_list_id);
|
||||
$priceListName = $priceListModel ? $priceListModel->name : null;
|
||||
}
|
||||
|
||||
// Collect all offers with their details
|
||||
$allOffersForChannel[] = [
|
||||
'id' => $offer->id,
|
||||
'variation_name' => $offer->variation ? $offer->variation->name : null,
|
||||
'stock_current' => (int) $offer->stock_current,
|
||||
'status_id' => (int) $offer->status_id,
|
||||
'is_active' => (int) $offer->status_id === 1,
|
||||
'tariff_id' => $offer->tariff_id ? (int) $offer->tariff_id : null,
|
||||
'price_taxed' => (float) $priceCandidate->price_taxed,
|
||||
'quantity' => (int) $priceCandidate->quantity,
|
||||
'price_list_name' => $priceListName,
|
||||
];
|
||||
|
||||
// Keep first valid offer as the main candidate
|
||||
if (!$candidateOffer) {
|
||||
$priceValue = $priceCandidate;
|
||||
$candidateOffer = $offer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$offerId = $candidateOffer ? $candidateOffer->id : null;
|
||||
$offerStock = $candidateOffer ? (int) $candidateOffer->stock_current : null;
|
||||
$offerIsActive = $candidateOffer ? (int) $candidateOffer->status_id === 1 : false;
|
||||
$offerTariffStatus = $candidateOffer && $candidateOffer->tariff ? (int) $candidateOffer->tariff->status_id : null;
|
||||
$offerHasStock = $candidateOffer && $candidateOffer->stock_current !== null
|
||||
? (float) $candidateOffer->stock_current > 0
|
||||
: null;
|
||||
$offerTariffId = $candidateOffer && $candidateOffer->tariff_id ? (int) $candidateOffer->tariff_id : null;
|
||||
|
||||
return [
|
||||
'id' => $channel->id,
|
||||
'name' => $channel->name,
|
||||
'code' => $channel->code,
|
||||
'price_taxed' => $priceValue ? (float) $priceValue->price_taxed : null,
|
||||
'quantity' => $priceValue ? (int) $priceValue->quantity : null,
|
||||
'offer_id' => $offerId,
|
||||
'offer_is_active' => $offerIsActive,
|
||||
'offer_stock_current' => $offerStock,
|
||||
'offer_has_stock' => $offerHasStock,
|
||||
'tariff_status_id' => $offerTariffStatus,
|
||||
'tariff_id' => $offerTariffId,
|
||||
'all_offers' => $allOffersForChannel,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Repositories\Shop;
|
||||
|
||||
use App\Mail\Acheminement;
|
||||
use App\Mail\AlertePaiementAnnule;
|
||||
use App\Mail\ConfirmationCommande;
|
||||
use App\Mail\Preparation;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class OrderMails
|
||||
{
|
||||
@@ -37,4 +39,25 @@ class OrderMails
|
||||
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,36 +48,36 @@ class OrderMetrics
|
||||
|
||||
public static function countOfToday()
|
||||
{
|
||||
return Order::ofToday()->count();
|
||||
return Order::notCancelled()->ofToday()->count();
|
||||
}
|
||||
|
||||
public static function countOfLastWeek()
|
||||
{
|
||||
return Order::ofLastWeek()->count();
|
||||
return Order::notCancelled()->ofLastWeek()->count();
|
||||
}
|
||||
|
||||
public static function countOfLastMonth()
|
||||
{
|
||||
return Order::ofLastMonth()->count();
|
||||
return Order::notCancelled()->ofLastMonth()->count();
|
||||
}
|
||||
|
||||
public static function getTotalOfToday()
|
||||
{
|
||||
return Order::ofToday()->sum('total_taxed');
|
||||
return Order::notCancelled()->ofToday()->sum('total_taxed');
|
||||
}
|
||||
|
||||
public static function getTotalOfLastWeek()
|
||||
{
|
||||
return Order::ofLastWeek()->sum('total_taxed');
|
||||
return Order::notCancelled()->ofLastWeek()->sum('total_taxed');
|
||||
}
|
||||
|
||||
public static function getTotalOfLastMonth()
|
||||
{
|
||||
return Order::ofLastMonth()->sum('total_taxed');
|
||||
return Order::notCancelled()->ofLastMonth()->sum('total_taxed');
|
||||
}
|
||||
|
||||
public static function getModel()
|
||||
{
|
||||
return Order::query();
|
||||
return Order::notCancelled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,26 @@ class Orders
|
||||
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)
|
||||
{
|
||||
$data = array_flip(self::statuses());
|
||||
@@ -135,7 +155,7 @@ class Orders
|
||||
|
||||
public static function statuses()
|
||||
{
|
||||
return ['En attente', 'Préparation', 'Expédié', 'Livré'];
|
||||
return ['En attente', 'Préparation', 'Expédié', 'Livré', 'Annulé'];
|
||||
}
|
||||
|
||||
public static function getPaymentType($id)
|
||||
|
||||
@@ -104,7 +104,9 @@ class Paybox
|
||||
return true;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($invoice, $order, $reference, $payload, $existingPayment, &$shouldNotify) {
|
||||
$isCancelled = (int) $order->status === 4;
|
||||
|
||||
DB::transaction(function () use ($invoice, $order, $reference, $payload, $existingPayment, &$shouldNotify, $isCancelled) {
|
||||
$attributes = [
|
||||
'payment_type' => 1,
|
||||
'amount' => $invoice->total_shipped,
|
||||
@@ -134,14 +136,24 @@ class Paybox
|
||||
|
||||
Invoices::checkPayments($invoice->id);
|
||||
|
||||
$paidStatus = Orders::getStatusByName('Préparation');
|
||||
if ($paidStatus !== '' && (int) $order->status !== (int) $paidStatus) {
|
||||
$order->status = $paidStatus;
|
||||
$order->save();
|
||||
if (! $isCancelled) {
|
||||
$paidStatus = Orders::getStatusByName('Préparation');
|
||||
if ($paidStatus !== '' && (int) $order->status !== (int) $paidStatus) {
|
||||
$order->status = $paidStatus;
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($shouldNotify) {
|
||||
if ($isCancelled && $shouldNotify) {
|
||||
Log::warning('Paybox payment received on cancelled order', [
|
||||
'order_id' => $order->id,
|
||||
'order_ref' => $order->ref,
|
||||
'invoice_id' => $invoice->id,
|
||||
'reference' => $reference,
|
||||
]);
|
||||
OrderMails::sendCancelledOrderPaymentAlert($order->id, $reference);
|
||||
} elseif ($shouldNotify) {
|
||||
try {
|
||||
OrderMails::sendOrderConfirmed($order->id);
|
||||
} catch (\Throwable $exception) {
|
||||
|
||||
@@ -46,12 +46,34 @@ class PriceListValues
|
||||
{
|
||||
foreach ($values as $value) {
|
||||
$value['price_list_id'] = $price_list_id;
|
||||
if ($value['price']) {
|
||||
if (self::hasPrice($value)) {
|
||||
self::store($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function purgeRemovedValues($price_list_id, array $ids)
|
||||
{
|
||||
if (! count($ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PriceListValue::byPriceList($price_list_id)
|
||||
->whereIn('id', $ids)
|
||||
->delete();
|
||||
}
|
||||
|
||||
protected static function hasPrice($value): bool
|
||||
{
|
||||
if (! array_key_exists('price', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$price = $value['price'];
|
||||
|
||||
return $price !== null && $price !== '';
|
||||
}
|
||||
|
||||
public static function getModel()
|
||||
{
|
||||
return PriceListValue::query();
|
||||
|
||||
@@ -17,7 +17,7 @@ class PriceLists
|
||||
'taxes' => Taxes::getOptions(),
|
||||
'price_list' => [
|
||||
'tariff_id' => $tariffId,
|
||||
'price_list_values' => array_fill(0, 3, ''),
|
||||
'price_list_values' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -50,9 +50,8 @@ class PriceLists
|
||||
public static function edit($id)
|
||||
{
|
||||
$price_list = self::getFull($id)->toArray();
|
||||
$n = count($price_list['price_list_values']);
|
||||
if ($n <= 3) {
|
||||
$price_list['price_list_values'] += array_fill($n, 3 - $n, '');
|
||||
if (count($price_list['price_list_values']) === 0) {
|
||||
$price_list['price_list_values'][] = [];
|
||||
}
|
||||
|
||||
return $price_list;
|
||||
@@ -71,9 +70,14 @@ class PriceLists
|
||||
public static function store($data)
|
||||
{
|
||||
$id = $data['id'] ?? false;
|
||||
$price_list_values = $data['price_list_values'] ?? false;
|
||||
$price_list_values = $data['price_list_values'] ?? [];
|
||||
$deleted_values = array_map('intval', array_filter($data['deleted_price_list_value_ids'] ?? [], function ($value) {
|
||||
return $value !== null && $value !== '';
|
||||
}));
|
||||
unset($data['price_list_values']);
|
||||
unset($data['deleted_price_list_value_ids']);
|
||||
$price_list = $id ? self::update($data) : self::create($data);
|
||||
PriceListValues::purgeRemovedValues($price_list->id, $deleted_values);
|
||||
PriceListValues::storePrices($price_list->id, $price_list_values);
|
||||
|
||||
return $price_list;
|
||||
|
||||
@@ -49,7 +49,7 @@ class SaleChannels
|
||||
}
|
||||
}
|
||||
|
||||
return self::getByCode('EXP');
|
||||
return self::getByCode('POSTE');
|
||||
}
|
||||
|
||||
public static function getByCode($code)
|
||||
|
||||
@@ -8,8 +8,14 @@ class Searches
|
||||
{
|
||||
public static function search($options)
|
||||
{
|
||||
// Get article IDs from Scout search
|
||||
$searchResults = Article::search($options['search_name'])->get()->pluck('id');
|
||||
|
||||
// Filter to only include visible articles
|
||||
$visibleArticleIds = Article::whereIn('id', $searchResults)->visible()->pluck('id');
|
||||
|
||||
return collect(Articles::getArticlesToSell([
|
||||
'ids' => Article::search($options['search_name'])->get()->pluck('id'),
|
||||
'ids' => $visibleArticleIds,
|
||||
]))->sortBy('searchOrder')->toArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -50,11 +50,30 @@ class Tariffs
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
return Tariff::query();
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// --- The default avatar size
|
||||
'size' => 80,
|
||||
// Default configuration group for Gravatar
|
||||
'default' => [
|
||||
// --- The default avatar size
|
||||
'size' => 80,
|
||||
|
||||
// --- The default avatar to display if we have no results
|
||||
// (bool) false
|
||||
// (string) 404
|
||||
// (string) mm: (mystery-man) a simple, cartoon-style silhouetted outline of a person (does not vary by email hash).
|
||||
// (string) identicon: a geometric pattern based on an email hash.
|
||||
// (string) monsterid: a generated 'monster' with different colors, faces, etc.
|
||||
// (string) wavatar: generated faces with differing features and backgrounds.
|
||||
// (string) retro: awesome generated, 8-bit arcade-style pixelated faces.
|
||||
'default' => 'identicon',
|
||||
// --- The default avatar to display if we have no results
|
||||
// (bool) false
|
||||
// (string) 404
|
||||
// (string) mm: (mystery-man) a simple, cartoon-style silhouetted outline of a person (does not vary by email hash).
|
||||
// (string) identicon: a geometric pattern based on an email hash.
|
||||
// (string) monsterid: a generated 'monster' with different colors, faces, etc.
|
||||
// (string) wavatar: generated faces with differing features and backgrounds.
|
||||
// (string) retro: awesome generated, 8-bit arcade-style pixelated faces.
|
||||
'fallback' => 'identicon',
|
||||
|
||||
// --- Set the type of avatars we allow to show
|
||||
// - g: suitable for display on all websites with any audience type.
|
||||
// - pg: may contain rude gestures, provocatively dressed individuals, the lesser swear words, or mild violence.
|
||||
// - r: may contain such things as harsh profanity, intense violence, nudity, or hard drug use.
|
||||
// - x: may contain hardcore sexual imagery or extremely disturbing violence.
|
||||
'maxRating' => 'g',
|
||||
// --- Whether to use HTTPS protocol
|
||||
'secure' => false,
|
||||
|
||||
// --- Set the type of avatars we allow to show
|
||||
// - g: suitable for display on all websites with any audience type.
|
||||
// - pg: may contain rude gestures, provocatively dressed individuals, the lesser swear words, or mild violence.
|
||||
// - r: may contain such things as harsh profanity, intense violence, nudity, or hard drug use.
|
||||
// - x: may contain hardcore sexual imagery or extremely disturbing violence.
|
||||
'maximumRating' => 'g',
|
||||
|
||||
// --- Force default image to always display
|
||||
'forceDefault' => false,
|
||||
|
||||
// --- Optional file extension appended to URL
|
||||
'forceExtension' => 'jpg',
|
||||
]
|
||||
];
|
||||
|
||||
@@ -126,7 +126,7 @@ return [
|
||||
* The path where to store temporary files while performing image conversions.
|
||||
* If set to null, storage_path('media-library/temp') will be used.
|
||||
*/
|
||||
'temporary_directory_path' => null,
|
||||
'temporary_directory_path' => env('MEDIA_LIBRARY_TEMP_PATH', storage_path('media-library/temp')),
|
||||
|
||||
/*
|
||||
* The engine that should perform the image conversions.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shop_articles', function (Blueprint $table) {
|
||||
$table->dropUnique('ref');
|
||||
$table->unique(['ref', 'deleted_at'], 'shop_articles_ref_deleted_at_unique');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('shop_articles', function (Blueprint $table) {
|
||||
$table->dropUnique('shop_articles_ref_deleted_at_unique');
|
||||
$table->unique('ref');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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é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é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)]);
|
||||
}
|
||||
};
|
||||
@@ -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)]);
|
||||
}
|
||||
};
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('mail_templates')->insert([
|
||||
'mailable' => 'App\\Mail\\AlerteStock',
|
||||
'subject' => json_encode(['fr' => '[Stock bas] {{article}} — {{stock_restant}} unités restantes'], JSON_UNESCAPED_UNICODE),
|
||||
'html_template' => json_encode(['fr' => $this->getHtmlTemplate()], JSON_UNESCAPED_UNICODE),
|
||||
'text_template' => json_encode(['fr' => $this->getTextTemplate()], JSON_UNESCAPED_UNICODE),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('mail_templates')
|
||||
->where('mailable', 'App\\Mail\\AlerteStock')
|
||||
->delete();
|
||||
}
|
||||
|
||||
private function getHtmlTemplate(): string
|
||||
{
|
||||
return '<table style="min-width: 100%;" width="100%" cellspacing="0" cellpadding="0">'
|
||||
.'<tbody><tr><td>'
|
||||
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody><tr>'
|
||||
.'<td style="margin: 0px auto; padding: 32px 0px 24px 4px; vertical-align: top;" align="center">'
|
||||
.'<a href="https://www.jardinenvie.com/" style="text-decoration: none; color: #000000;">'
|
||||
.'<img alt="Jardin\'enVie" src="https://boutique.jardinenvie.com/img/logo.png" style="margin: 0px auto; display: block;" width="300" height="138" border="0" /></a></td>'
|
||||
.'</tr></tbody></table>'
|
||||
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody><tr><td style="margin: 0 auto; padding: 0px; vertical-align: top;">'
|
||||
.'<table style="border-collapse: collapse; margin: 0 auto; width: 512px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody>'
|
||||
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 40px; color: #000000; font-size: 28px; padding: 20px 0px 10px 0px; font-weight: 800; text-align: center;">'
|
||||
.'Alerte stock bas</td></tr>'
|
||||
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 28px; color: #000000; font-size: 16px; text-align: center; padding: 10px 0 30px 0;">'
|
||||
.'<p>Le stock de l\'article <strong>{{article}}</strong> (offre n°{{offre}}) '
|
||||
.'a atteint le seuil d\'alerte.</p>'
|
||||
.'<p style="font-size: 24px; font-weight: bold; color: #c0392b; padding: 10px 0;">{{stock_restant}} unités restantes</p>'
|
||||
.'<p>Seuil d\'alerte configuré : {{seuil}} unités</p>'
|
||||
.'<p style="padding-top: 15px; color: #666;">Pensez à ré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ô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';
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('mail_templates')->insert([
|
||||
'mailable' => 'App\\Mail\\AlertePaiementAnnule',
|
||||
'subject' => json_encode(['fr' => '[URGENT] Paiement reçu sur commande annulée {{numero_commande}}'], JSON_UNESCAPED_UNICODE),
|
||||
'html_template' => json_encode(['fr' => $this->getHtmlTemplate()], JSON_UNESCAPED_UNICODE),
|
||||
'text_template' => json_encode(['fr' => $this->getTextTemplate()], JSON_UNESCAPED_UNICODE),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('mail_templates')
|
||||
->where('mailable', 'App\\Mail\\AlertePaiementAnnule')
|
||||
->delete();
|
||||
}
|
||||
|
||||
private function getHtmlTemplate(): string
|
||||
{
|
||||
return '<table style="min-width: 100%;" width="100%" cellspacing="0" cellpadding="0">'
|
||||
.'<tbody><tr><td>'
|
||||
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody><tr>'
|
||||
.'<td style="margin: 0px auto; padding: 32px 0px 24px 4px; vertical-align: top;" align="center">'
|
||||
.'<a href="https://www.jardinenvie.com/" style="text-decoration: none; color: #000000;">'
|
||||
.'<img alt="Jardin\'enVie" src="https://boutique.jardinenvie.com/img/logo.png" style="margin: 0px auto; display: block;" width="300" height="138" border="0" /></a></td>'
|
||||
.'</tr></tbody></table>'
|
||||
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody><tr><td style="margin: 0 auto; padding: 0px; vertical-align: top;">'
|
||||
.'<table style="border-collapse: collapse; margin: 0 auto; width: 512px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody>'
|
||||
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 40px; color: #c0392b; font-size: 28px; padding: 20px 0px 10px 0px; font-weight: 800; text-align: center;">'
|
||||
.'⚠ Paiement sur commande annulé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 été reçu sur une commande <strong>annulée</strong>.</p>'
|
||||
.'<p style="font-size: 20px; font-weight: bold; color: #c0392b; padding: 10px 0;">Un remboursement est probablement né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éf. paiement :</td><td>{{reference_paiement}}</td></tr>'
|
||||
.'</table>'
|
||||
.'<p style="padding-top: 15px; color: #666;">Veuillez procéder au remboursement du client dans les plus brefs dé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ô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';
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('mail_templates')
|
||||
->where('mailable', 'App\\Mail\\AlerteStock')
|
||||
->update([
|
||||
'html_template' => json_encode(['fr' => $this->getHtmlTemplate()], JSON_UNESCAPED_UNICODE),
|
||||
'text_template' => json_encode(['fr' => $this->getTextTemplate()], JSON_UNESCAPED_UNICODE),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('mail_templates')
|
||||
->where('mailable', 'App\\Mail\\AlerteStock')
|
||||
->update([
|
||||
'html_template' => json_encode(['fr' => $this->getOldHtmlTemplate()], JSON_UNESCAPED_UNICODE),
|
||||
'text_template' => json_encode(['fr' => $this->getOldTextTemplate()], JSON_UNESCAPED_UNICODE),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getHtmlTemplate(): string
|
||||
{
|
||||
return '<table style="min-width: 100%;" width="100%" cellspacing="0" cellpadding="0">'
|
||||
.'<tbody><tr><td>'
|
||||
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody><tr>'
|
||||
.'<td style="margin: 0px auto; padding: 32px 0px 24px 4px; vertical-align: top;" align="center">'
|
||||
.'<a href="https://www.jardinenvie.com/" style="text-decoration: none; color: #000000;">'
|
||||
.'<img alt="Jardin\'enVie" src="https://boutique.jardinenvie.com/img/logo.png" style="margin: 0px auto; display: block;" width="300" height="138" border="0" /></a></td>'
|
||||
.'</tr></tbody></table>'
|
||||
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody><tr><td style="margin: 0 auto; padding: 0px; vertical-align: top;">'
|
||||
.'<table style="border-collapse: collapse; margin: 0 auto; width: 512px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||
.'<tbody>'
|
||||
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 40px; color: #000000; font-size: 28px; padding: 20px 0px 10px 0px; font-weight: 800; text-align: center;">'
|
||||
.'Alerte stock bas</td></tr>'
|
||||
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 28px; color: #000000; font-size: 16px; text-align: center; padding: 10px 0 30px 0;">'
|
||||
.'<p>Le stock de l\'article <strong>{{article}}</strong> (offre n°{{offre}}) '
|
||||
.'a atteint le seuil d\'alerte.</p>'
|
||||
.'<p style="font-size: 24px; font-weight: bold; color: #c0392b; padding: 10px 0;">{{stock_restant}} unités restantes</p>'
|
||||
.'<p>Seuil d\'alerte configuré : {{seuil}} unité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>'
|
||||
.' <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 à ré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ô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°{{offre}}) '
|
||||
.'a atteint le seuil d\'alerte.</p>'
|
||||
.'<p style="font-size: 24px; font-weight: bold; color: #c0392b; padding: 10px 0;">{{stock_restant}} unités restantes</p>'
|
||||
.'<p>Seuil d\'alerte configuré : {{seuil}} unités</p>'
|
||||
.'<p style="padding-top: 15px; color: #666;">Pensez à ré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ô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';
|
||||
}
|
||||
};
|
||||
9
resources/lang/en/fg.php
Normal file
9
resources/lang/en/fg.php
Normal 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
9
resources/lang/fr/fg.php
Normal 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',
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,105 @@ 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%;
|
||||
}
|
||||
}
|
||||
|
||||
/* -- 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
])
|
||||
|
||||
@section('content')
|
||||
@include('Admin.Shop.Articles.form')
|
||||
@include('Admin.Shop.Articles.form', [
|
||||
'cancel_url' => route('Admin.Shop.Articles.index'),
|
||||
])
|
||||
@endsection
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
@if ($category['id'] ?? false)
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a href="{{ route('Admin.Shop.Articles.index', ['category_id' => $category['id']]) }}" class="btn btn-outline-primary">
|
||||
Voir les articles de ce rayon
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<div class="row mb-3">
|
||||
|
||||
@@ -14,57 +14,30 @@
|
||||
|
||||
@push('js')
|
||||
<script>
|
||||
var position = '';
|
||||
var target_node = '';
|
||||
|
||||
$(function() {
|
||||
|
||||
var $tree = $('#tree1').tree({
|
||||
dragAndDrop: true,
|
||||
onDragStop: handleMove,
|
||||
autoOpen: 0
|
||||
});
|
||||
|
||||
|
||||
$tree.on('tree.move', function(e) {
|
||||
// e.preventDefault();
|
||||
var position = e.move_info.position;
|
||||
var target_node = e.move_info.target_node;
|
||||
var moved_node = e.move_info.moved_node;
|
||||
|
||||
position = e.move_info.position;
|
||||
target_node = e.move_info.target_node;
|
||||
|
||||
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);
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: "{{ route('Admin.Shop.Categories.moveTree') }}",
|
||||
data: {
|
||||
node_id: moved_node.id,
|
||||
type: position,
|
||||
target_id: target_node.id
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
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>
|
||||
@endpush
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
])
|
||||
|
||||
@section('content')
|
||||
{{ Form::open(['route' => 'Admin.Shop.Contents.store', 'id' => 'content-form', 'autocomplete' => 'off']) }}
|
||||
{{ Form::open(['route' => 'Admin.Shop.Contents.store', 'id' => 'homepage-form', 'autocomplete' => 'off']) }}
|
||||
<input type="hidden" name="id" value="{{ $content['id'] }}">
|
||||
@include('Admin.Shop.Contents.form')
|
||||
</form>
|
||||
|
||||
@@ -25,13 +25,21 @@
|
||||
@foreach ($lastOrders as $order)
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ route('Admin.Shop.Customers.edit', ['id' => $order->customer->id]) }}"
|
||||
class="alert-link green">
|
||||
{{ $order->customer->first_name }}
|
||||
{{ $order->customer->last_name }}
|
||||
</a>
|
||||
@if ($order->customer)
|
||||
<a href="{{ route('Admin.Shop.Customers.edit', ['id' => $order->customer->id]) }}"
|
||||
class="alert-link green">
|
||||
{{ $order->customer->first_name }}
|
||||
{{ $order->customer->last_name }}
|
||||
</a>
|
||||
@else
|
||||
<span class="text-muted">Client supprimé</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($order->customer)
|
||||
{{ $order->customer->city }} ({{ substr($order->customer->zipcode, 0, 2) }})
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $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 class="text-right font-weight-bold">
|
||||
{{ $order->total_shipped }} €
|
||||
|
||||
@@ -1,49 +1,79 @@
|
||||
{{ 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', [
|
||||
'name' => 'article_id',
|
||||
'id_name' => 'article_id',
|
||||
'list' => $articles ?? null,
|
||||
'value' => $offer['article_id'] ?? null,
|
||||
'with_empty' => '',
|
||||
'class' => 'select2 select_article',
|
||||
'label' => 'Article',
|
||||
'required' => true,
|
||||
])
|
||||
<div class="d-flex align-items-end">
|
||||
<div class="flex-grow-1">
|
||||
@include('components.form.select', [
|
||||
'name' => 'article_id',
|
||||
'id_name' => 'article_id',
|
||||
'list' => $articles ?? null,
|
||||
'value' => $offer['article_id'] ?? null,
|
||||
'with_empty' => '',
|
||||
'class' => 'select2 select_article',
|
||||
'label' => 'Article',
|
||||
'required' => true,
|
||||
])
|
||||
</div>
|
||||
<a id="edit-article-link" href="#" class="btn btn-sm btn-outline-secondary ml-2 mb-1" title="Modifier l'article" style="display:none;">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-4">
|
||||
@include('components.form.select', [
|
||||
'name' => 'variation_id',
|
||||
'id_name' => 'variation_id',
|
||||
'list' => $variations ?? null,
|
||||
'value' => $offer['variation_id'] ?? null,
|
||||
'with_empty' => '',
|
||||
'class' => 'select2 select_variation',
|
||||
'label' => __('shop.packages.name'),
|
||||
'required' => true,
|
||||
])
|
||||
<div class="d-flex align-items-end">
|
||||
<div class="flex-grow-1">
|
||||
@include('components.form.select', [
|
||||
'name' => 'variation_id',
|
||||
'id_name' => 'variation_id',
|
||||
'list' => $variations ?? null,
|
||||
'value' => $offer['variation_id'] ?? null,
|
||||
'with_empty' => '',
|
||||
'class' => 'select2 select_variation',
|
||||
'label' => __('shop.packages.name'),
|
||||
'required' => true,
|
||||
])
|
||||
</div>
|
||||
<a id="edit-variation-link" href="#" class="btn btn-sm btn-outline-secondary ml-2 mb-1" title="Modifier la déclinaison" style="display:none;">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
@include('components.form.select', [
|
||||
'name' => 'tariff_id',
|
||||
'id_name' => 'tariff_id',
|
||||
'list' => $tariffs ?? null,
|
||||
'value' => $offer['tariff_id'] ?? null,
|
||||
'with_empty' => '',
|
||||
'class' => 'select2 select_tariffs',
|
||||
'label' => 'Tarif',
|
||||
'required' => true,
|
||||
])
|
||||
<div class="d-flex align-items-end">
|
||||
<div class="flex-grow-1">
|
||||
@include('components.form.select', [
|
||||
'name' => 'tariff_id',
|
||||
'id_name' => 'tariff_id',
|
||||
'list' => $tariffs ?? null,
|
||||
'value' => $offer['tariff_id'] ?? null,
|
||||
'with_empty' => '',
|
||||
'class' => 'select2 select_tariffs',
|
||||
'label' => 'Tarif',
|
||||
'required' => true,
|
||||
])
|
||||
</div>
|
||||
<a id="edit-tariff-link" href="#" class="btn btn-sm btn-outline-secondary ml-2 mb-1" title="Modifier le tarif" style="display:none;">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="col-2">
|
||||
@include('components.form.input', [
|
||||
'name' => 'weight',
|
||||
'value' => $offer['weight'] ?? null,
|
||||
@@ -51,6 +81,15 @@
|
||||
'required' => true,
|
||||
])
|
||||
</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>
|
||||
|
||||
@component('components.card', ['title' => 'Disponibilité', 'class' => 'mt-5'])
|
||||
@@ -96,13 +135,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 +149,83 @@
|
||||
{!! 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
|
||||
$('#status_id').bootstrapToggle();
|
||||
|
||||
// Article visibility badges in select2
|
||||
var articleVisibilities = {!! json_encode($article_visibilities ?? (object)[]) !!};
|
||||
|
||||
function formatArticle(item) {
|
||||
if (!item.id) return item.text;
|
||||
var visible = articleVisibilities[item.id];
|
||||
var badge = (visible == 1)
|
||||
? '<span class="badge badge-success" style="font-size:0.75em;margin-left:4px;">Visible</span>'
|
||||
: '<span class="badge badge-warning" style="font-size:0.75em;margin-left:4px;">Invisible</span>';
|
||||
return $('<span style="display:flex;justify-content:space-between;align-items:center;width:100%;">' + item.text + badge + '</span>');
|
||||
}
|
||||
|
||||
$('#article_id').select2('destroy').select2({
|
||||
placeholder: "{{ __('select_a_value') }}",
|
||||
allowClear: false,
|
||||
width: { value: '100%' },
|
||||
templateResult: formatArticle,
|
||||
templateSelection: formatArticle
|
||||
});
|
||||
|
||||
// Tariff status badges in select2
|
||||
var tariffStatuses = {!! json_encode($tariff_statuses ?? (object)[]) !!};
|
||||
var tariffStatusLabels = {!! json_encode($tariff_status_labels ?? []) !!};
|
||||
var tariffStatusColors = {0: '#28a745', 1: '#ffc107', 2: '#6c757d', 3: '#dc3545'};
|
||||
var tariffPLCounts = {!! json_encode($tariff_pricelist_counts ?? (object)[]) !!};
|
||||
|
||||
function formatTariff(item) {
|
||||
if (!item.id) return item.text;
|
||||
var statusId = tariffStatuses[item.id];
|
||||
var color = tariffStatusColors[statusId] || '#6c757d';
|
||||
var label = tariffStatusLabels[statusId] || '';
|
||||
var plCount = tariffPLCounts[item.id] || 0;
|
||||
var warning = plCount == 0 ? '<span style="margin-left:4px;cursor:help;" title="Aucune liste de prix"><i class="fas fa-exclamation-triangle text-danger" style="font-size:1.4em;vertical-align:-2px;"></i></span>' : '';
|
||||
return $('<span style="display:flex;justify-content:space-between;align-items:center;width:100%;">' + item.text + '<span>' + warning + '<span class="badge" style="background:' + color + ';color:#fff;font-size:0.75em;margin-left:4px;">' + label + '</span></span></span>');
|
||||
}
|
||||
|
||||
$('#tariff_id').select2('destroy').select2({
|
||||
placeholder: "{{ __('select_a_value') }}",
|
||||
allowClear: false,
|
||||
width: { value: '100%' },
|
||||
templateResult: formatTariff,
|
||||
templateSelection: formatTariff
|
||||
});
|
||||
|
||||
function updateEditLink(selectId, linkId, routeTemplate) {
|
||||
var val = $('#' + selectId).val();
|
||||
var $link = $('#' + linkId);
|
||||
if (val) {
|
||||
$link.attr('href', routeTemplate.replace('__ID__', val)).show();
|
||||
} else {
|
||||
$link.hide();
|
||||
}
|
||||
}
|
||||
|
||||
var articleRoute = '{{ route('Admin.Shop.Articles.edit', ['id' => '__ID__']) }}';
|
||||
var variationRoute = '{{ route('Admin.Shop.Variations.edit', ['id' => '__ID__']) }}';
|
||||
var tariffRoute = '{{ route('Admin.Shop.Tariffs.edit', ['id' => '__ID__']) }}';
|
||||
|
||||
// Init on page load
|
||||
updateEditLink('article_id', 'edit-article-link', articleRoute);
|
||||
updateEditLink('variation_id', 'edit-variation-link', variationRoute);
|
||||
updateEditLink('tariff_id', 'edit-tariff-link', tariffRoute);
|
||||
|
||||
// Update on change
|
||||
$('#article_id').on('change', function() {
|
||||
updateEditLink('article_id', 'edit-article-link', articleRoute);
|
||||
});
|
||||
$('#variation_id').on('change', function() {
|
||||
updateEditLink('variation_id', 'edit-variation-link', variationRoute);
|
||||
});
|
||||
$('#tariff_id').on('change', function() {
|
||||
updateEditLink('tariff_id', 'edit-tariff-link', tariffRoute);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -15,4 +15,9 @@
|
||||
<td>
|
||||
@include('components.form.inputs.money', ['name' => 'price_list_values[' . $index . '][price_taxed]', 'value' => $price_list_value['price_taxed'] ?? null, 'required' => true, 'class' => 'price_taxed'])
|
||||
</td>
|
||||
</tr>
|
||||
<td class="text-center align-middle">
|
||||
<button type="button" class="btn btn-outline-danger btn-xs remove-price" title="{{ __('Supprimer') }}">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -30,12 +30,18 @@
|
||||
<th>Unit. HT</th>
|
||||
<th>TVA</th>
|
||||
<th>Unit. TTC</th>
|
||||
<th class="text-center" style="width: 60px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($price_list['price_list_values'] as $price_list_value)
|
||||
@include('Admin.Shop.PriceListValues.partials.row_price', ['index' => $loop->index])
|
||||
@php($priceListValues = $price_list['price_list_values'] ?? [])
|
||||
@php($nextIndex = count($priceListValues))
|
||||
|
||||
@foreach ($priceListValues as $index => $price_list_value)
|
||||
@include('Admin.Shop.PriceListValues.partials.row_price', ['index' => $index, 'price_list_value' => $price_list_value])
|
||||
@endforeach
|
||||
|
||||
@include('Admin.Shop.PriceListValues.partials.row_price', ['index' => $nextIndex, 'price_list_value' => []])
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
@@ -49,21 +55,30 @@
|
||||
</tfoot>
|
||||
</table>
|
||||
@endcomponent
|
||||
|
||||
<div id="deleted-price-list-values"></div>
|
||||
</form>
|
||||
|
||||
|
||||
<script>
|
||||
var priceRowIndex = 0;
|
||||
var lastRemovedIndex = null;
|
||||
|
||||
function handleAddPrice() {
|
||||
$('#add_price').click( function () {
|
||||
var index = $('#prices-table tbody tr').length;
|
||||
$.get("{{ route('Admin.Shop.PriceListValues.addPrice') }}/" + index, function(data) {
|
||||
$("#prices-table").append(data);
|
||||
})
|
||||
handlePrices();
|
||||
addEmptyPriceRow();
|
||||
})
|
||||
}
|
||||
|
||||
function addEmptyPriceRow() {
|
||||
var index = nextPriceRowIndex();
|
||||
$.get("{{ route('Admin.Shop.PriceListValues.addPrice') }}/" + index, function(data) {
|
||||
$("#prices-table tbody").append(data);
|
||||
handlePrices();
|
||||
handleRemovePrice();
|
||||
lastRemovedIndex = null;
|
||||
});
|
||||
}
|
||||
|
||||
function handle_prices() {
|
||||
$('.price').change(function() {
|
||||
$col_tax = $(this).parent().parent().find('.tax');
|
||||
@@ -101,9 +116,82 @@
|
||||
handle_prices_taxed();
|
||||
}
|
||||
|
||||
function handleRemovePrice() {
|
||||
$('#prices-table').off('click', '.remove-price').on('click', '.remove-price', function() {
|
||||
var $row = $(this).closest('tr');
|
||||
var idx = extractRowIndex($row);
|
||||
var id = $row.find('input[name$="[id]"]').val();
|
||||
if (id) {
|
||||
registerDeletedPrice(id);
|
||||
}
|
||||
$row.remove();
|
||||
lastRemovedIndex = idx;
|
||||
ensureAtLeastOneRow();
|
||||
});
|
||||
}
|
||||
|
||||
function registerDeletedPrice(id) {
|
||||
var $container = $('#deleted-price-list-values');
|
||||
if ($container.find('input[value="' + id + '"]').length === 0) {
|
||||
$container.append('<input type="hidden" name="deleted_price_list_value_ids[]" value="' + id + '">');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAtLeastOneRow() {
|
||||
if ($('#prices-table tbody tr').length === 0) {
|
||||
if (lastRemovedIndex !== null) {
|
||||
$.get("{{ route('Admin.Shop.PriceListValues.addPrice') }}/" + lastRemovedIndex, function(data) {
|
||||
$("#prices-table tbody").append(data);
|
||||
handlePrices();
|
||||
handleRemovePrice();
|
||||
});
|
||||
} else {
|
||||
addEmptyPriceRow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function computeStartingIndex() {
|
||||
var maxIndex = -1;
|
||||
$('#prices-table tbody tr').each(function() {
|
||||
var $idInput = $(this).find('input[name$="[id]"]');
|
||||
var name = $idInput.attr('name');
|
||||
if (name) {
|
||||
var matches = name.match(/price_list_values\[(\d+)\]\[id\]/);
|
||||
if (matches && matches[1]) {
|
||||
var idx = parseInt(matches[1], 10);
|
||||
if (!isNaN(idx) && idx > maxIndex) {
|
||||
maxIndex = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return maxIndex + 1;
|
||||
}
|
||||
|
||||
function nextPriceRowIndex() {
|
||||
return priceRowIndex++;
|
||||
}
|
||||
|
||||
function extractRowIndex($row) {
|
||||
var $idInput = $row.find('input[name$="[id]"]');
|
||||
var name = $idInput.attr('name');
|
||||
if (!name) {
|
||||
return priceRowIndex;
|
||||
}
|
||||
|
||||
var matches = name.match(/price_list_values\[(\d+)\]\[id\]/);
|
||||
|
||||
return matches && matches[1] ? parseInt(matches[1], 10) : priceRowIndex;
|
||||
}
|
||||
|
||||
$(function() {
|
||||
priceRowIndex = computeStartingIndex();
|
||||
handleAddPrice();
|
||||
handlePrices();
|
||||
handleRemovePrice();
|
||||
ensureAtLeastOneRow();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
@include('load.form.password_toggle')
|
||||
<div class="modal-body">
|
||||
<div class="row" style="padding: 10px 20px;">
|
||||
<div class="col-xs-12 text-center" id="changePasswordMessage"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
@if ($article['offers']['semences'] ?? false)
|
||||
@include('Shop.Articles.partials.addBasket', [
|
||||
'data' => $article['offers']['semences'],
|
||||
'title' => 'Semences',
|
||||
'model' => 'semences',
|
||||
'bgClass' => 'bg-green-light',
|
||||
])
|
||||
@endif
|
||||
@php
|
||||
// Check if article is not visible OR has no offers at all
|
||||
$hasNoOffers = empty($article['offers'] ?? false) || !array_filter($article['offers']);
|
||||
$shouldShowComingSoon = !($article['visible'] ?? true) || $hasNoOffers;
|
||||
@endphp
|
||||
|
||||
@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',
|
||||
])
|
||||
@if ($shouldShowComingSoon)
|
||||
{{-- Display "Coming Soon" box when article is not visible or has no offers --}}
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center p-4">
|
||||
<h4 class="text-info mb-3">
|
||||
<i class="fas fa-clock"></i>
|
||||
</h4>
|
||||
<h5 class="card-title mb-0">Bientôt disponible</h5>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
{{-- Display normal offers for visible articles with available offers --}}
|
||||
@foreach ($article['offers'] as $natureKey => $natureOffers)
|
||||
@if (!empty($natureOffers))
|
||||
@include('Shop.Articles.partials.addBasket', [
|
||||
'data' => $natureOffers,
|
||||
'title' => ucfirst($natureKey),
|
||||
'model' => $natureKey,
|
||||
'bgClass' => 'bg-green-light',
|
||||
])
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@include('load.basket')
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
'name' => 'quantity',
|
||||
'class' => 'quantity',
|
||||
'id_name' => $model . '-quantity',
|
||||
'value' => (int) $data[0]['prices'][0]['quantity'],
|
||||
'min' => $data[0]['prices'][0]['quantity'],
|
||||
'value' => 1,
|
||||
'min' => 1,
|
||||
'max' => $data[0]['stock'] ?? false,
|
||||
'step' => 1,
|
||||
])
|
||||
</div>
|
||||
@@ -44,10 +45,35 @@
|
||||
|
||||
@push('js')
|
||||
<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() {
|
||||
update{{ ucfirst($model) }}Max();
|
||||
setPrice('{{ $model }}');
|
||||
});
|
||||
$('#{{ $model }}-offer_id').change(function() {
|
||||
update{{ ucfirst($model) }}Max();
|
||||
setPrice('{{ $model }}');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5 col-xs-12 text-justify">
|
||||
{!! $article['description']['semences'] ?? null !!}
|
||||
{!! $article['description']['plants'] ?? null !!}
|
||||
{!! $article['description']['variety'] ?? null !!}
|
||||
{!! $article['description']['merchandise'] ?? null !!}
|
||||
{!! $article['description']['description'] ?? null !!}
|
||||
|
||||
@if ($article['description']['plus'] ?? false)
|
||||
<h3>Spécificités</h3>
|
||||
@@ -48,35 +47,107 @@
|
||||
|
||||
</div>
|
||||
<div class="col-lg-3 col-xs-12">
|
||||
@if (config('app.debug') && ($article['current_sale_channel'] ?? false))
|
||||
<div class="alert alert-info p-2 mb-3">
|
||||
<strong>Canal actif :</strong>
|
||||
{{ $article['current_sale_channel']['name'] ?? 'N/A' }}
|
||||
<span class="d-block small text-muted">
|
||||
ID {{ $article['current_sale_channel']['id'] ?? '–' }} · Code {{ $article['current_sale_channel']['code'] ?? '–' }}
|
||||
</span>
|
||||
@if (!empty($article['available_sale_channels']))
|
||||
<hr class="my-2">
|
||||
<strong class="d-block">Offres disponibles dans :</strong>
|
||||
<ul class="list-unstyled mb-0 small">
|
||||
@foreach ($article['available_sale_channels'] as $channel)
|
||||
<li class="d-flex justify-content-between align-items-start">
|
||||
<span>
|
||||
@if (auth('web')->check() && !empty($article['available_sale_channels']))
|
||||
<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 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;
|
||||
@endphp
|
||||
@foreach ($article['available_sale_channels'] as $channel)
|
||||
@php
|
||||
$isCurrentChannel = $currentSaleChannelId === $channel['id'];
|
||||
$priceTaxed = $channel['price_taxed'] ?? null;
|
||||
$quantity = $channel['quantity'] ?? null;
|
||||
$offerStock = $channel['offer_stock_current'] ?? null;
|
||||
$offerIsActive = $channel['offer_is_active'] ?? false;
|
||||
$offerHasStock = $channel['offer_has_stock'] ?? null;
|
||||
$highlightStyle = $isCurrentChannel ? 'background-color: rgba(0, 0, 0, 0.06);' : '';
|
||||
$nameClass = ($offerIsActive && $offerHasStock !== false) ? '' : 'text-muted';
|
||||
$flags = [];
|
||||
if (! $offerIsActive) {
|
||||
$flags[] = 'inactive';
|
||||
}
|
||||
if ($offerHasStock === false) {
|
||||
$flags[] = 'no-stock';
|
||||
}
|
||||
@endphp
|
||||
<li style="{{ $highlightStyle }}">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<span class="{{ $nameClass }}">
|
||||
• {{ $channel['name'] }}
|
||||
<span class="d-block text-muted" style="font-size: 0.85em; padding-left: 0.9em;">code {{ $channel['code'] }}</span>
|
||||
</span>
|
||||
@if (isset($channel['price_taxed']))
|
||||
<span class="ml-2 text-nowrap text-right">
|
||||
{{ number_format($channel['price_taxed'], 2, ',', ' ') }} € TTC
|
||||
@if (! empty($channel['quantity']))
|
||||
<span class="d-block text-muted" style="font-size: 0.85em;">Qté min. {{ $channel['quantity'] }}</span>
|
||||
@endif
|
||||
<span class="d-block text-muted" style="font-size: 0.85em; padding-left: 0.9em;">
|
||||
Code {{ $channel['code'] }}{!! $flags ? ' · <strong class="text-dark">'.implode('</strong> · <strong class="text-dark">', $flags).'</strong>' : '' !!}
|
||||
</span>
|
||||
</span>
|
||||
@if ($priceTaxed !== null)
|
||||
@php
|
||||
$tariffId = $channel['tariff_id'] ?? null;
|
||||
@endphp
|
||||
@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">
|
||||
{{ number_format($priceTaxed, 2, ',', ' ') }} € TTC
|
||||
@if (! empty($quantity))
|
||||
<span class="d-block text-muted" style="font-size: 0.85em;">Qté min. {{ $quantity }}</span>
|
||||
@endif
|
||||
</a>
|
||||
@else
|
||||
<span class="ml-2 text-nowrap text-right {{ $nameClass }}">
|
||||
{{ number_format($priceTaxed, 2, ',', ' ') }} € TTC
|
||||
@if (! empty($quantity))
|
||||
<span class="d-block text-muted" style="font-size: 0.85em;">Qté min. {{ $quantity }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="ml-2 text-muted">–</span>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
@if (!empty($channel['all_offers']))
|
||||
<ul class="list-unstyled mb-0 mt-1" style="padding-left: 0.75em;">
|
||||
@foreach ($channel['all_offers'] as $offer)
|
||||
@php
|
||||
$isSelectedOffer = $offer['id'] === $channel['offer_id'];
|
||||
$offerClass = $offer['is_active'] ? 'text-dark' : 'text-muted';
|
||||
$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" 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>
|
||||
@if ($offer['variation_name'])
|
||||
{{ $offer['variation_name'] }}
|
||||
@endif
|
||||
- Stock: <strong class="{{ $stockClass }}">{{ $offer['stock_current'] }}</strong>
|
||||
@if (!$offer['is_active'])
|
||||
<span class="text-muted">(inactive)</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-right text-nowrap ml-2">
|
||||
{{ number_format($offer['price_taxed'], 2, ',', ' ') }} €
|
||||
@if ($offer['quantity'] > 1)
|
||||
<span class="text-muted">(min {{ $offer['quantity'] }})</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@include('Shop.Articles.partials.ArticleAddBasket')
|
||||
@@ -85,3 +156,67 @@
|
||||
@endsection
|
||||
|
||||
@include('load.layout.modal')
|
||||
|
||||
@if (auth('web')->check() && !empty($article['available_sale_channels']))
|
||||
@push('styles')
|
||||
<style>
|
||||
#article-admin-offers .admin-link-group {
|
||||
transition: background-color 0.15s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#article-admin-offers .admin-price-link {
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
margin: -2px -4px;
|
||||
}
|
||||
|
||||
#article-admin-offers .admin-offer-link {
|
||||
display: block;
|
||||
padding: 2px 4px;
|
||||
margin: -2px -4px;
|
||||
}
|
||||
|
||||
#article-admin-offers .admin-link-group:hover,
|
||||
#article-admin-offers .admin-link-group:focus,
|
||||
#article-admin-offers .admin-link-group.linked-hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const container = document.getElementById('article-admin-offers');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = Array.from(container.querySelectorAll('a.admin-link-group[href]'));
|
||||
const grouped = new Map();
|
||||
|
||||
links.forEach((link) => {
|
||||
const href = link.getAttribute('href');
|
||||
if (!grouped.has(href)) {
|
||||
grouped.set(href, []);
|
||||
}
|
||||
grouped.get(href).push(link);
|
||||
});
|
||||
|
||||
grouped.forEach((group) => {
|
||||
group.forEach((link) => {
|
||||
const addHighlight = () => group.forEach((item) => item.classList.add('linked-hover'));
|
||||
const removeHighlight = () => group.forEach((item) => item.classList.remove('linked-hover'));
|
||||
|
||||
link.addEventListener('mouseenter', addHighlight);
|
||||
link.addEventListener('mouseleave', removeHighlight);
|
||||
link.addEventListener('focus', addHighlight);
|
||||
link.addEventListener('blur', removeHighlight);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endif
|
||||
|
||||
@@ -53,7 +53,12 @@
|
||||
|
||||
$('.basket-quantity').change(function() {
|
||||
var offer_id = $(this).data('id');
|
||||
var quantity = $(this).val();
|
||||
var quantity = parseInt($(this).val()) || 1;
|
||||
var min = parseInt($(this).attr('min')) || 1;
|
||||
var max = parseInt($(this).attr('max'));
|
||||
if (quantity < min) quantity = min;
|
||||
if (max && quantity > max) quantity = max;
|
||||
$(this).val(quantity);
|
||||
var $row = $(this).closest('.row');
|
||||
updateBasket(offer_id, quantity, function() {
|
||||
calculatePrice($row);
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
'value' => $item['quantity'],
|
||||
'class' => 'basket-quantity',
|
||||
'data_id' => $item['id'],
|
||||
'min' => 1,
|
||||
'max' => $item['stock'] ?? false,
|
||||
])
|
||||
</div>
|
||||
<div class="col-4 text-right" style="font-size: 2em;" id="basket_total-{{ $item['id'] }}">
|
||||
|
||||
@@ -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'] }}"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
? xhr.responseJSON.message
|
||||
: "{{ __('Une erreur est survenue lors de l\'enregistrement du canal de vente préféré.') }}";
|
||||
|
||||
console.error('Sale channel update failed', {
|
||||
status: xhr.status,
|
||||
response: xhr.responseJSON || xhr.responseText,
|
||||
selectedSaleChannel
|
||||
});
|
||||
|
||||
alert(message);
|
||||
|
||||
if (currentSaleChannelId) {
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
'name' => 'phone',
|
||||
'value' => $customer['phone'] ?? (old('phone') ?? ''),
|
||||
'label' => 'Téléphone',
|
||||
'required' => true,
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<div class="mb-5 bg-green-light shadow2">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h1 class="p-2 green" style="font-size: 2em;">{{ $shelve['name'] }}</h1>
|
||||
<h1 class="p-2 green shelve-title">{{ $shelve['name'] }}</h1>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<a href="{{ route('Shop.Categories.show', ['id' => $shelve['id']]) }}"
|
||||
class="mt-2 mr-2 btn btn-green-dark">
|
||||
Découvrir la sélection
|
||||
class="mt-2 mr-2 btn btn-green-dark shelve-btn">
|
||||
Découvrir<span class="shelve-btn-suffix"> la sélection</span>
|
||||
</a>
|
||||
<!--
|
||||
<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="col-11 mx-auto shelve_slider_{{ $shelve['id'] }} slider">
|
||||
@foreach ($shelve['articles'] as $name => $article)
|
||||
<div class="text-center pr-2 pl-2">
|
||||
<div class="text-center pr-2 pl-2 shelve-slide">
|
||||
<a class="green" href="{{ route('Shop.Articles.show', ['id' => $article['id']]) }}">
|
||||
<img data-lazy="{{ App\Repositories\Shop\Articles::getPreviewSrc($article['image'] ?? false) }}"
|
||||
class="d-block w-100 rounded" alt="{{ $name }}" />
|
||||
<div style="height: 48px;">
|
||||
<div class="shelve-article-label">
|
||||
{{ $name }}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -50,7 +53,7 @@
|
||||
|
||||
@include('load.layout.chevron')
|
||||
|
||||
@push('js')
|
||||
@prepend('js')
|
||||
<script>
|
||||
$('#customer').click(function() {
|
||||
$(".personal_data").addClass('d-none');
|
||||
@@ -65,7 +68,9 @@
|
||||
});
|
||||
|
||||
function refreshBasketTotal(deliveryId, deliveryTypeId) {
|
||||
options = deliveryId + '/' + deliveryTypeId;
|
||||
var safeDeliveryId = deliveryId !== undefined && deliveryId !== null ? deliveryId : '';
|
||||
var safeDeliveryTypeId = deliveryTypeId !== undefined && deliveryTypeId !== null ? deliveryTypeId : '';
|
||||
var options = safeDeliveryId + '/' + safeDeliveryTypeId;
|
||||
$.get("{{ Route('Shop.Basket.getBasketTotal') }}/" + options, function(data) {
|
||||
$('#basketTotal').html(data);
|
||||
});
|
||||
@@ -73,4 +78,4 @@
|
||||
|
||||
initChevron();
|
||||
</script>
|
||||
@endpush
|
||||
@endprepend
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
<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>
|
||||
@include('Shop.Orders.partials.deliveries')
|
||||
</x-layout.collapse>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
@include('Shop.Orders.partials.payments')
|
||||
</x-layout.collapse>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<div class="alert alert-info">
|
||||
<p>{{ __('boilerplate::auth.firstlogin.intro') }}</p>
|
||||
</div>
|
||||
@include('load.form.password_toggle')
|
||||
<div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}">
|
||||
{{ Form::label('password', __('boilerplate::auth.fields.password')) }}
|
||||
{{ Form::input('password', 'password', Request::old('password'), ['class' => 'form-control', 'autofocus']) }}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<div class="input-group form-group {{ $errors->has('password') ? 'has-error' : '' }}">
|
||||
{{ Form::password('password', ['class' => 'form-control', 'placeholder' => __('boilerplate::auth.fields.password'), 'required']) }}
|
||||
<div class="input-group-append">
|
||||
<div class="btn btn-outline-secondary">
|
||||
<i class="fa fa-lock"></i>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary password-toggle" type="button" tabindex="-1">
|
||||
<i class="fa fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!! $errors->first('password', '<p class="text-danger"><strong>:message</strong></p>') !!}
|
||||
@@ -46,4 +46,5 @@
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
@include('load.form.password_toggle')
|
||||
{!! Form::close() !!}
|
||||
|
||||
62
resources/views/Shop/auth/partials/password_rules.blade.php
Normal file
62
resources/views/Shop/auth/partials/password_rules.blade.php
Normal 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
|
||||
@@ -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">
|
||||
@@ -66,6 +68,8 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@include('load.form.password_toggle')
|
||||
|
||||
@push('js')
|
||||
<script>
|
||||
$('#use_for_delivery').click(function() {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@include('load.form.password_toggle')
|
||||
|
||||
<div class="row mb-3 mt-3">
|
||||
<label for="new-password" class="col-md-6 control-label text-right">Mot de passe actuel</label>
|
||||
<div class="col-md-6">
|
||||
@@ -13,7 +15,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 +24,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>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
{{ 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>') !!}
|
||||
</div>
|
||||
@include('load.form.password_toggle')
|
||||
<div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}">
|
||||
{{ Form::password('password', ['class' => 'form-control', 'placeholder' => __('boilerplate::auth.fields.password'), 'required']) }}
|
||||
{!! $errors->first('password','<p class="text-danger"><strong>:message</strong></p>') !!}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
@if (!empty($text))
|
||||
<div class="row m-0 mb-3">
|
||||
<div class="col-12 p-3 green-dark" style="font-size: 1.2em;">{!! $text !!}</div>
|
||||
<div class="col-12 p-3 green-dark homepage-text" style="font-size: 1.2em;">{!! $text !!}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
])
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user