Compare commits
99 Commits
1.0.0-rc.2
...
master
| 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 | ||
|
|
cc8dfa29b4 | ||
|
|
62bce92d6d | ||
|
|
8d130b9741 | ||
|
|
2d7436a12b | ||
|
|
f25a62ed26 | ||
|
|
36764f2647 | ||
|
|
e37cad6699 | ||
|
|
ae7f8ed2c9 | ||
|
|
a3a86f4b2f | ||
|
|
9c081574c8 | ||
|
|
11edccad02 | ||
|
|
7c796802be | ||
|
|
5cc43bc889 | ||
|
|
f094411f10 | ||
|
|
ccc477f291 | ||
|
|
7217d945a3 | ||
|
|
9185269874 | ||
|
|
e42e3b4c0d | ||
|
|
a7ae946797 |
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 \
|
||||
@@ -55,6 +56,8 @@ 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
|
||||
@@ -83,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).
|
||||
|
||||
@@ -11,7 +11,7 @@ use Yajra\DataTables\Html\Column;
|
||||
|
||||
class CustomerInvoicesDataTable extends DataTable
|
||||
{
|
||||
public $model_name = 'invoices';
|
||||
public $model_name = 'customer_invoices';
|
||||
|
||||
public $sortedColumn = 1;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,15 +12,6 @@ class CustomerInvoiceController extends Controller
|
||||
return $dataTable->render('Admin.Shop.CustomerInvoices.list');
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$data = [
|
||||
'invoice' => Invoices::get($id),
|
||||
];
|
||||
|
||||
return view('Admin.Shop.CustomerInvoices.view', $data);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
return Invoices::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);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Shop\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Repositories\Core\User\ShopCart;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -31,6 +32,7 @@ class LoginController extends Controller
|
||||
]);
|
||||
|
||||
if ($this->guard()->attempt($credentials, $request->get('remember'))) {
|
||||
ShopCart::migrateGuestCartToUser();
|
||||
$request->session()->regenerate();
|
||||
if (back()->getTargetUrl() === route('Shop.Orders.store')) {
|
||||
$route = 'Shop.Orders.order';
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Shop\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Shop\RegisterCustomer;
|
||||
use App\Repositories\Core\User\ShopCart;
|
||||
use App\Repositories\Shop\CustomerSaleChannels;
|
||||
use App\Repositories\Shop\CustomerAddresses;
|
||||
use App\Repositories\Shop\Customers;
|
||||
@@ -33,6 +34,7 @@ class RegisterController extends Controller
|
||||
$user = $this->create($request->all());
|
||||
|
||||
$this->guard()->login($user);
|
||||
ShopCart::migrateGuestCartToUser();
|
||||
|
||||
return $request->wantsJson()
|
||||
? new JsonResponse([], 201)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
namespace App\Http\Controllers\Shop;
|
||||
|
||||
use App\Repositories\Core\User\ShopCart;
|
||||
use App\Repositories\Shop\Baskets;
|
||||
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
|
||||
{
|
||||
@@ -43,23 +49,237 @@ class CustomerController extends Controller
|
||||
public function storeProfileAjax(Request $request)
|
||||
{
|
||||
$data = $request->all();
|
||||
|
||||
if (array_key_exists('default_sale_channel_id', $data)) {
|
||||
$customer = Customers::get(Customers::getId());
|
||||
$newSaleChannelId = (int) $data['default_sale_channel_id'];
|
||||
$currentSaleChannelId = (int) ($customer->default_sale_channel_id ?? 0);
|
||||
|
||||
if ($newSaleChannelId && $newSaleChannelId !== $currentSaleChannelId && ShopCart::count() > 0) {
|
||||
$cartItems = ShopCart::getContent();
|
||||
$unavailable = [];
|
||||
|
||||
foreach ($cartItems as $item) {
|
||||
$offerId = (int) $item->id;
|
||||
|
||||
if (! Offers::getPrice($offerId, 1, $newSaleChannelId)) {
|
||||
$offer = Offers::get($offerId, ['article']);
|
||||
$unavailable[] = $offer->article->name ?? $item->name;
|
||||
|
||||
if (count($unavailable) >= 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($unavailable)) {
|
||||
$list = implode(', ', $unavailable);
|
||||
|
||||
return response()->json([
|
||||
'error' => 1,
|
||||
'message' => __('Certains articles de votre panier ne sont pas disponibles dans ce canal : :products. Merci de finaliser votre commande ou de retirer ces articles avant de changer de canal.', ['products' => $list]),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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) {
|
||||
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]);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (Customers::isNotConnected()) {
|
||||
return response()->json(['message' => __('Authentification requise.')], 403);
|
||||
}
|
||||
|
||||
$prefix = $request->input('prefix');
|
||||
$types = ['deliveries' => 1, 'invoices' => 2];
|
||||
|
||||
if (! array_key_exists($prefix, $types)) {
|
||||
return response()->json(['message' => __('Type d\'adresse inconnu.')], 422);
|
||||
}
|
||||
|
||||
$addressData = $request->input($prefix, []);
|
||||
|
||||
$validator = Validator::make($addressData, [
|
||||
'name' => ['nullable', 'string', 'max:150'],
|
||||
'address' => ['required', 'string', 'max:255'],
|
||||
'address2' => ['nullable', 'string', 'max:255'],
|
||||
'zipcode' => ['required', 'string', 'max:30'],
|
||||
'city' => ['required', 'string', 'max:255'],
|
||||
], [
|
||||
'address.required' => __('Merci de renseigner l\'adresse.'),
|
||||
'zipcode.required' => __('Merci de renseigner le code postal.'),
|
||||
'city.required' => __('Merci de renseigner la ville.'),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => __('Merci de vérifier les informations saisies.'),
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$customerId = Customers::getId();
|
||||
$data['customer_id'] = $customerId;
|
||||
$data['type'] = $types[$prefix];
|
||||
|
||||
if (empty($data['name'])) {
|
||||
$data['name'] = Customers::getName($customerId);
|
||||
}
|
||||
|
||||
$address = CustomerAddresses::store($data);
|
||||
CustomerAddresses::setDefault($customerId, $address->id, $types[$prefix]);
|
||||
|
||||
$html = view('Shop.Customers.partials.address_item', [
|
||||
'address' => $address->toArray(),
|
||||
'prefix' => $prefix,
|
||||
'inputName' => $request->input('input_name'),
|
||||
'with_name' => true,
|
||||
'selected' => $address->id,
|
||||
])->render();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
'message' => __('Adresse enregistrée.'),
|
||||
'id' => $address->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete_address($id)
|
||||
{
|
||||
$ret = CustomerAddresses::destroy($id);
|
||||
$address = CustomerAddresses::get($id);
|
||||
|
||||
return redirect()->route('Shop.Customers.edit');
|
||||
if (! $address || (int) $address->customer_id !== (int) Customers::getId()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$remaining = CustomerAddresses::getModel()
|
||||
->byCustomer($address->customer_id)
|
||||
->byType($address->type)
|
||||
->count();
|
||||
|
||||
if ($remaining <= 1) {
|
||||
return redirect()->route('Shop.Customers.edit')
|
||||
->with('growl', [__('Vous devez conserver au moins une adresse par type.'), 'warning']);
|
||||
}
|
||||
|
||||
CustomerAddresses::destroy($id);
|
||||
CustomerAddresses::ensureDefault($address->customer_id, $address->type);
|
||||
|
||||
return redirect()->route('Shop.Customers.edit')
|
||||
->with('growl', [__('Adresse supprimée.'), 'success']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,14 @@ class InvoiceController extends Controller
|
||||
|
||||
public function view($uuid)
|
||||
{
|
||||
$invoice = Invoices::view($uuid);
|
||||
|
||||
if (! $invoice) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'invoice' => Invoices::view($uuid),
|
||||
'invoice' => $invoice,
|
||||
];
|
||||
|
||||
return view('Shop.Invoices.view', $data);
|
||||
|
||||
@@ -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;
|
||||
@@ -49,12 +51,35 @@ class OrderController extends Controller
|
||||
{
|
||||
if (ShopCart::count()) {
|
||||
$customer = Customers::getWithAddresses();
|
||||
$deliveries = Deliveries::getByCustomer();
|
||||
$customerId = $customer ? $customer->id : false;
|
||||
$defaultSaleChannelId = SaleChannels::getDefaultID($customerId);
|
||||
$deliveries = $defaultSaleChannelId
|
||||
? Deliveries::getBySaleChannels([$defaultSaleChannelId])
|
||||
: Deliveries::getByCustomer($customerId);
|
||||
$deliveries = $deliveries ? $deliveries->values() : collect();
|
||||
|
||||
$customerData = $customer ? $customer->toArray() : false;
|
||||
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 = [
|
||||
'customer' => $customer ? $customer->toArray() : false,
|
||||
'customer' => $customerData,
|
||||
'basket' => Baskets::getBasketTotal(),
|
||||
'deliveries' => $deliveries ? $deliveries->toArray() : [],
|
||||
'deliveries' => $deliveries->toArray(),
|
||||
'delivery_types' => DeliveryTypes::getWithPrice(Baskets::getWeight()),
|
||||
];
|
||||
|
||||
@@ -70,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) {
|
||||
@@ -78,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');
|
||||
@@ -87,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) {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Models\Shop;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CustomerAddress extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $table = 'shop_customer_addresses';
|
||||
|
||||
@@ -39,7 +39,7 @@ class Invoice extends Model
|
||||
|
||||
public function address(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomerAddress::class, 'invoice_address_id');
|
||||
return $this->belongsTo(CustomerAddress::class, 'invoice_address_id')->withTrashed();
|
||||
}
|
||||
|
||||
public function scopeByCustomer($query, $customerId)
|
||||
|
||||
@@ -29,7 +29,7 @@ class Order extends Model
|
||||
|
||||
public function delivery_address(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomerAddress::class, 'delivery_address_id');
|
||||
return $this->belongsTo(CustomerAddress::class, 'delivery_address_id')->withTrashed();
|
||||
}
|
||||
|
||||
public function delivery(): BelongsTo
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,11 +94,106 @@ class ShopCart
|
||||
return self::get()->getContent();
|
||||
}
|
||||
|
||||
public static function get()
|
||||
public static function migrateGuestCartToUser($userId = null)
|
||||
{
|
||||
$userId = Auth::guard('customer')->id();
|
||||
$sessionKey = 'cart_'.sha1(static::class . ($userId ?? 'guest'));
|
||||
$userId = self::resolveUserId($userId);
|
||||
|
||||
return Cart::session($sessionKey);
|
||||
if ($userId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$guestSessionKey = self::sessionKey();
|
||||
$guestItems = Cart::session($guestSessionKey)->getContent();
|
||||
|
||||
if ($guestItems->count() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userSessionKey = self::sessionKey($userId);
|
||||
|
||||
foreach ($guestItems as $item) {
|
||||
$existing = Cart::session($userSessionKey)->get($item->id);
|
||||
|
||||
if ($existing) {
|
||||
Cart::session($userSessionKey)->update($item->id, [
|
||||
'quantity' => [
|
||||
'relative' => false,
|
||||
'value' => $existing->quantity + $item->quantity,
|
||||
],
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemData = [
|
||||
'id' => $item->id,
|
||||
'name' => $item->name,
|
||||
'price' => $item->price,
|
||||
'quantity' => $item->quantity,
|
||||
'attributes' => self::extractAttributes($item),
|
||||
];
|
||||
|
||||
if (isset($item->associatedModel)) {
|
||||
$itemData['associatedModel'] = $item->associatedModel;
|
||||
}
|
||||
|
||||
$conditions = self::extractConditions($item);
|
||||
if (! empty($conditions)) {
|
||||
$itemData['conditions'] = $conditions;
|
||||
}
|
||||
|
||||
Cart::session($userSessionKey)->add($itemData);
|
||||
}
|
||||
|
||||
Cart::session($guestSessionKey)->clear();
|
||||
Cart::session($userSessionKey);
|
||||
}
|
||||
|
||||
protected static function extractAttributes($item)
|
||||
{
|
||||
if (! isset($item->attributes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_object($item->attributes) && method_exists($item->attributes, 'toArray')) {
|
||||
return $item->attributes->toArray();
|
||||
}
|
||||
|
||||
return (array) $item->attributes;
|
||||
}
|
||||
|
||||
protected static function extractConditions($item)
|
||||
{
|
||||
if (! isset($item->conditions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_object($item->conditions) && method_exists($item->conditions, 'toArray')) {
|
||||
return $item->conditions->toArray();
|
||||
}
|
||||
|
||||
return (array) $item->conditions;
|
||||
}
|
||||
|
||||
protected static function resolveUserId($userId = null)
|
||||
{
|
||||
return $userId ?? Auth::guard('customer')->id();
|
||||
}
|
||||
|
||||
protected static function sessionKey($userId = null)
|
||||
{
|
||||
$key = $userId ?? 'guest';
|
||||
|
||||
return 'cart_'.sha1(static::class.$key);
|
||||
}
|
||||
|
||||
protected static function session($userId = null)
|
||||
{
|
||||
return Cart::session(self::sessionKey($userId));
|
||||
}
|
||||
|
||||
public static function get($userId = null)
|
||||
{
|
||||
return self::session(self::resolveUserId($userId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']])) {
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace App\Repositories\Shop;
|
||||
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;
|
||||
@@ -16,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];
|
||||
@@ -40,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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -63,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();
|
||||
@@ -70,9 +79,33 @@ 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;
|
||||
}
|
||||
|
||||
@@ -108,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:
|
||||
}
|
||||
@@ -148,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) {
|
||||
@@ -166,7 +210,7 @@ class Articles
|
||||
ksort($data);
|
||||
}
|
||||
|
||||
return $data ?? false;
|
||||
return $data ?? [];
|
||||
}
|
||||
|
||||
public static function getDataForSale($article)
|
||||
@@ -273,8 +317,6 @@ class Articles
|
||||
case 'merchandise':
|
||||
$model = $model->merchandise();
|
||||
break;
|
||||
default:
|
||||
$model = $model->botanic();
|
||||
}
|
||||
|
||||
return $model;
|
||||
|
||||
@@ -100,25 +100,58 @@ class Baskets
|
||||
$offers = Offers::getWithPricesByIds(self::getIds(), $saleChannelId);
|
||||
foreach ($basket as $item) {
|
||||
$offer = $offers->where('id', $item->id)->first();
|
||||
if (! $offer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$priceValue = Offers::getPrice($item->id, $item->quantity, $saleChannelId);
|
||||
$unitPrice = $priceValue ? (float) $priceValue->price_taxed : (float) $item->price;
|
||||
|
||||
$article_nature = strtolower($offer->article->article_nature->name);
|
||||
$data[$article_nature][] = [
|
||||
'id' => (int) $item->id,
|
||||
'name' => $item->name,
|
||||
'quantity' => (int) $item->quantity,
|
||||
'price' => $item->price,
|
||||
'price' => $unitPrice,
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
||||
return $data ?? false;
|
||||
}
|
||||
|
||||
public static function refreshPrices($saleChannelId = false)
|
||||
{
|
||||
$saleChannelId = $saleChannelId ? $saleChannelId : SaleChannels::getDefaultID();
|
||||
$basket = ShopCart::getContent();
|
||||
|
||||
foreach ($basket as $item) {
|
||||
$priceValue = Offers::getPrice($item->id, $item->quantity, $saleChannelId);
|
||||
|
||||
if (! $priceValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ShopCart::get()->update($item->id, [
|
||||
'price' => $priceValue->price_taxed,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getBasketData($id, $quantity = 1)
|
||||
{
|
||||
$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.';
|
||||
|
||||
@@ -12,17 +12,35 @@ class CustomerAddresses
|
||||
public static function storeByCustomer($customer, $data)
|
||||
{
|
||||
$deliveries = $data['deliveries'] ?? false;
|
||||
if ($deliveries && $deliveries['zipcode'] && $deliveries['city']) {
|
||||
$deliveries['customer_id'] = $customer->id;
|
||||
$deliveries['type'] = 1;
|
||||
self::store($deliveries);
|
||||
if ($deliveries) {
|
||||
if (! empty($deliveries['address_id'])) {
|
||||
self::setDefault($customer->id, (int) $deliveries['address_id'], 1);
|
||||
}
|
||||
|
||||
if (! empty($deliveries['zipcode']) && ! empty($deliveries['city'])) {
|
||||
$payload = $deliveries;
|
||||
unset($payload['address_id']);
|
||||
$payload['customer_id'] = $customer->id;
|
||||
$payload['type'] = 1;
|
||||
$newAddress = self::store($payload);
|
||||
self::setDefault($customer->id, $newAddress->id, 1);
|
||||
}
|
||||
}
|
||||
|
||||
$invoices = $data['invoices'] ?? false;
|
||||
if ($invoices && $invoices['zipcode'] && $invoices['city']) {
|
||||
$invoices['customer_id'] = $customer->id;
|
||||
$invoices['type'] = 2;
|
||||
self::store($invoices);
|
||||
if ($invoices) {
|
||||
if (! empty($invoices['address_id'])) {
|
||||
self::setDefault($customer->id, (int) $invoices['address_id'], 2);
|
||||
}
|
||||
|
||||
if (! empty($invoices['zipcode']) && ! empty($invoices['city'])) {
|
||||
$payload = $invoices;
|
||||
unset($payload['address_id']);
|
||||
$payload['customer_id'] = $customer->id;
|
||||
$payload['type'] = 2;
|
||||
$newAddress = self::store($payload);
|
||||
self::setDefault($customer->id, $newAddress->id, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,14 +88,24 @@ class CustomerAddresses
|
||||
|
||||
public static function getInvoiceAddress($customerId)
|
||||
{
|
||||
$addresses = CustomerAddress::byCustomer($customerId)->byInvoicing()->get();
|
||||
return count($addresses) ? $addresses->first() : self::getByCustomer($customerId);
|
||||
$address = CustomerAddress::byCustomer($customerId)
|
||||
->byInvoicing()
|
||||
->orderByDesc('priority')
|
||||
->orderBy('id')
|
||||
->first();
|
||||
|
||||
return $address ?? self::getByCustomer($customerId);
|
||||
}
|
||||
|
||||
public static function getDeliveryAddress($customerId)
|
||||
{
|
||||
$addresses = CustomerAddress::byCustomer($customerId)->byDelivery()->get();
|
||||
return count($addresses) ? $addresses->first() : self::getByCustomer($customerId);
|
||||
$address = CustomerAddress::byCustomer($customerId)
|
||||
->byDelivery()
|
||||
->orderByDesc('priority')
|
||||
->orderBy('id')
|
||||
->first();
|
||||
|
||||
return $address ?? self::getByCustomer($customerId);
|
||||
}
|
||||
|
||||
public static function getByCustomer($customerId = false)
|
||||
@@ -92,6 +120,40 @@ class CustomerAddresses
|
||||
return ((int) $type === 1) ? '<i class="fa fa-fw fa-truck"></i>' : '<i class="fa fa-fw fa-file-invoice"></i>';
|
||||
}
|
||||
|
||||
public static function setDefault($customerId, $addressId, $type)
|
||||
{
|
||||
if (! $addressId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$address = self::get($addressId);
|
||||
|
||||
if (! $address || (int) $address->customer_id !== (int) $customerId || (int) $address->type !== (int) $type) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::getModel()->byCustomer($customerId)->byType($type)->update(['priority' => null]);
|
||||
|
||||
$address->priority = 1;
|
||||
$address->save();
|
||||
}
|
||||
|
||||
public static function ensureDefault($customerId, $type)
|
||||
{
|
||||
$hasDefault = self::getModel()->byCustomer($customerId)->byType($type)->where('priority', 1)->exists();
|
||||
|
||||
if ($hasDefault) {
|
||||
return;
|
||||
}
|
||||
|
||||
$address = self::getModel()->byCustomer($customerId)->byType($type)->orderBy('id')->first();
|
||||
|
||||
if ($address) {
|
||||
$address->priority = 1;
|
||||
$address->save();
|
||||
}
|
||||
}
|
||||
|
||||
public static function toggleActive($id, $active)
|
||||
{
|
||||
return self::update(['active' => $active], $id);
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Datatables\Shop\CustomerInvoicesDataTable;
|
||||
use App\Datatables\Shop\CustomerOrdersDataTable;
|
||||
use App\Models\Shop\Customer;
|
||||
use App\Traits\Model\Basic;
|
||||
use App\Repositories\Shop\CustomerAddresses;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -30,16 +31,31 @@ class Customers
|
||||
public static function getSaleChannels($customerId = false)
|
||||
{
|
||||
$customer = $customerId ? self::get($customerId) : self::getAuth();
|
||||
$saleChannels = $customer ? $customer->sale_channels : false;
|
||||
$saleChannels = collect();
|
||||
|
||||
return $saleChannels ? $saleChannels : SaleChannels::getDefault();
|
||||
if ($customer) {
|
||||
$customer->loadMissing('sale_channels');
|
||||
$saleChannels = $customer->sale_channels ?? collect();
|
||||
|
||||
if ($saleChannels instanceof \Illuminate\Support\Collection && $saleChannels->isNotEmpty()) {
|
||||
return $saleChannels;
|
||||
}
|
||||
}
|
||||
|
||||
$default = SaleChannels::getDefault($customerId);
|
||||
|
||||
return $default ? collect([$default]) : collect();
|
||||
}
|
||||
|
||||
public static function getSaleChannel($customerId = false)
|
||||
{
|
||||
$saleChannels = self::getSaleChannels($customerId);
|
||||
|
||||
return $saleChannels->first();
|
||||
if ($saleChannels instanceof \Illuminate\Support\Collection) {
|
||||
return $saleChannels->first();
|
||||
}
|
||||
|
||||
return $saleChannels;
|
||||
}
|
||||
|
||||
public static function getDeliveries()
|
||||
@@ -57,12 +73,22 @@ class Customers
|
||||
|
||||
public static function editProfile($id = false)
|
||||
{
|
||||
return $id ? [
|
||||
'customer' => self::get($id, ['addresses', 'deliveries'])->toArray(),
|
||||
'deliveries' => Deliveries::getAllWithSaleChannel()->toArray(),
|
||||
if (! $id) {
|
||||
abort('403');
|
||||
}
|
||||
|
||||
$customer = self::get($id, ['addresses', 'deliveries', 'sale_channels']);
|
||||
|
||||
$saleChannels = self::getSaleChannels($id);
|
||||
|
||||
return [
|
||||
'customer' => $customer->toArray(),
|
||||
'sale_channels' => $saleChannels->toArray(),
|
||||
'deliveries' => Deliveries::getByCustomer($id)->toArray(),
|
||||
'sale_channel_checks' => Shop::getSaleChannelAvailabilitySummary($saleChannels->pluck('id')->toArray()),
|
||||
'orders' => (new CustomerOrdersDataTable())->html(),
|
||||
'invoices' => (new CustomerInvoicesDataTable())->html(),
|
||||
] : abort('403');
|
||||
];
|
||||
}
|
||||
|
||||
public static function getAddresses($id = false)
|
||||
@@ -95,6 +121,16 @@ class Customers
|
||||
$data = $customer->toArray();
|
||||
$data['sale_channels'] = $customer->sale_channels->pluck('id')->toArray();
|
||||
$data['deliveries'] = Deliveries::getBySaleChannels($data['sale_channels'])->toArray();
|
||||
$data['delivery_address_id'] = optional(CustomerAddresses::getDeliveryAddress($id))->id;
|
||||
$data['invoice_address_id'] = optional(CustomerAddresses::getInvoiceAddress($id))->id;
|
||||
|
||||
if (! $data['delivery_address_id'] && ! empty($data['delivery_addresses'])) {
|
||||
$data['delivery_address_id'] = $data['delivery_addresses'][0]['id'] ?? null;
|
||||
}
|
||||
|
||||
if (! $data['invoice_address_id'] && ! empty($data['invoice_addresses'])) {
|
||||
$data['invoice_address_id'] = $data['invoice_addresses'][0]['id'] ?? null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
@@ -102,8 +138,8 @@ class Customers
|
||||
public static function storeFull($data)
|
||||
{
|
||||
$data2 = $data;
|
||||
if ($data['sale_channels'] ?? false) {
|
||||
$saleChannels = $data['sale_channels'] ?? false;
|
||||
$saleChannels = array_key_exists('sale_channels', $data) ? $data['sale_channels'] : null;
|
||||
if ($saleChannels !== null) {
|
||||
unset($data['sale_channels']);
|
||||
}
|
||||
if ($data['deliveries'] ?? false) {
|
||||
@@ -113,7 +149,9 @@ class Customers
|
||||
unset($data['invoices']);
|
||||
}
|
||||
$customer = self::store($data);
|
||||
$customer->sale_channels()->sync($saleChannels);
|
||||
if ($saleChannels !== null) {
|
||||
$customer->sale_channels()->sync($saleChannels);
|
||||
}
|
||||
CustomerAddresses::storeByCustomer($customer, $data2);
|
||||
|
||||
return $customer->id;
|
||||
@@ -141,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();
|
||||
|
||||
@@ -21,12 +21,12 @@ class Deliveries
|
||||
$customer = $customerId ? Customers::get($customerId) : Customers::getAuth();
|
||||
$saleChannels = $customer ? $customer->sale_channels->pluck('id')->toArray() : [SaleChannels::getDefaultID()];
|
||||
|
||||
return $saleChannels ? self::getBySaleChannels($saleChannels) : false;
|
||||
return $saleChannels ? self::getBySaleChannels($saleChannels) : collect();
|
||||
}
|
||||
|
||||
public static function getBySaleChannels($saleChannels)
|
||||
{
|
||||
return Delivery::bySaleChannels($saleChannels)->with('sale_channel')->get();
|
||||
return Delivery::bySaleChannels($saleChannels)->active()->with('sale_channel')->get();
|
||||
}
|
||||
|
||||
public static function getSaleChannelId($deliveryId)
|
||||
@@ -41,7 +41,7 @@ class Deliveries
|
||||
|
||||
public static function getAllWithSaleChannel()
|
||||
{
|
||||
return Delivery::orderBy('name', 'asc')->active()->public()->with('sale_channel')->get();
|
||||
return Delivery::orderBy('name', 'asc')->active()->with('sale_channel')->get();
|
||||
}
|
||||
|
||||
public static function toggleActive($id, $active)
|
||||
|
||||
@@ -15,9 +15,15 @@ class DeliveryTypes
|
||||
$types = self::getAll();
|
||||
|
||||
foreach ($types as $type) {
|
||||
$price = self::getPrice($type->id, $weight);
|
||||
|
||||
if ($price === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$type->id] = [
|
||||
'name' => $type->name,
|
||||
'price' => self::getPrice($type->id, $weight),
|
||||
'price' => $price,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,15 @@ class InvoicePDF
|
||||
public static function get($id)
|
||||
{
|
||||
$invoice = Invoices::getFull($id);
|
||||
$customFields = [];
|
||||
if ($orderRef = optional($invoice->order)->ref) {
|
||||
$customFields['Numéro de commande'] = $orderRef;
|
||||
}
|
||||
|
||||
$customer = new Party([
|
||||
'name' => $invoice->customer->name,
|
||||
'name' => optional($invoice->customer)->name ?? __('Client inconnu'),
|
||||
'address' => self::makeAddress($invoice->address),
|
||||
'custom_fields' => [
|
||||
'order number' => $invoice->order->ref,
|
||||
],
|
||||
'custom_fields' => $customFields,
|
||||
]);
|
||||
|
||||
$items = self::makeItems($invoice->order->detail);
|
||||
@@ -48,7 +51,17 @@ class InvoicePDF
|
||||
|
||||
public static function makeAddress($address)
|
||||
{
|
||||
return $address->address.'<br>'.$address->zipcode.' '.$address->city;
|
||||
if (! $address) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = array_filter([
|
||||
$address->address ?? '',
|
||||
$address->address2 ?? '',
|
||||
trim(($address->zipcode ?? '').' '.($address->city ?? '')),
|
||||
]);
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
public static function makeItems($details)
|
||||
|
||||
@@ -36,7 +36,13 @@ class Invoices
|
||||
|
||||
public static function view($uuid)
|
||||
{
|
||||
$data = self::getFullByUUID($uuid)->toArray();
|
||||
$invoice = self::getFullByUUID($uuid);
|
||||
|
||||
if (! $invoice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $invoice->toArray();
|
||||
$data['payment_type'] = InvoicePayments::getPaymentType($data['payment_type']);
|
||||
$data['status'] = self::getStatus($data['status']);
|
||||
|
||||
|
||||
@@ -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,9 @@
|
||||
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;
|
||||
|
||||
class Offers
|
||||
@@ -13,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(),
|
||||
];
|
||||
}
|
||||
@@ -166,4 +173,87 @@ class Offers
|
||||
{
|
||||
return Offer::query();
|
||||
}
|
||||
|
||||
public static function getSaleChannelsForArticle($articleId)
|
||||
{
|
||||
$channels = SaleChannel::query()
|
||||
->whereHas('price_lists', function ($query) use ($articleId) {
|
||||
$query->whereHas('tariff.offers', function ($subQuery) use ($articleId) {
|
||||
$subQuery->byArticle($articleId);
|
||||
})->whereHas('price_list_values');
|
||||
})
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$offers = Offer::query()
|
||||
->byArticle($articleId)
|
||||
->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) {
|
||||
// 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;
|
||||
|
||||
@@ -3,22 +3,53 @@
|
||||
namespace App\Repositories\Shop;
|
||||
|
||||
use App\Models\Shop\SaleChannel;
|
||||
use App\Repositories\Shop\Customers;
|
||||
use App\Traits\Model\Basic;
|
||||
|
||||
class SaleChannels
|
||||
{
|
||||
use Basic;
|
||||
|
||||
public static function getDefaultID()
|
||||
public static function getDefaultID($customerId = false)
|
||||
{
|
||||
$default = self::getDefault();
|
||||
$default = self::getDefault($customerId);
|
||||
|
||||
return $default ? self::getDefault()->id : false;
|
||||
return $default ? $default->id : false;
|
||||
}
|
||||
|
||||
public static function getDefault()
|
||||
public static function getDefault($customerId = false)
|
||||
{
|
||||
return self::getByCode('EXP');
|
||||
$sessionChannelId = session('shop.default_sale_channel_id');
|
||||
if ($sessionChannelId) {
|
||||
$sessionChannel = SaleChannel::find($sessionChannelId);
|
||||
if ($sessionChannel) {
|
||||
return $sessionChannel;
|
||||
}
|
||||
}
|
||||
|
||||
$customer = $customerId ? Customers::get($customerId) : Customers::getAuth();
|
||||
|
||||
if ($customer) {
|
||||
$customer->loadMissing('sale_channels');
|
||||
|
||||
if ($customer->default_sale_channel_id) {
|
||||
$preferred = $customer->sale_channels->firstWhere('id', $customer->default_sale_channel_id);
|
||||
if (! $preferred) {
|
||||
$preferred = SaleChannel::find($customer->default_sale_channel_id);
|
||||
}
|
||||
if ($preferred) {
|
||||
session(['shop.default_sale_channel_id' => $preferred->id]);
|
||||
return $preferred;
|
||||
}
|
||||
}
|
||||
|
||||
if ($customer->sale_channels->isNotEmpty()) {
|
||||
session(['shop.default_sale_channel_id' => $customer->sale_channels->first()->id]);
|
||||
return $customer->sale_channels->first();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
47
app/Repositories/Shop/Shop.php
Normal file
47
app/Repositories/Shop/Shop.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Shop;
|
||||
|
||||
use App\Repositories\Core\User\ShopCart;
|
||||
|
||||
class Shop
|
||||
{
|
||||
public static function getSaleChannelAvailabilitySummary(array $saleChannelIds): array
|
||||
{
|
||||
if (empty($saleChannelIds) || ShopCart::count() === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$cartItems = ShopCart::getContent();
|
||||
$summary = [];
|
||||
|
||||
foreach ($saleChannelIds as $saleChannelId) {
|
||||
$saleChannelId = (int) $saleChannelId;
|
||||
$issues = [];
|
||||
$issueCount = 0;
|
||||
|
||||
foreach ($cartItems as $item) {
|
||||
$offerId = (int) $item->id;
|
||||
|
||||
if (! Offers::getPrice($offerId, 1, $saleChannelId)) {
|
||||
$offer = Offers::get($offerId, ['article']);
|
||||
$issues[] = $offer->article->name ?? $item->name;
|
||||
$issueCount++;
|
||||
|
||||
if (count($issues) >= 3) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($issues)) {
|
||||
$summary[$saleChannelId] = [
|
||||
'full_count' => $issueCount,
|
||||
'names' => array_slice($issues, 0, 3),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
@@ -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,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('shop_deliveries')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$columns = ['created_by', 'updated_by', 'deleted_by'];
|
||||
$columnsToDrop = [];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::hasColumn('shop_deliveries', $column)) {
|
||||
$columnsToDrop[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
if ($columnsToDrop) {
|
||||
Schema::table('shop_deliveries', function (Blueprint $table) use ($columnsToDrop) {
|
||||
$table->dropColumn($columnsToDrop);
|
||||
});
|
||||
}
|
||||
|
||||
Schema::table('shop_deliveries', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('created_by')->nullable()->after('event_date_end');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->after('created_by');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->after('updated_by');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('shop_deliveries')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('shop_deliveries', function (Blueprint $table) {
|
||||
$table->dropColumn(['created_by', 'updated_by', 'deleted_by']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('shop_customers', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('shop_customers', 'default_sale_channel_id')) {
|
||||
$table->unsignedInteger('default_sale_channel_id')->nullable()->after('settings');
|
||||
$table->index('default_sale_channel_id', 'shop_customers_default_sale_channel_id_index');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('shop_customers', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('shop_customers', 'default_sale_channel_id')) {
|
||||
$table->dropIndex('shop_customers_default_sale_channel_id_index');
|
||||
$table->dropColumn('default_sale_channel_id');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -223,6 +223,9 @@ return [
|
||||
'successmod' => 'Le canal de vente a été correctement modifié',
|
||||
'successdel' => 'Le canal de vente a été correctement effacé',
|
||||
'confirmdelete' => 'Confirmez-vous la suppression du canal de vente ?',
|
||||
'missing_offers' => '{1} Ce canal de vente n\'a pas d\'offre pour :count produit.|[2,*] Ce canal de vente n\'a pas d\'offre pour :count produits.',
|
||||
'missing_offers_all' => 'Ce canal de vente n\'a aucune offre pour tous les produits de votre panier.',
|
||||
'cannot_select_with_cart' => 'Vous ne pouvez pas sélectionner ce mode d\'achat tant que votre panier contient des produits non disponibles dans ce mode.',
|
||||
],
|
||||
'shelves' => [
|
||||
'title' => 'Rayons',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
// Simple notification helper used by blade templates (fallback to Bootstrap alerts)
|
||||
window.growl = function(message, type) {
|
||||
var alertTypes = {
|
||||
success: 'alert-success',
|
||||
error: 'alert-danger',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
};
|
||||
var cssClass = alertTypes[type] || alertTypes.info;
|
||||
var $container = $('#growl-container');
|
||||
|
||||
if (!$container.length) {
|
||||
$container = $('<div id="growl-container" class="growl-container position-fixed w-100" style="top: 1rem; left: 0; z-index: 1080; pointer-events: none;"></div>');
|
||||
$('body').append($container);
|
||||
}
|
||||
|
||||
var $alert = $('<div class="alert ' + cssClass + ' alert-dismissible fade show mx-auto shadow" role="alert" style="max-width: 420px; pointer-events: all;"></div>');
|
||||
$alert.append($('<span></span>').text(message));
|
||||
$alert.append('<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>');
|
||||
$container.append($alert);
|
||||
|
||||
setTimeout(function() {
|
||||
$alert.alert('close');
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// Prevent closing from click inside dropdown
|
||||
$(document).on('click', '.dropdown-menu', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
|
||||
// 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>
|
||||
|
||||
@@ -4,8 +4,28 @@
|
||||
'model' => 'customer_invoices',
|
||||
'with_print' => false,
|
||||
'with_filters' => false,
|
||||
'show_callback' => 'AdminCustomerInvoiceView(id);',
|
||||
])
|
||||
<x-layout.modal title="Filtres" id="modal-customer_invoices-filters">
|
||||
@include('Admin.Shop.CustomerInvoices.partials.filters', ['model' => 'customer_invoices'])
|
||||
</x-layout.modal>
|
||||
</x-card>
|
||||
|
||||
@include('load.layout.modal')
|
||||
|
||||
@push('js')
|
||||
<script>
|
||||
(function() {
|
||||
const customerInvoiceShowTemplate = "{{ route('Shop.Invoices.view', ['uuid' => '__UUID__']) }}";
|
||||
|
||||
window.AdminCustomerInvoiceView = function(id) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = customerInvoiceShowTemplate.replace('__UUID__', id);
|
||||
openModal('Voir une facture', '#invoice-form', url, false, false, 'xl', true);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -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 }} €
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
{{ Form::label('active', __('Actif')) }}<br/>
|
||||
<input type="hidden" name="active" value="0">
|
||||
@include("components.form.toggle", [
|
||||
'name' => 'active',
|
||||
'value' => $delivery['active'] ?? false,
|
||||
@@ -24,6 +25,7 @@
|
||||
</div>
|
||||
<div class="col-3">
|
||||
{{ Form::label('is_public', __('Type')) }}
|
||||
<input type="hidden" name="is_public" value="0">
|
||||
@include('components.form.toggle', [
|
||||
'name' => 'is_public',
|
||||
'value' => $delivery['is_public'] ?? false,
|
||||
|
||||
@@ -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,9 +47,176 @@
|
||||
|
||||
</div>
|
||||
<div class="col-lg-3 col-xs-12">
|
||||
@if (auth('web')->check() && !empty($article['available_sale_channels']))
|
||||
<div id="article-admin-offers" class="alert alert-info p-2 mb-3">
|
||||
<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'] }}{!! $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
|
||||
</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')
|
||||
</div>
|
||||
</div>
|
||||
@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,12 +53,17 @@
|
||||
|
||||
$('.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);
|
||||
calculateTotal();
|
||||
});
|
||||
}, $row);
|
||||
});
|
||||
|
||||
$('.basket-delete').click(function() {
|
||||
@@ -70,13 +75,20 @@
|
||||
});
|
||||
}
|
||||
|
||||
function updateBasket(offer_id, quantity, callback) {
|
||||
function updateBasket(offer_id, quantity, callback, $row) {
|
||||
var data = {
|
||||
offer_id: offer_id,
|
||||
quantity: quantity,
|
||||
update: true
|
||||
};
|
||||
$.post("{{ route('Shop.Basket.addBasket') }}", data, callback);
|
||||
$.post("{{ route('Shop.Basket.addBasket') }}", data, function(response) {
|
||||
if ($row && response && response.added && typeof response.added.price !== 'undefined') {
|
||||
$row.find('.basket-price').text(fixNumber(response.added.price));
|
||||
$row.find('.basket-total-row').text(fixNumber(response.added.price * $row.find('.basket-quantity').val()));
|
||||
}
|
||||
callback(response);
|
||||
refreshBasketTop();
|
||||
});
|
||||
}
|
||||
|
||||
function calculatePrice($that) {
|
||||
|
||||
@@ -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'] }}">
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
'prefix' => 'deliveries',
|
||||
'addresses' => $customer['delivery_addresses'],
|
||||
'with_name' => true,
|
||||
'selected' => $customer['delivery_address_id'] ?? null,
|
||||
])
|
||||
</x-card>
|
||||
|
||||
@@ -32,6 +33,7 @@
|
||||
'prefix' => 'invoices',
|
||||
'addresses' => $customer['invoice_addresses'],
|
||||
'with_name' => true,
|
||||
'selected' => $customer['invoice_address_id'] ?? null,
|
||||
])
|
||||
</x-card>
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="row mt-3 address-row" data-address-id="{{ $address['id'] }}">
|
||||
<div class="col-1">
|
||||
@php
|
||||
$inputName = $inputName ?? (isset($prefix) && $prefix ? $prefix.'[address_id]' : 'address_id');
|
||||
$currentValue = $selected ?? null;
|
||||
@endphp
|
||||
<x-form.radios.icheck name="{{ $inputName }}" val="{{ $address['id'] }}"
|
||||
:value="$currentValue" id="address_{{ $address['id'] }}" />
|
||||
</div>
|
||||
<div class="col-10">
|
||||
@if ($with_name ?? false)
|
||||
{{ $address['name'] ?? '' }}<br />
|
||||
@endif
|
||||
{{ $address['address'] }}<br />
|
||||
@if (! empty($address['address2']))
|
||||
{{ $address['address2'] }}<br />
|
||||
@endif
|
||||
{{ $address['zipcode'] }} {{ $address['city'] }}
|
||||
</div>
|
||||
<div class="col-1 text-right">
|
||||
<a class="text-danger" href="{{ route('Shop.Customers.delete_address', ['id' => $address['id']]) }}">
|
||||
<i class="fa fa-trash" data-id="{{ $address['id'] }}"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,26 +1,14 @@
|
||||
@foreach ($addresses ?? [] as $address)
|
||||
<div class="row mt-3">
|
||||
<div class="col-1">
|
||||
<x-form.radios.icheck name="@if ($prefix ?? false) {{ $prefix }} . '[]' @endif address_id"
|
||||
val="{{ $address['id'] }}" id="address_{{ $address['id'] }}" />
|
||||
</div>
|
||||
<div class="col-10">
|
||||
@if ($with_name ?? false)
|
||||
{{ $address['name'] }}<br />
|
||||
@endif
|
||||
{{ $address['address'] }}<br />
|
||||
@if ($address['address2'])
|
||||
{{ $address['address2'] }}<br />
|
||||
@endif
|
||||
{{ $address['zipcode'] }} {{ $address['city'] }}
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<a href="{{ route('Shop.Customers.delete_address') }}/{{ $address['id'] }}">
|
||||
<i class="fa fa-trash" class="delete" data-id="{{ $address['id'] }}"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<div id="addresses_list_{{ $prefix }}" class="addresses-list">
|
||||
@foreach ($addresses ?? [] as $address)
|
||||
@include('Shop.Customers.partials.address_item', [
|
||||
'address' => $address,
|
||||
'prefix' => $prefix ?? null,
|
||||
'inputName' => $inputName ?? null,
|
||||
'with_name' => $with_name ?? false,
|
||||
'selected' => $selected ?? null,
|
||||
])
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div id="add_address_container_{{ $prefix }}" class="green-dark d-none mb-3 mt-3">
|
||||
<x-card classBody="bg-green-dark yellow" title="Nouvelle adresse" classTitle="h4">
|
||||
@@ -31,6 +19,18 @@
|
||||
'label' => 'Adresse',
|
||||
'customer' => [],
|
||||
])
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6 mb-2">
|
||||
<x-form.button id="save_address_{{ $prefix }}" class="btn-success btn-sm btn-block"
|
||||
icon="fa-save" txt="Enregistrer cette adresse"
|
||||
:metadata="'data-prefix='.$prefix" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-2">
|
||||
<x-form.button id="cancel_address_{{ $prefix }}" class="btn-outline-light btn-sm btn-block"
|
||||
icon="fa-times" txt="Annuler"
|
||||
:metadata="'data-prefix='.$prefix" />
|
||||
</div>
|
||||
</div>
|
||||
</x-card>
|
||||
</div>
|
||||
|
||||
@@ -43,8 +43,72 @@
|
||||
|
||||
@push('js')
|
||||
<script>
|
||||
$('#add_address_{{ $prefix }}').click(function() {
|
||||
$('#add_address_container_{{ $prefix }}').toggleClass('d-none');
|
||||
})
|
||||
(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') }}';
|
||||
|
||||
var $toggleBtn = $('#add_address_{{ $prefix }}');
|
||||
|
||||
$toggleBtn.on('click', function() {
|
||||
var isHidden = $formContainer.hasClass('d-none');
|
||||
if (isHidden) {
|
||||
$formContainer.removeClass('d-none');
|
||||
$toggleBtn.prop('disabled', true);
|
||||
}
|
||||
});
|
||||
|
||||
$('#cancel_address_{{ $prefix }}').on('click', function() {
|
||||
$formContainer.addClass('d-none');
|
||||
$formContainer.find('input[type="text"]').val('');
|
||||
$toggleBtn.prop('disabled', false);
|
||||
});
|
||||
|
||||
$('#save_address_{{ $prefix }}').on('click', function() {
|
||||
const data = $formContainer.find(':input').serialize();
|
||||
$.ajax({
|
||||
url: storeUrl,
|
||||
method: 'POST',
|
||||
data: data + '&prefix=' + prefix + (inputName ? '&input_name=' + inputName : ''),
|
||||
success: function(response) {
|
||||
if (response.html) {
|
||||
$list.append(response.html);
|
||||
}
|
||||
$formContainer.addClass('d-none');
|
||||
$formContainer.find('input[type="text"]').val('');
|
||||
$toggleBtn.prop('disabled', false);
|
||||
if (response.id) {
|
||||
const $newRadio = $list.find('#address_' + response.id);
|
||||
$list.find('input[type="radio"]').not($newRadio).prop('checked', false);
|
||||
$newRadio.prop('checked', true);
|
||||
}
|
||||
if (typeof $.fn.iCheck === 'function') {
|
||||
$list.find('input[type="radio"]').iCheck('destroy');
|
||||
if (typeof initIcheck === 'function') {
|
||||
initIcheck('#addresses_list_{{ $prefix }} input[type="radio"]');
|
||||
if (response.id) {
|
||||
$list.find('#address_' + response.id).iCheck('check');
|
||||
}
|
||||
}
|
||||
}
|
||||
const message = response.message || '{{ __('Adresse enregistrée.') }}';
|
||||
if (typeof growl === 'function') {
|
||||
growl(message, 'success');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let message = '{{ __('Une erreur est survenue lors de l\'enregistrement de l\'adresse.') }}';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
if (typeof growl === 'function') {
|
||||
growl(message, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -1,25 +1,174 @@
|
||||
@foreach ($deliveries as $delivery)
|
||||
<div class="row">
|
||||
<div class="col-1 text-right pt-1">
|
||||
@push('styles')
|
||||
<style>
|
||||
.sale-channel-wrapper {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sale-channel-wrapper:not(.blocked) .card-body {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: .75rem;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.sale-channel-wrapper:not(.blocked) .card-body:hover {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0.35rem 0.8rem rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.sale-channel-wrapper.blocked .card-body {
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: .75rem;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.sale-channel-wrapper .card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.sale-channel-toggle {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.sale-channel-content strong {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.sale-channel-warning {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.sale-channel-wrapper .icheck-success > input:first-child + label::before,
|
||||
.sale-channel-wrapper .icheck-primary > input:first-child + label::before,
|
||||
.sale-channel-wrapper .icheck-danger > input:first-child + label::before {
|
||||
opacity: 1;
|
||||
border-width: 2px;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.sale-channel-wrapper.blocked .icheck-success > input:first-child + label::before,
|
||||
.sale-channel-wrapper.blocked .icheck-primary > input:first-child + label::before,
|
||||
.sale-channel-wrapper.blocked .icheck-danger > input:first-child + label::before {
|
||||
border-color: #cbd5f5;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.sale-channel-wrapper .icheck-success > input:first-child + label,
|
||||
.sale-channel-wrapper .icheck-primary > input:first-child + label,
|
||||
.sale-channel-wrapper .icheck-danger > input:first-child + label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sale-channel-wrapper [class*="icheck-"] > input:first-child:disabled + label {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@php
|
||||
$saleChannelsCollection = collect($sale_channels);
|
||||
$firstSaleChannel = $saleChannelsCollection->first();
|
||||
$selectedSaleChannelId = $customer['default_sale_channel_id'] ?? ($firstSaleChannel['id'] ?? null);
|
||||
$cartCount = app('App\\Repositories\\Core\\User\\ShopCart')::count();
|
||||
@endphp
|
||||
|
||||
@if ($cartCount > 0)
|
||||
<div class="alert alert-warning">
|
||||
<strong>Note :</strong> en changeant votre mode d'achat, les articles de votre panier seront transférés sur la liste de prix correspondant au nouveau canal de vente sélectionné.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@foreach ($saleChannelsCollection as $saleChannel)
|
||||
@php
|
||||
$check = $sale_channel_checks[$saleChannel['id']] ?? null;
|
||||
$isBlocked = $check && $saleChannel['id'] !== $selectedSaleChannelId;
|
||||
@endphp
|
||||
<div class="card sale-channel-wrapper mb-3 @if($isBlocked) blocked @endif">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1 sale-channel-toggle">
|
||||
|
||||
@include('components.form.radios.icheck', [
|
||||
'name' => 'delivery_id',
|
||||
'id_name' => 'delivery_id_' . $delivery['id'],
|
||||
'value' => $delivery['id'],
|
||||
'checked' => $customer['sale_delivery_id'] ?? false,
|
||||
'class' => 'delivery',
|
||||
'name' => 'sale_channel_id',
|
||||
'id_name' => 'sale_channel_id_' . $saleChannel['id'],
|
||||
'val' => $saleChannel['id'],
|
||||
'value' => $selectedSaleChannelId,
|
||||
'class' => 'sale-channel',
|
||||
'disabled' => $isBlocked,
|
||||
])
|
||||
</div>
|
||||
<div class="col-11 pt-3">
|
||||
<strong>{{ $delivery['name'] }} - {{ $delivery['sale_channel']['name'] }}</strong><br />
|
||||
<p>{{ $delivery['description'] }}</p>
|
||||
</div>
|
||||
<div class="col-11 sale-channel-content @if($isBlocked) text-muted @endif">
|
||||
<strong>{{ $saleChannel['name'] }}</strong><br />
|
||||
<p class="mb-2">{!! $saleChannel['description'] ?? '' !!}</p>
|
||||
@if ($check)
|
||||
<div class="text-danger small mb-0 sale-channel-warning">
|
||||
@php $missingCount = $check['full_count'] ?? count($check['names']); @endphp
|
||||
@if ($cartCount > 0 && $missingCount >= $cartCount)
|
||||
{{ __('shop.sale_channels.missing_offers_all') }}
|
||||
@else
|
||||
{{ trans_choice('shop.sale_channels.missing_offers', $missingCount, ['count' => $missingCount]) }}
|
||||
<br>
|
||||
@if ($missingCount > 3)
|
||||
<span class="d-block">{{ implode(', ', array_slice($check['names'], 0, 3)) }}, …</span>
|
||||
@else
|
||||
<span class="d-block">{{ implode(', ', $check['names']) }}</span>
|
||||
@endif
|
||||
@endif
|
||||
<div class="d-flex align-items-start mt-1">
|
||||
<span class="mr-1">⚠️</span>
|
||||
<span>{{ __('shop.sale_channels.cannot_select_with_cart') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@push('js')
|
||||
<script>
|
||||
$('.delivery').off().change(function() {
|
||||
console.log($(this).val());
|
||||
const $saleChannels = $('.sale-channel');
|
||||
const updateUrl = '{{ route('Shop.Customers.storeProfileAjax') }}';
|
||||
const token = '{{ csrf_token() }}';
|
||||
const customerId = {{ $customer['id'] ?? 'null' }};
|
||||
let currentSaleChannelId = '{{ $selectedSaleChannelId }}';
|
||||
|
||||
$saleChannels.off().change(function() {
|
||||
if (!customerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSaleChannel = $(this).val();
|
||||
|
||||
$.post(updateUrl, {
|
||||
_token: token,
|
||||
id: customerId,
|
||||
default_sale_channel_id: selectedSaleChannel,
|
||||
}).done(function() {
|
||||
currentSaleChannelId = selectedSaleChannel;
|
||||
window.location.reload();
|
||||
}).fail(function(xhr) {
|
||||
const message = xhr.responseJSON && xhr.responseJSON.message
|
||||
? 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) {
|
||||
$saleChannels.filter('[value="' + currentSaleChannelId + '"]').prop('checked', true);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
'name' => 'phone',
|
||||
'value' => $customer['phone'] ?? (old('phone') ?? ''),
|
||||
'label' => 'Téléphone',
|
||||
'required' => true,
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,69 @@
|
||||
<nav>
|
||||
<div class="nav nav-tabs pl-2">
|
||||
<a href="#deliveriesTab" data-toggle="tab" class="nav-item nav-link active" role="tab" aria-selected="true">
|
||||
MON MODE D'ACHAT
|
||||
</a>
|
||||
<a href="#ordersTab" data-toggle="tab" class="nav-item nav-link" role="tab" aria-selected="false">
|
||||
SUIVI DE COMMANDES
|
||||
</a>
|
||||
<a href="#invoicesTab" data-toggle="tab" class="nav-item nav-link" role="tab" aria-selected="false">
|
||||
FACTURES
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
@php
|
||||
$saleChannels = $sale_channels ?? [];
|
||||
@endphp
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active pt-0 pb-0" id="deliveriesTab">
|
||||
<x-card classBody="bg-light">
|
||||
@include('Shop.Customers.partials.deliveries')
|
||||
</x-card>
|
||||
@if (count($saleChannels) > 1)
|
||||
<nav>
|
||||
<div class="nav nav-tabs pl-2">
|
||||
<a href="#deliveriesTab" data-toggle="tab" class="nav-item nav-link active" role="tab" aria-selected="true">
|
||||
MON MODE D'ACHAT
|
||||
</a>
|
||||
<a href="#ordersTab" data-toggle="tab" class="nav-item nav-link" role="tab" aria-selected="false">
|
||||
SUIVI DE COMMANDES
|
||||
</a>
|
||||
<a href="#invoicesTab" data-toggle="tab" class="nav-item nav-link" role="tab" aria-selected="false">
|
||||
FACTURES
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active pt-0 pb-0" id="deliveriesTab">
|
||||
<x-card classBody="bg-light">
|
||||
@include('Shop.Customers.partials.deliveries')
|
||||
</x-card>
|
||||
</div>
|
||||
<div class="tab-pane fade show pt-0 pb-0" id="ordersTab">
|
||||
<x-card classBody="bg-light">
|
||||
@include('Shop.Orders.partials.list', [
|
||||
'dataTable' => $orders,
|
||||
])
|
||||
</x-card>
|
||||
</div>
|
||||
<div class="tab-pane fade show pt-0 pb-0" id="invoicesTab">
|
||||
<x-card classBody="bg-light">
|
||||
@include('Shop.Invoices.partials.list', [
|
||||
'dataTable' => $invoices,
|
||||
])
|
||||
</x-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade show pt-0 pb-0" id="ordersTab">
|
||||
<x-card classBody="bg-light">
|
||||
@include('Shop.Orders.partials.list', [
|
||||
'dataTable' => $orders,
|
||||
])
|
||||
</x-card>
|
||||
@else
|
||||
<nav>
|
||||
<div class="nav nav-tabs pl-2">
|
||||
<a href="#ordersTab" data-toggle="tab" class="nav-item nav-link active" role="tab" aria-selected="true">
|
||||
SUIVI DE COMMANDES
|
||||
</a>
|
||||
<a href="#invoicesTab" data-toggle="tab" class="nav-item nav-link" role="tab" aria-selected="false">
|
||||
FACTURES
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active pt-0 pb-0" id="ordersTab">
|
||||
<x-card classBody="bg-light">
|
||||
@include('Shop.Orders.partials.list', [
|
||||
'dataTable' => $orders,
|
||||
])
|
||||
</x-card>
|
||||
</div>
|
||||
<div class="tab-pane fade show pt-0 pb-0" id="invoicesTab">
|
||||
<x-card classBody="bg-light">
|
||||
@include('Shop.Invoices.partials.list', [
|
||||
'dataTable' => $invoices,
|
||||
])
|
||||
</x-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade show pt-0 pb-0" id="invoicesTab">
|
||||
<x-card classBody="bg-light">
|
||||
@include('Shop.Invoices.partials.list', [
|
||||
'dataTable' => $invoices,
|
||||
])
|
||||
</x-card>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user