Compare commits
67 Commits
1.0.0-rc.2
...
1.0.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
74
AGENTS.md
Normal file
74
AGENTS.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 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.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Commits in this repo mix Conventional Commit prefixes (`new:`, `fix:`,
|
||||
`chg:`); `fix: prevent null totals`. Keep messages in the imperative
|
||||
mood and reference ticket IDs when available.
|
||||
|
||||
Pull requests must describe scope, list schema or configuration
|
||||
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.
|
||||
|
||||
## 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;
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ class CustomerOrdersDataTable extends DataTable
|
||||
{
|
||||
$datatables
|
||||
->editColumn('status', function (Order $order) {
|
||||
if ($order->status == 0 && in_array($order->payment_type, [2, 3])) {
|
||||
return 'En attente de règlement';
|
||||
}
|
||||
|
||||
return Orders::getStatus($order->status);
|
||||
})
|
||||
->editColumn('created_at', function (Order $order) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -63,6 +63,17 @@ class ArticleController extends Controller
|
||||
return view('Admin.Shop.Articles.edit', $data);
|
||||
}
|
||||
|
||||
public function duplicate($id)
|
||||
{
|
||||
$data = Articles::getFull($id);
|
||||
// Prepare for creation: blank id/slug, tweak name to indicate copy
|
||||
$data['article']['id'] = null;
|
||||
$data['article']['slug'] = null;
|
||||
$data['article']['name'] = ($data['article']['name'] ?? '').' (copie)';
|
||||
|
||||
return view('Admin.Shop.Articles.create', $data);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
return Articles::destroy($id);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,6 +9,7 @@ use App\Repositories\Core\User\ShopCart;
|
||||
use App\Repositories\Shop\Baskets;
|
||||
use App\Repositories\Shop\Contents;
|
||||
use App\Repositories\Shop\Customers;
|
||||
use App\Repositories\Shop\CustomerAddresses;
|
||||
use App\Repositories\Shop\Deliveries;
|
||||
use App\Repositories\Shop\DeliveryTypes;
|
||||
use App\Repositories\Shop\OrderMails;
|
||||
@@ -49,12 +50,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()),
|
||||
];
|
||||
|
||||
@@ -78,7 +102,9 @@ class OrderController extends Controller
|
||||
}
|
||||
OrderMails::sendOrderConfirmed($order->id);
|
||||
|
||||
return redirect()->route('Shop.Orders.confirmed');
|
||||
return redirect()->route('Shop.Orders.confirmed', [
|
||||
'payment_type' => $data['payment_type'],
|
||||
]);
|
||||
}
|
||||
|
||||
return view('Shop.Orders.order');
|
||||
@@ -87,9 +113,18 @@ class OrderController extends Controller
|
||||
public function confirmed()
|
||||
{
|
||||
ShopCart::clear();
|
||||
$paymentType = request('payment_type');
|
||||
$content = Contents::getOrderConfirmedContent();
|
||||
$paymentLabel = match ($paymentType) {
|
||||
'2' => 'chèque',
|
||||
'3' => 'virement',
|
||||
default => null,
|
||||
};
|
||||
|
||||
return view('Shop.Orders.confirmed', ['content' => $content]);
|
||||
return view('Shop.Orders.confirmed', [
|
||||
'content' => $content,
|
||||
'payment_label' => $paymentLabel,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPdf($uuid)
|
||||
|
||||
@@ -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()],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
@@ -70,9 +73,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 +135,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 +178,18 @@ class Articles
|
||||
$articles = self::getArticlesWithOffers($options);
|
||||
$searchOrder = $options['ids'] ?? false ? array_flip($options['ids']->toArray()) : false;
|
||||
foreach ($articles as $article) {
|
||||
// Skip articles without an offer/tariff/price list for the resolved sale channel
|
||||
if (!isset($article->offers[0]) || ! $article->offers[0]->tariff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$price_lists = $article->offers[0]->tariff->price_lists->toArray();
|
||||
if (! count($price_lists)) {
|
||||
continue;
|
||||
}
|
||||
if (empty($price_lists[0]['price_list_values'][0] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
if (! is_array($data[$article->name] ?? false)) {
|
||||
$data[$article->name] = self::getDataForSale($article);
|
||||
if ($searchOrder) {
|
||||
@@ -166,7 +204,7 @@ class Articles
|
||||
ksort($data);
|
||||
}
|
||||
|
||||
return $data ?? false;
|
||||
return $data ?? [];
|
||||
}
|
||||
|
||||
public static function getDataForSale($article)
|
||||
|
||||
@@ -100,12 +100,19 @@ 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,
|
||||
@@ -115,6 +122,24 @@ class Baskets
|
||||
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']);
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -166,4 +169,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
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,45 @@ div.megamenu ul.megamenu li.megamenu.level1
|
||||
.dropdown-menu > li:hover > .submenu{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px){
|
||||
#navbarContentMobile {
|
||||
max-height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
#navbarContentMobile .navbar-nav {
|
||||
flex-direction: column;
|
||||
}
|
||||
#navbarContentMobile .navbar-nav .col {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
#navbarContentMobile .dropdown-menu {
|
||||
display: block;
|
||||
position: static;
|
||||
float: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
#navbarContentMobile .dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
#navbarContentMobile .dropdown-menu .container,
|
||||
#navbarContentMobile .dropdown-menu .row {
|
||||
margin: 0;
|
||||
}
|
||||
#navbarContentMobile .dropdown-menu .shadow {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.category-card .card-body {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Supprimer les grandes marges du container en affichage mobile/tablette */
|
||||
.container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,8 +1,17 @@
|
||||
{{ Form::open(['route' => 'Admin.Shop.Offers.store', 'id' => 'offer-form', 'autocomplete' => 'off']) }}
|
||||
<input type="hidden" name="id" value="{{ $offer['id'] ?? false }}">
|
||||
|
||||
@if (($offer['id'] ?? false) && ($offer['article_id'] ?? false))
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a href="{{ route('Shop.Articles.show', ['id' => $offer['article_id']]) }}" class="btn btn-outline-primary" target="_blank" rel="noopener">
|
||||
Voir la page publique de l'article
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-8">
|
||||
<div class="col-12">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
@include('components.form.select', [
|
||||
@@ -96,13 +105,6 @@
|
||||
</div>
|
||||
@endcomponent
|
||||
</div>
|
||||
<div class="col-4">
|
||||
@component('components.card', ['title' => 'Previsualisation'])
|
||||
<div id="preview-article"></div>
|
||||
<div id="preview-variation"></div>
|
||||
<div id="preview-tariff"></div>
|
||||
@endcomponent
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -117,59 +119,8 @@
|
||||
{!! JsValidator::formRequest('App\Http\Requests\Admin\Shop\StoreOfferPost', '#offer-form') !!}
|
||||
|
||||
<script>
|
||||
function handleArticle() {
|
||||
$('.select_article').change(function() {
|
||||
previewArticle($(this).val());
|
||||
})
|
||||
}
|
||||
|
||||
function previewArticle(id) {
|
||||
var url = '{{ route('Admin.Shop.Offers.previewArticle') }}/' + id;
|
||||
$('#preview-article').load(url, function() {
|
||||
initChevron();
|
||||
});
|
||||
}
|
||||
|
||||
function handleVariation() {
|
||||
$('.select_variation').change(function() {
|
||||
previewVariation($(this).val());
|
||||
})
|
||||
}
|
||||
|
||||
function previewVariation(id) {
|
||||
var url = '{{ route('Admin.Shop.Offers.previewVariation') }}/' + id;
|
||||
$('#preview-variation').load(url, function() {
|
||||
initChevron();
|
||||
});
|
||||
}
|
||||
|
||||
function handleTariff() {
|
||||
$('.select_tariffs').change(function() {
|
||||
previewTariff($(this).val());
|
||||
})
|
||||
}
|
||||
|
||||
function previewTariff(id) {
|
||||
var url = '{{ route('Admin.Shop.Offers.previewTariff') }}/' + id;
|
||||
$('#preview-tariff').load(url, function() {
|
||||
initChevron();
|
||||
});
|
||||
}
|
||||
|
||||
function initPreview() {
|
||||
previewArticle("{{ $offer['article_id'] ?? null }}");
|
||||
previewVariation("{{ $offer['variation_id'] ?? null }}");
|
||||
previewTariff("{{ $offer['tariff_id'] ?? null }}");
|
||||
}
|
||||
|
||||
handleArticle();
|
||||
handleVariation();
|
||||
handleTariff();
|
||||
initChevron();
|
||||
initSaveForm('#offer-form');
|
||||
initSelect2();
|
||||
@if ($offer['id'] ?? false)
|
||||
initPreview();
|
||||
@endif
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,37 +1,59 @@
|
||||
@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']['semences'] ?? false)
|
||||
&& empty($article['offers']['plants'] ?? false)
|
||||
&& empty($article['offers']['legumes'] ?? false)
|
||||
&& empty($article['offers']['marchandise'] ?? false);
|
||||
$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 ($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 --}}
|
||||
@if ($article['offers']['semences'] ?? false)
|
||||
@include('Shop.Articles.partials.addBasket', [
|
||||
'data' => $article['offers']['semences'],
|
||||
'title' => 'Semences',
|
||||
'model' => 'semences',
|
||||
'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']['plants'] ?? false)
|
||||
@include('Shop.Articles.partials.addBasket', [
|
||||
'data' => $article['offers']['plants'],
|
||||
'title' => 'Plants',
|
||||
'model' => 'plants',
|
||||
'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 ($article['offers']['legumes'] ?? false)
|
||||
@include('Shop.Articles.partials.addBasket', [
|
||||
'data' => $article['offers']['legumes'],
|
||||
'title' => 'Légumes',
|
||||
'model' => 'legumes',
|
||||
'bgClass' => 'bg-green-light',
|
||||
])
|
||||
@endif
|
||||
|
||||
@if ($article['offers']['marchandise'] ?? false)
|
||||
@include('Shop.Articles.partials.addBasket', [
|
||||
'data' => $article['offers']['marchandise'],
|
||||
'title' => 'Marchandises',
|
||||
'model' => 'marchandise',
|
||||
'bgClass' => 'bg-green-light',
|
||||
])
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@include('load.basket')
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5 col-xs-12 text-justify">
|
||||
{!! $article['description']['variety'] ?? null !!}
|
||||
{!! $article['description']['semences'] ?? null !!}
|
||||
{!! $article['description']['plants'] ?? null !!}
|
||||
{!! $article['description']['variety'] ?? null !!}
|
||||
{!! $article['description']['merchandise'] ?? null !!}
|
||||
|
||||
@if ($article['description']['plus'] ?? false)
|
||||
@@ -48,9 +48,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
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
updateBasket(offer_id, quantity, function() {
|
||||
calculatePrice($row);
|
||||
calculateTotal();
|
||||
});
|
||||
}, $row);
|
||||
});
|
||||
|
||||
$('.basket-delete').click(function() {
|
||||
@@ -70,13 +70,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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,8 +4,20 @@
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{!! $content !!}
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fa fa-check-circle text-success" style="font-size: 5rem;"></i>
|
||||
<div class="mt-4" style="font-size: 1.2rem;">
|
||||
{!! $content !!}
|
||||
</div>
|
||||
@if($payment_label ?? false)
|
||||
<div class="mt-3" style="font-size: 1.1rem;">
|
||||
Votre commande a bien été enregistrée, elle vous sera expédiée dès réception de votre {{ $payment_label }}.
|
||||
</div>
|
||||
<div class="mt-3" style="font-size: 1.1rem;">
|
||||
<i class="fa fa-exclamation-triangle text-warning mr-1"></i>
|
||||
Sans réception de votre paiement au bout de 30 jours, votre commande sera annulée.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -40,7 +40,10 @@
|
||||
<div class="col-sm-12 col-lg-4">
|
||||
<x-card class='shadow'>
|
||||
<div id="basketTotal">
|
||||
@include('Shop.Baskets.partials.basketTotal', ['basket' => $basket])
|
||||
@include('Shop.Baskets.partials.basketTotal', [
|
||||
'basket' => $basket,
|
||||
'sale_channel' => $basket['sale_channel'] ?? null,
|
||||
])
|
||||
</div>
|
||||
</x-card>
|
||||
</div>
|
||||
@@ -50,7 +53,7 @@
|
||||
|
||||
@include('load.layout.chevron')
|
||||
|
||||
@push('js')
|
||||
@prepend('js')
|
||||
<script>
|
||||
$('#customer').click(function() {
|
||||
$(".personal_data").addClass('d-none');
|
||||
@@ -65,7 +68,9 @@
|
||||
});
|
||||
|
||||
function refreshBasketTotal(deliveryId, deliveryTypeId) {
|
||||
options = deliveryId + '/' + deliveryTypeId;
|
||||
var safeDeliveryId = deliveryId !== undefined && deliveryId !== null ? deliveryId : '';
|
||||
var safeDeliveryTypeId = deliveryTypeId !== undefined && deliveryTypeId !== null ? deliveryTypeId : '';
|
||||
var options = safeDeliveryId + '/' + safeDeliveryTypeId;
|
||||
$.get("{{ Route('Shop.Basket.getBasketTotal') }}/" + options, function(data) {
|
||||
$('#basketTotal').html(data);
|
||||
});
|
||||
@@ -73,4 +78,4 @@
|
||||
|
||||
initChevron();
|
||||
</script>
|
||||
@endpush
|
||||
@endprepend
|
||||
|
||||
@@ -1,13 +1,51 @@
|
||||
@php
|
||||
$addresses = collect($addresses ?? []);
|
||||
$preselectedAddressId = old($name);
|
||||
|
||||
if ($preselectedAddressId === null && is_string($name) && str_contains($name, '[')) {
|
||||
$dotName = preg_replace('/\[(.*?)\]/', '.$1', $name);
|
||||
$dotName = trim($dotName, '.');
|
||||
$preselectedAddressId = $dotName ? old($dotName) : null;
|
||||
}
|
||||
|
||||
if (($preselectedAddressId === null || $preselectedAddressId === '') && $addresses->isNotEmpty()) {
|
||||
$defaultAddress = $addresses->firstWhere('priority', 1);
|
||||
|
||||
if (! $defaultAddress) {
|
||||
$defaultAddress = $addresses
|
||||
->filter(function ($address) {
|
||||
return (int) ($address['priority'] ?? 0) > 0;
|
||||
})
|
||||
->sortByDesc(function ($address) {
|
||||
return (int) ($address['priority'] ?? 0);
|
||||
})
|
||||
->first();
|
||||
}
|
||||
|
||||
if (! $defaultAddress) {
|
||||
$defaultAddress = $addresses->firstWhere('is_default', true)
|
||||
?? $addresses->firstWhere('default', true);
|
||||
}
|
||||
|
||||
if (! $defaultAddress) {
|
||||
$defaultAddress = $addresses->first();
|
||||
}
|
||||
|
||||
$preselectedAddressId = $defaultAddress['id'] ?? null;
|
||||
}
|
||||
|
||||
$addresses = $addresses->all();
|
||||
@endphp
|
||||
|
||||
@if ($addresses)
|
||||
@foreach ($addresses ?? [] as $address)
|
||||
@foreach ($addresses as $address)
|
||||
<div class="row mb-3">
|
||||
<div class="col-1">
|
||||
@include('components.form.radios.icheck', [
|
||||
'name' => $name,
|
||||
'val' => $address['id'],
|
||||
'id' => $prefix . '_address_' . $address['id'],
|
||||
'value' =>
|
||||
old($name) ?? $address['priority'] || count($addresses) === 1 ? $address['id'] : false,
|
||||
'value' => $preselectedAddressId,
|
||||
])
|
||||
</div>
|
||||
<div class="col-11">
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
@php
|
||||
$defaultSaleChannelId = $customer['default_sale_channel_id'] ?? null;
|
||||
$preselectedDeliveryId = old('delivery_id');
|
||||
|
||||
if (! $preselectedDeliveryId && $defaultSaleChannelId) {
|
||||
$match = collect($deliveries)->firstWhere('sale_channel_id', $defaultSaleChannelId);
|
||||
$preselectedDeliveryId = $match['id'] ?? null;
|
||||
}
|
||||
@endphp
|
||||
|
||||
@foreach ($deliveries as $delivery)
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
@include('components.form.radios.icheck', [
|
||||
'name' => 'delivery_id',
|
||||
'val' => $delivery['id'],
|
||||
'value' => (int) old('delivery_id') === $delivery['id'] ? $delivery['id'] : null,
|
||||
'value' => $preselectedDeliveryId,
|
||||
'id' => 'delivery_' . $delivery['id'],
|
||||
'class' => 'delivery_mode' . ($delivery['at_house'] ? ' at_house' : ''),
|
||||
])
|
||||
@@ -29,16 +39,29 @@ ci-contre
|
||||
@push('js')
|
||||
<script>
|
||||
function handleDeliveries() {
|
||||
$('#delivery_mode input.delivery_mode').change(function() {
|
||||
if ($(this).hasClass('at_house')) {
|
||||
var $deliveryInputs = $('#delivery_mode input.delivery_mode');
|
||||
|
||||
$deliveryInputs.change(function() {
|
||||
var $currentDelivery = $(this);
|
||||
var deliveryTypeId = $('input[name=delivery_type_id]:checked').val();
|
||||
|
||||
if ($currentDelivery.hasClass('at_house')) {
|
||||
$('#delivery_addresses').closest('.card').removeClass('d-none');
|
||||
var deliveryTypeId = $('input[name=delivery_type_id]:checked').val()
|
||||
} else {
|
||||
$('#delivery_addresses').closest('.card').addClass('d-none');
|
||||
}
|
||||
var deliveryId = $(this).val();
|
||||
|
||||
var deliveryId = $currentDelivery.val();
|
||||
refreshBasketTotal(deliveryId, deliveryTypeId);
|
||||
});
|
||||
|
||||
var $preselected = $deliveryInputs.filter(':checked').first();
|
||||
|
||||
if ($preselected.length) {
|
||||
$preselected.trigger('change');
|
||||
} else {
|
||||
$('#delivery_addresses').closest('.card').addClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
handleDeliveries();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<div id="registred">
|
||||
<x-layout.collapse id="invoice_addresses" title="Adresse de facturation" class="rounded-lg mb-3" uncollapsed=true>
|
||||
@include('Shop.Orders.partials.addresses', [
|
||||
'addresses' => $customer['invoice_addresses'] ?? false,
|
||||
'prefix' => 'invoice',
|
||||
'name' => 'invoice[invoice_address_id]',
|
||||
@include('Shop.Customers.partials.addresses', [
|
||||
'addresses' => $customer['invoice_addresses'] ?? [],
|
||||
'prefix' => 'invoices',
|
||||
'inputName' => 'invoice[invoice_address_id]',
|
||||
'with_name' => true,
|
||||
'selected' => $customer['invoice_address_id'] ?? null,
|
||||
])
|
||||
</x-layout.collapse>
|
||||
|
||||
@@ -13,10 +15,12 @@
|
||||
|
||||
<x-layout.collapse id="delivery_addresses" title="Adresse de livraison" class="rounded-lg mb-3 d-none"
|
||||
uncollapsed=true>
|
||||
@include('Shop.Orders.partials.addresses', [
|
||||
'addresses' => $customer['delivery_addresses'] ?? false,
|
||||
'prefix' => 'delivery',
|
||||
'name' => 'delivery_address_id',
|
||||
@include('Shop.Customers.partials.addresses', [
|
||||
'addresses' => $customer['delivery_addresses'] ?? [],
|
||||
'prefix' => 'deliveries',
|
||||
'inputName' => 'delivery_address_id',
|
||||
'with_name' => true,
|
||||
'selected' => $customer['delivery_address_id'] ?? null,
|
||||
])
|
||||
@include('Shop.Orders.partials.shipping')
|
||||
</x-layout.collapse>
|
||||
|
||||
@@ -7,7 +7,18 @@
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach ($delivery_types as $delivery_type_id => $delivery_type)
|
||||
@php
|
||||
$deliveryTypes = collect($delivery_types);
|
||||
$preselectedDeliveryTypeId = old('delivery_type_id');
|
||||
|
||||
if ($preselectedDeliveryTypeId === null || $preselectedDeliveryTypeId === '') {
|
||||
$preselectedDeliveryTypeId = $deliveryTypes->keys()->first();
|
||||
}
|
||||
|
||||
$deliveryTypes = $deliveryTypes->all();
|
||||
@endphp
|
||||
|
||||
@foreach ($deliveryTypes as $delivery_type_id => $delivery_type)
|
||||
<tr>
|
||||
<td>
|
||||
@include('components.form.radios.icheck', [
|
||||
@@ -15,6 +26,7 @@
|
||||
'val' => $delivery_type_id,
|
||||
'id' => 'delivery_type_' . $delivery_type_id,
|
||||
'class' => 'delivery_type',
|
||||
'value' => $preselectedDeliveryTypeId,
|
||||
])
|
||||
</td>
|
||||
<td>
|
||||
@@ -31,11 +43,19 @@
|
||||
@push('js')
|
||||
<script>
|
||||
function handleDeliveryTypes() {
|
||||
$('input.delivery_type').change(function() {
|
||||
var $deliveryTypeInputs = $('input.delivery_type');
|
||||
|
||||
$deliveryTypeInputs.change(function() {
|
||||
var deliveryTypeId = $(this).val();
|
||||
var deliveryId = $('input[name=delivery_id]:checked').val()
|
||||
refreshBasketTotal(deliveryId, deliveryTypeId);
|
||||
});
|
||||
|
||||
var $preselected = $deliveryTypeInputs.filter(':checked').first();
|
||||
|
||||
if ($preselected.length) {
|
||||
$preselected.trigger('change');
|
||||
}
|
||||
}
|
||||
handleDeliveryTypes();
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<h1 style="font-size: 1.5em;">
|
||||
<h1 class="breadcrumb-title">
|
||||
@foreach($breadcrumb ?? [] as $parent)
|
||||
<a href="{{ route('Shop.Categories.show', ['id' => $parent['id']]) }}" style="text-decoration: none; color: inherit;">{{ $parent['name'] }}</a> /
|
||||
<a href="{{ route('Shop.Categories.show', ['id' => $parent['id']]) }}" class="breadcrumb-link">{{ $parent['name'] }}</a> /
|
||||
@endforeach
|
||||
<span style="font-size: 1.4em;">
|
||||
{{ $category['name'] }}
|
||||
</span>
|
||||
<span class="breadcrumb-current">{{ $category['name'] }}</span>
|
||||
</h1>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<h1 style="font-size: 2em;">{{ $category['name'] }}</h1>
|
||||
<h3 style="font-size: 1.2em;">{!! $category['description'] !!}</h3>
|
||||
<h1 class="category-title">{{ $category['name'] }}</h1>
|
||||
<h3 class="category-description">{!! $category['description'] !!}</h3>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
@include('Shop.layout.partials.category_add')
|
||||
@@ -12,4 +12,4 @@
|
||||
<div class="col-12">
|
||||
@include('Shop.layout.partials.category_articles')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="row">
|
||||
<div class="row mx-n1">
|
||||
@if ($articles ?? false)
|
||||
@foreach ($articles as $product_name => $article)
|
||||
<div class="col-lg-3 col-xs-12 mb-3">
|
||||
<div class="category-card col-6 col-md-4 col-lg-3 mb-2 px-1">
|
||||
@include('Shop.Articles.partials.article')
|
||||
</div>
|
||||
@endforeach
|
||||
@@ -46,4 +46,3 @@
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
|
||||
62
resources/views/Shop/auth/partials/password_rules.blade.php
Normal file
62
resources/views/Shop/auth/partials/password_rules.blade.php
Normal file
@@ -0,0 +1,62 @@
|
||||
@php
|
||||
$passwordInputId = $passwordInputId ?? 'password';
|
||||
@endphp
|
||||
|
||||
<ul class="list-unstyled small mt-1 mb-0 password-rules" data-input="#{{ $passwordInputId }}" style="display: none;">
|
||||
<li data-rule="length"><i class="fa fa-fw fa-times"></i> Au moins 8 caractères</li>
|
||||
<li data-rule="lowercase"><i class="fa fa-fw fa-times"></i> Au moins une lettre minuscule</li>
|
||||
<li data-rule="uppercase"><i class="fa fa-fw fa-times"></i> Au moins une lettre majuscule</li>
|
||||
<li data-rule="number"><i class="fa fa-fw fa-times"></i> Au moins un chiffre</li>
|
||||
<li data-rule="special"><i class="fa fa-fw fa-times"></i> Au moins un caractère spécial</li>
|
||||
</ul>
|
||||
|
||||
@once
|
||||
@push('css')
|
||||
<style>
|
||||
.password-rules li {
|
||||
color: #dc3545;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.password-rules li.valid {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('js')
|
||||
<script>
|
||||
$(function() {
|
||||
$('.password-rules').each(function() {
|
||||
var $rules = $(this);
|
||||
var inputSelector = $rules.data('input');
|
||||
var $input = $(inputSelector);
|
||||
|
||||
if (!$input.length) return;
|
||||
|
||||
var checks = {
|
||||
length: function(v) { return v.length >= 8; },
|
||||
lowercase: function(v) { return /[a-z]/.test(v); },
|
||||
uppercase: function(v) { return /[A-Z]/.test(v); },
|
||||
number: function(v) { return /[0-9]/.test(v); },
|
||||
special: function(v) { return /[^A-Za-z0-9]/.test(v); }
|
||||
};
|
||||
|
||||
$input.on('input keyup', function() {
|
||||
var val = $(this).val();
|
||||
if (val.length === 0) {
|
||||
$rules.hide();
|
||||
return;
|
||||
}
|
||||
$rules.find('li').each(function() {
|
||||
var rule = $(this).data('rule');
|
||||
if (checks[rule]) {
|
||||
$(this).toggleClass('valid', checks[rule](val));
|
||||
}
|
||||
});
|
||||
$rules.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -11,10 +11,12 @@
|
||||
<label>Mot de passe *</label>
|
||||
{{ Form::password('password', [
|
||||
'class' => 'form-control',
|
||||
'id' => 'password',
|
||||
'placeholder' => __('boilerplate::auth.fields.password'),
|
||||
'required',
|
||||
]) }}
|
||||
{!! $errors->registration->first('password', '<p class="text-danger"><strong>:message</strong></p>') !!}
|
||||
@include('Shop.auth.partials.password_rules', ['passwordInputId' => 'password'])
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
<label for="new-password" class="col-md-6 control-label text-right">Nouveau mot de passe</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input id="new-password" type="password" class="form-control" name="new-password" required>
|
||||
<input id="new-password" type="password" class="form-control" name="new-password">
|
||||
@include('Shop.auth.partials.password_rules', ['passwordInputId' => 'new-password'])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +22,6 @@
|
||||
<label for="new-password-confirm" class="col-md-6 control-label text-right">Confirmez votre mot de passe</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input id="new-password-confirm" type="password" class="form-control" name="new-password_confirmation" required>
|
||||
<input id="new-password-confirm" type="password" class="form-control" name="new-password_confirmation">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{{ asset('img/favicon.ico') }}">
|
||||
<link rel="stylesheet" href="/css/site.min.css?{{ date('Ymd') }}" type="text/css" media="all">
|
||||
|
||||
@stack('styles')
|
||||
@stack('css')
|
||||
|
||||
</head>
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
<div class="row bg-light">
|
||||
<div class="row bg-light align-items-center">
|
||||
|
||||
<div class="col-sm-12 col-lg-5">
|
||||
<div class="col-6 col-lg-5 d-flex align-items-center">
|
||||
<a href="/"><img src="/img/logo.png" height="52" alt="Jardin'Envie"></a>
|
||||
<span class="green ml-3">Variétés Paysannes de la Semence à l'Assiette</span>
|
||||
<span class="green ml-3 d-none d-md-inline">Variétés Paysannes de la Semence à l'Assiette</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-lg-4 pt-2">
|
||||
<div class="col-12 col-lg-4 pt-2 order-3 order-lg-2">
|
||||
@include('Shop.layout.partials.search')
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-lg-3 pt-2 text-right">
|
||||
<div class="col-6 col-lg-3 pt-2 text-right order-2 order-lg-3 d-flex justify-content-end align-items-center">
|
||||
@include('Shop.layout.partials.header-catalog')
|
||||
@include('Shop.layout.partials.header-profile')
|
||||
@include('Shop.layout.partials.header-basket')
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row d-lg-none bg-green-dark">
|
||||
<div class="col-12 p-0">
|
||||
<div class="collapse" id="navbarContentMobile">
|
||||
<nav class="navbar navbar-dark p-0">
|
||||
@include('Shop.layout.partials.sections-menu-list')
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,17 +10,40 @@
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="btn btn-sm fa fa-search"></i></span>
|
||||
</div>
|
||||
<div class="input-group-append d-lg-none">
|
||||
<button class="navbar-toggler navbar-light" type="button" data-toggle="collapse"
|
||||
data-target="#navbarContentMobile" aria-controls="navbarContentMobile" aria-expanded="false"
|
||||
aria-label="Menu catégories">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@push('js')
|
||||
<script>
|
||||
$(function() {
|
||||
const articleShowUrlTemplate = "{{ route('Shop.Articles.show', ['id' => '__ARTICLE_ID__']) }}";
|
||||
|
||||
$('#search-general .fa-search').click(function() {
|
||||
$('#search-general').submit();
|
||||
});
|
||||
|
||||
initAutocomplete('#search_name');
|
||||
function redirectToArticle(item, evt) {
|
||||
if (!item || typeof item.value === 'undefined' || item.value === null || item.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
const targetUrl = articleShowUrlTemplate.replace('__ARTICLE_ID__', item.value);
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
|
||||
initAutocomplete('#search_name', redirectToArticle);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<ul class="navbar-nav w-100">
|
||||
@foreach ($categories as $menu)
|
||||
<li class="nav-item dropdown megamenu p-2 col
|
||||
@if (in_array($menu['id'], [$category['id'] ?? false, $category['parent_id'] ?? false, $category['parent']['parent_id'] ?? false])) active @endif">
|
||||
@if ($menu['children'] ?? false)
|
||||
<a id="megamenu_{{ $menu['id'] }}" href="{{ route('Shop.Categories.show', ['id' => $menu['id']]) }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="nav-link dropdown-toggle text-uppercase">
|
||||
{{ $menu['name'] }}
|
||||
</a>
|
||||
<div aria-labelledby="megamenu_{{ $menu['id'] }}" class="dropdown-menu border-0 p-0 m-0">
|
||||
@include('Shop.layout.partials.megamenu')
|
||||
</div>
|
||||
@else
|
||||
<a href="{{ route('Shop.Categories.show', ['id' => $menu['id']]) }}" class="nav-link text-uppercase text-white">
|
||||
{{ $menu['name'] }}
|
||||
</a>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@@ -1,27 +1,9 @@
|
||||
<div class="row mb-3 bg-green-dark">
|
||||
<div class="row mb-3 bg-green-dark d-none d-lg-block">
|
||||
<div class="col-12 pl-0 pr-0">
|
||||
<nav class="navbar navbar-expand-lg p-0">
|
||||
<div class="collapse navbar-collapse" id="navbarContent">
|
||||
<ul class="navbar-nav w-100">
|
||||
@foreach ($categories as $menu)
|
||||
<li class="nav-item dropdown megamenu p-2 col
|
||||
@if (in_array($menu['id'], [$category['id'] ?? false, $category['parent_id'] ?? false, $category['parent']['parent_id'] ?? false])) active @endif">
|
||||
@if ($menu['children'] ?? false)
|
||||
<a id="megamenu_{{ $menu['id'] }}" href="{{ route('Shop.Categories.show', ['id' => $menu['id']]) }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="nav-link dropdown-toggle text-uppercase">
|
||||
{{ $menu['name'] }}
|
||||
</a>
|
||||
<div aria-labelledby="megamenu_{{ $menu['id'] }}" class="dropdown-menu border-0 p-0 m-0">
|
||||
@include('Shop.layout.partials.megamenu')
|
||||
</div>
|
||||
@else
|
||||
<a href="{{ route('Shop.Categories.show', ['id' => $menu['id']]) }}" class="nav-link text-uppercase text-white">
|
||||
{{ $menu['name'] }}
|
||||
</a>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark p-0">
|
||||
<div class="navbar-collapse show" id="navbarContent">
|
||||
@include('Shop.layout.partials.sections-menu-list')
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,20 @@
|
||||
@isset($trigger) data-trigger="{{ $trigger }}" @endisset
|
||||
@isset($container) data-container="{{ $container }}" @endisset
|
||||
@isset($html) data-html="true" @endisset
|
||||
@isset($metadata) {{ $metadata }} @endisset>
|
||||
@isset($metadata) {{ $metadata }} @endisset
|
||||
@isset($attr)
|
||||
@if (is_array($attr))
|
||||
@foreach ($attr as $key => $value)
|
||||
@if ($value === true)
|
||||
{{ $key }}
|
||||
@elseif ($value !== false && $value !== null)
|
||||
{{ $key }}="{{ $value }}"
|
||||
@endif
|
||||
@endforeach
|
||||
@else
|
||||
{{ $attr }}
|
||||
@endif
|
||||
@endisset>
|
||||
<i class="fa fa-fw {{ $icon ?? '' }}"></i>
|
||||
{{ $txt ?? '' }}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@include('components.form.button', [
|
||||
'class' => 'btn-info duplicate ' . ($class ?? ''),
|
||||
'icon' => 'fa-copy',
|
||||
'txt' => __('Dupliquer'),
|
||||
'attr' => ['data-url' => $duplicate_url ?? $duplicateUrl ?? null],
|
||||
])
|
||||
@@ -1,7 +1,33 @@
|
||||
@php
|
||||
$cancelUrl = $cancel_url ?? $cancelUrl ?? null;
|
||||
$duplicateUrl = $duplicate_url ?? $duplicateUrl ?? null;
|
||||
@endphp
|
||||
|
||||
@push('header-actions')
|
||||
<div class="form-buttons d-flex align-items-center ml-3">
|
||||
@include('components.form.buttons.button-cancel', [
|
||||
'class' => 'btn-sm mr-2',
|
||||
'url' => $cancelUrl,
|
||||
])
|
||||
@if($duplicateUrl)
|
||||
@include('components.form.buttons.button-duplicate', [
|
||||
'class' => 'btn-sm mr-2',
|
||||
'duplicate_url' => $duplicateUrl,
|
||||
])
|
||||
@endif
|
||||
@include('components.form.buttons.button-save', [
|
||||
'class' => 'btn-sm',
|
||||
])
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
<div class="row pt-0 pb-3">
|
||||
<div class="col-12">
|
||||
<div class="text-right form-buttons">
|
||||
@include('components.form.buttons.button-cancel')
|
||||
@include('components.form.buttons.button-cancel', ['url' => $cancelUrl])
|
||||
@if($duplicateUrl)
|
||||
@include('components.form.buttons.button-duplicate', ['duplicate_url' => $duplicateUrl])
|
||||
@endif
|
||||
@include('components.form.buttons.button-save')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<div class="content-header pt-2 pb-1">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2 align-items-end">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">
|
||||
{{ $title ?? null}}
|
||||
@isset($subtitle)
|
||||
<small class="font-weight-light ml-1 text-md">{{ $subtitle }}</small>
|
||||
@endisset
|
||||
</h1>
|
||||
<div class="row mb-2 align-items-center">
|
||||
<div class="col-sm-8">
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<h1 class="m-0 text-dark d-flex align-items-center flex-grow-1">
|
||||
{{ $title ?? null}}
|
||||
@isset($subtitle)
|
||||
<small class="font-weight-light ml-1 text-md">{{ $subtitle }}</small>
|
||||
@endisset
|
||||
</h1>
|
||||
@stack('header-actions')
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="col-sm-4">
|
||||
<ol class="breadcrumb float-sm-right text-sm">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{{ route('boilerplate.dashboard') }}">
|
||||
|
||||
@@ -94,6 +94,18 @@
|
||||
|
||||
@stack('scripts')
|
||||
@stack('js')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.form-buttons .duplicate').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var url = this.dataset.url || this.getAttribute('data-url');
|
||||
if (url) {
|
||||
window.location = url;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
var id = item.value;
|
||||
$('#' + field).val(id);
|
||||
|
||||
if (typeof(callback) != 'undefined') {
|
||||
var c = callback + '(' + id + ')';
|
||||
eval(c);
|
||||
if (typeof callback === 'function') {
|
||||
callback.call(this, item, evt);
|
||||
} else if (typeof callback === 'string' && callback.length && typeof window[callback] === 'function') {
|
||||
window[callback].call(this, item, evt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
/<(p|a|div|span|strike|strong|i|u)[^>]*?>(\s| |<br\/>|\r|\n)*?<\/(p|a|div|span|strike|strong|i|u)>/gi,
|
||||
''); // Empty tags
|
||||
},
|
||||
skin: "boilerplate",
|
||||
skin: "oxide",
|
||||
content_css: 'default',
|
||||
language: '{{ App::getLocale() }}',
|
||||
file_picker_callback: function(callback, value, meta) {
|
||||
var x = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName(
|
||||
|
||||
@@ -9,14 +9,16 @@
|
||||
var selector = (typeof(sel) == 'undefined') ? '.save' : sel;
|
||||
$(selector).off().click(function(e) {
|
||||
if (typeof initValidator === 'function') {
|
||||
console.log('click');
|
||||
e.preventDefault();
|
||||
if ($(form).valid()) {
|
||||
$(this).prop("disabled", true);
|
||||
$(this).html($(this).data('loading-text'));
|
||||
$(form).submit();
|
||||
} else {
|
||||
console.log('erreur');
|
||||
const $firstInvalid = $(form).find(':invalid, .error').first();
|
||||
if ($firstInvalid.length) {
|
||||
$firstInvalid.focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$(form).submit();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user