12 Commits

Author SHA1 Message Date
Valentin Lab
1f4177cdb3 fix: missing translation for article's deletion confirmation modal 2025-12-14 21:43:46 +01:00
Valentin Lab
66c035ef9a new: add `duplicate` button for articles 2025-12-14 21:25:42 +01:00
Valentin Lab
fefd6209ac new: add tooltip to the existing links towards "offre" and "tarif" 2025-12-13 22:26:40 +01:00
Valentin Lab
f5ec254c0e new: add a direct link toward article's admin edit form from the public article page 2025-12-13 22:18:42 +01:00
Valentin Lab
a43e82f3d9 new: remove the "previsualisation" side pane from the "offre" admin edit form 2025-12-13 22:07:16 +01:00
Valentin Lab
65460fd9f1 new: add a link to public article page from article and offres admin edit form 2025-12-13 22:04:56 +01:00
Valentin Lab
d9ae84310d fix: avoid sharing route to JSON data to offers
No paths leads there, and there are no reason to keep this route.
2025-12-13 22:02:46 +01:00
Valentin Lab
2fc091d754 chg: make the herited info pane closed by default in admin edit form for articles 2025-12-13 21:46:45 +01:00
Valentin Lab
7887e2d532 new: make all forms have a cancel/save button on the top also 2025-12-13 21:43:40 +01:00
Valentin Lab
f92e175731 fix: prevent error 500 upon displaying empty 'Rayons' 2025-12-13 21:43:40 +01:00
Valentin Lab
6bb910bb54 fix: make 'Rayons' title adaptable to screen width 2025-12-13 21:43:40 +01:00
Valentin Lab
1db3725fb2 new: make the menu visible on mobile 2025-12-13 21:43:40 +01:00
21 changed files with 223 additions and 80 deletions

View File

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

View File

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

View File

@@ -178,10 +178,18 @@ class Articles
$articles = self::getArticlesWithOffers($options);
$searchOrder = $options['ids'] ?? false ? array_flip($options['ids']->toArray()) : false;
foreach ($articles as $article) {
// Skip articles without an offer/tariff/price list for the resolved sale channel
if (!isset($article->offers[0]) || ! $article->offers[0]->tariff) {
continue;
}
$price_lists = $article->offers[0]->tariff->price_lists->toArray();
if (! count($price_lists)) {
continue;
}
if (empty($price_lists[0]['price_list_values'][0] ?? null)) {
continue;
}
if (! is_array($data[$article->name] ?? false)) {
$data[$article->name] = self::getDataForSale($article);
if ($searchOrder) {

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,17 @@
{{ Form::open(['route' => 'Admin.Shop.Offers.store', 'id' => 'offer-form', 'autocomplete' => 'off']) }}
<input type="hidden" name="id" value="{{ $offer['id'] ?? false }}">
@if (($offer['id'] ?? false) && ($offer['article_id'] ?? false))
<div class="d-flex justify-content-end mb-3">
<a href="{{ route('Shop.Articles.show', ['id' => $offer['article_id']]) }}" class="btn btn-outline-primary" target="_blank" rel="noopener">
Voir la page publique de l'article
<i class="fa fa-external-link"></i>
</a>
</div>
@endif
<div class="row mb-3">
<div class="col-8">
<div class="col-12">
<div class="row mb-3">
<div class="col-12">
@include('components.form.select', [
@@ -96,13 +105,6 @@
</div>
@endcomponent
</div>
<div class="col-4">
@component('components.card', ['title' => 'Previsualisation'])
<div id="preview-article"></div>
<div id="preview-variation"></div>
<div id="preview-tariff"></div>
@endcomponent
</div>
</div>
@@ -117,59 +119,8 @@
{!! JsValidator::formRequest('App\Http\Requests\Admin\Shop\StoreOfferPost', '#offer-form') !!}
<script>
function handleArticle() {
$('.select_article').change(function() {
previewArticle($(this).val());
})
}
function previewArticle(id) {
var url = '{{ route('Admin.Shop.Offers.previewArticle') }}/' + id;
$('#preview-article').load(url, function() {
initChevron();
});
}
function handleVariation() {
$('.select_variation').change(function() {
previewVariation($(this).val());
})
}
function previewVariation(id) {
var url = '{{ route('Admin.Shop.Offers.previewVariation') }}/' + id;
$('#preview-variation').load(url, function() {
initChevron();
});
}
function handleTariff() {
$('.select_tariffs').change(function() {
previewTariff($(this).val());
})
}
function previewTariff(id) {
var url = '{{ route('Admin.Shop.Offers.previewTariff') }}/' + id;
$('#preview-tariff').load(url, function() {
initChevron();
});
}
function initPreview() {
previewArticle("{{ $offer['article_id'] ?? null }}");
previewVariation("{{ $offer['variation_id'] ?? null }}");
previewTariff("{{ $offer['tariff_id'] ?? null }}");
}
handleArticle();
handleVariation();
handleTariff();
initChevron();
initSaveForm('#offer-form');
initSelect2();
@if ($offer['id'] ?? false)
initPreview();
@endif
</script>
@endpush

View File

@@ -50,7 +50,16 @@
<div class="col-lg-3 col-xs-12">
@if (auth('web')->check() && !empty($article['available_sale_channels']))
<div id="article-admin-offers" class="alert alert-info p-2 mb-3">
<strong class="d-block">Offres :</strong>
<div class="d-flex justify-content-between align-items-center">
<strong class="d-block mb-0">Offres :</strong>
<a href="{{ route('Admin.Shop.Articles.edit', $article['id']) }}" class="text-dark d-inline-flex align-items-center gap-1" style="font-size: 0.95rem;" title="Ouvrir la fiche article en admin" target="_blank" rel="noopener">
<svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9" />
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" />
</svg>
<span class="sr-only">Éditer l'article</span>
</a>
</div>
<ul class="list-unstyled mb-0 small">
@php
$currentSaleChannelId = $article['current_sale_channel']['id'] ?? null;
@@ -86,7 +95,7 @@
$tariffId = $channel['tariff_id'] ?? null;
@endphp
@if ($tariffId)
<a href="{{ route('Admin.Shop.Tariffs.edit', $tariffId) }}" target="_blank" rel="noopener" class="ml-2 text-nowrap text-right {{ $nameClass }} text-decoration-none text-reset d-inline-block admin-link-group admin-price-link">
<a href="{{ route('Admin.Shop.Tariffs.edit', $tariffId) }}" target="_blank" rel="noopener" title="Ouvrir le tarif" class="ml-2 text-nowrap text-right {{ $nameClass }} text-decoration-none text-reset d-inline-block admin-link-group admin-price-link">
{{ number_format($priceTaxed, 2, ',', ' ') }} € TTC
@if (! empty($quantity))
<span class="d-block text-muted" style="font-size: 0.85em;">Qté min. {{ $quantity }}</span>
@@ -113,7 +122,7 @@
$stockClass = $offer['stock_current'] > 0 ? 'text-success' : 'text-danger';
@endphp
<li class="small {{ $offerClass }}" style="font-size: 0.85em;">
<a href="{{ route('Admin.Shop.Offers.edit', $offer['id']) }}" target="_blank" rel="noopener" class="text-decoration-none {{ $offerClass }} admin-link-group admin-offer-link">
<a href="{{ route('Admin.Shop.Offers.edit', $offer['id']) }}" target="_blank" rel="noopener" title="Ouvrir l'offre" class="text-decoration-none {{ $offerClass }} admin-link-group admin-offer-link">
<div class="d-flex justify-content-between align-items-start">
<div>
<span style="opacity: 0.5;">{{ $isSelectedOffer ? '▸' : '○' }}</span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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