Compare commits

...

17 Commits

Author SHA1 Message Date
Valentin Lab
ef1964d472 fix: prevent error 500 on article pages 2025-11-03 11:35:16 +01:00
Valentin Lab
abb32e32b9 fix: add "Bientôt disponible" box on public product page without prices 2025-11-03 11:27:47 +01:00
Valentin Lab
8c29459489 new: make the debug info available to all backoffice users with helpful links 2025-11-03 11:23:58 +01:00
Valentin Lab
accb052f5c fix: repair price appearance on all articles 2025-11-03 11:23:24 +01:00
Valentin Lab
d5f095b5e5 fix: remove non-visible article from research results 2025-11-03 09:20:53 +01:00
Valentin Lab
fd628f3f95 fix: prevent error 500 on creation of new backoffice user 2025-11-03 09:09:36 +01:00
Valentin Lab
a10f0b35d9 fix: focus invalid field on error in article form 2025-10-15 14:49:41 +02:00
Valentin Lab
858421a9eb fix: prevent broken link upon thumbnail in variety list when having uploaded a PNG file 2025-10-15 14:48:51 +02:00
Valentin Lab
158bc4fd57 fix: provide correct temporary directory outside of `vendor/` 2025-10-15 14:08:16 +02:00
Valentin Lab
b7e3eefed6 new: allow to delete seuil lines in price-list's pice modal 2025-10-15 13:17:54 +02:00
Valentin Lab
67e4346c68 fix: pkg: do not create bogus `{cache,views,sessions}` directory in prod export 2025-10-15 12:46:01 +02:00
Valentin Lab
9ce62e82e5 fix: allow saving list-price's price seuil if seuil is unset or 0 2025-10-15 12:22:36 +02:00
Valentin Lab
7e93219774 fix: allow to re-use a deleted ref in articles 2025-10-15 12:05:16 +02:00
Valentin Lab
29f46b7287 fix: enable saving in price-list's price edit modal 2025-10-15 11:57:38 +02:00
Valentin Lab
1f02c932a0 fix: make varieties creation form avoid error 500 on save 2025-10-15 11:57:38 +02:00
Valentin Lab
7d8bd8c372 fix: make form submit apply modification on existing article 2025-10-15 11:57:38 +02:00
Valentin Lab
f4bd4ddf24 fix: prevent err 500 upon species edit form opening 2025-10-15 11:57:28 +02:00
22 changed files with 516 additions and 105 deletions

View File

@@ -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 \
@@ -84,8 +85,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 \

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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',

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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']])) {

View File

@@ -19,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];
@@ -135,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:
}

View File

@@ -3,6 +3,7 @@
namespace App\Repositories\Shop;
use App\Models\Shop\Offer;
use App\Models\Shop\PriceList;
use App\Models\Shop\PriceListValue;
use App\Models\Shop\SaleChannel;
use App\Traits\Model\Basic;
@@ -185,20 +186,44 @@ class Offers
->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;
break;
}
}
}
@@ -209,6 +234,7 @@ class Offers
$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,
@@ -221,6 +247,8 @@ class Offers
'offer_stock_current' => $offerStock,
'offer_has_stock' => $offerHasStock,
'tariff_status_id' => $offerTariffStatus,
'tariff_id' => $offerTariffId,
'all_offers' => $allOffersForChannel,
];
})->toArray();
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -49,7 +49,7 @@ class SaleChannels
}
}
return self::getByCode('EXP');
return self::getByCode('POSTE');
}
public static function getByCode($code)

View File

@@ -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();
}

View File

@@ -1,6 +1,8 @@
<?php
return [
// Default configuration group for Gravatar
'default' => [
// --- The default avatar size
'size' => 80,
@@ -12,12 +14,22 @@ return [
// (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',
'fallback' => 'identicon',
// --- 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.
'maxRating' => 'g',
'maximumRating' => 'g',
// --- Force default image to always display
'forceDefault' => false,
// --- Optional file extension appended to URL
'forceExtension' => 'jpg',
]
];

View File

@@ -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.

View File

@@ -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');
});
}
};

View File

@@ -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>
<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>

View File

@@ -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,19 +55,28 @@
</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;
addEmptyPriceRow();
})
}
function addEmptyPriceRow() {
var index = nextPriceRowIndex();
$.get("{{ route('Admin.Shop.PriceListValues.addPrice') }}/" + index, function(data) {
$("#prices-table").append(data);
})
$("#prices-table tbody").append(data);
handlePrices();
})
handleRemovePrice();
lastRemovedIndex = null;
});
}
function handle_prices() {
@@ -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>

View File

@@ -1,37 +1,59 @@
@if ($article['offers']['semences'] ?? false)
@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 ($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
@endif
@if ($article['offers']['plants'] ?? false)
@if ($article['offers']['plants'] ?? false)
@include('Shop.Articles.partials.addBasket', [
'data' => $article['offers']['plants'],
'title' => 'Plants',
'model' => 'plants',
'bgClass' => 'bg-green-light',
])
@endif
@endif
@if ($article['offers']['legumes'] ?? false)
@if ($article['offers']['legumes'] ?? false)
@include('Shop.Articles.partials.addBasket', [
'data' => $article['offers']['legumes'],
'title' => 'Légumes',
'model' => 'legumes',
'bgClass' => 'bg-green-light',
])
@endif
@endif
@if ($article['offers']['marchandise'] ?? false)
@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')

View File

@@ -48,8 +48,8 @@
</div>
<div class="col-lg-3 col-xs-12">
@if (config('app.debug') && !empty($article['available_sale_channels']))
<div class="alert alert-info p-2 mb-3">
@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>
<ul class="list-unstyled mb-0 small">
@php
@@ -82,16 +82,61 @@
</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" 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" 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>
@@ -103,3 +148,67 @@
@endsection
@include('load.layout.modal')
@if (auth('web')->check() && !empty($article['available_sale_channels']))
@push('styles')
<style>
#article-admin-offers .admin-link-group {
transition: background-color 0.15s ease;
border-radius: 3px;
}
#article-admin-offers .admin-price-link {
display: inline-block;
padding: 2px 4px;
margin: -2px -4px;
}
#article-admin-offers .admin-offer-link {
display: block;
padding: 2px 4px;
margin: -2px -4px;
}
#article-admin-offers .admin-link-group:hover,
#article-admin-offers .admin-link-group:focus,
#article-admin-offers .admin-link-group.linked-hover {
background-color: rgba(0, 123, 255, 0.1);
text-decoration: none;
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('article-admin-offers');
if (!container) {
return;
}
const links = Array.from(container.querySelectorAll('a.admin-link-group[href]'));
const grouped = new Map();
links.forEach((link) => {
const href = link.getAttribute('href');
if (!grouped.has(href)) {
grouped.set(href, []);
}
grouped.get(href).push(link);
});
grouped.forEach((group) => {
group.forEach((link) => {
const addHighlight = () => group.forEach((item) => item.classList.add('linked-hover'));
const removeHighlight = () => group.forEach((item) => item.classList.remove('linked-hover'));
link.addEventListener('mouseenter', addHighlight);
link.addEventListener('mouseleave', removeHighlight);
link.addEventListener('focus', addHighlight);
link.addEventListener('blur', removeHighlight);
});
});
});
</script>
@endpush
@endif

View File

@@ -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();

View File

@@ -75,12 +75,12 @@
if (typeof(tinyMCE) != 'undefined') {
tinyMCE.triggerSave();
}
$('form ' + form_id).submit();
getModalForm(form_id).trigger('submit');
status = 1;
}
function handlePostModal(form_id, url_save, callback) {
$('form ' + form_id).submit(function(e) {
getModalForm(form_id).off('submit').on('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
$.ajax({
@@ -98,6 +98,15 @@
});
});
}
function getModalForm(form_id) {
var $form = $(form_id);
if (! $form.length) {
$form = $('form' + form_id);
}
return $form;
}
</script>
@endpush