@extends('layouts.app') @section('title', __('lang_v1.bounce_rate')) @section('content')

@lang('lang_v1.bounce_rate') @lang('lang_v1.bounce_rate_desc')

@include('layouts.partials.error') {{-- Scope. Every figure and chart below obeys these, so the board reads for the whole chain, one state, one store, or one kind of failure. --}}
{!! Form::open(['route' => 'bounce-rate.index', 'method' => 'get', 'class' => 'form-inline']) !!} {{-- Native date inputs rather than the bootstrap-datepicker used elsewhere: that one is configured globally to the business's DISPLAY format, and these values are parsed server-side, so a dd/mm/yyyy would be read as an American date or not at all. type=date always submits ISO. --}}
{!! Form::label('start_date', __('report.start_date') . ':') !!} {!! Form::date('start_date', $filters['start_date'] ?? null, ['class' => 'form-control', 'id' => 'start_date', 'style' => 'width:160px;']) !!}
{!! Form::label('end_date', __('report.end_date') . ':') !!} {!! Form::date('end_date', $filters['end_date'] ?? null, ['class' => 'form-control', 'id' => 'end_date', 'style' => 'width:160px;']) !!}
{!! Form::label('state', __('business.state') . ':') !!} {!! Form::select('state', ['' => __('lang_v1.all_states')] + $states->mapWithKeys(fn($s) => [$s => $s])->all(), $filters['state'] ?? null, ['class' => 'form-control']) !!}
{!! Form::label('business_id', __('lang_v1.store') . ':') !!} {!! Form::select('business_id', ['' => __('lang_v1.all_stores')] + $businesses->all(), $filters['business_id'] ?? null, ['class' => 'form-control select2', 'style' => 'width:220px;']) !!}
{!! Form::label('bounce_type', __('lang_v1.bounce_reason') . ':') !!} {!! Form::select('bounce_type', [ '' => __('lang_v1.all'), 'out_of_stock' => __('lang_v1.bounce_out_of_stock'), 'not_listed' => __('lang_v1.bounce_not_listed'), ], $filters['bounce_type'] ?? null, ['class' => 'form-control']) !!}
@lang('lang_v1.reset') {!! Form::close() !!}
{{-- The rate, and the three numbers needed to argue with it. Kept separate on purpose: the rate is the KPI, the count is the volume, the value is the business case, and the store count is the blast radius. --}}
@php // Banded rather than a single colour: a bounce rate is only // meaningful against a threshold, and a flat red on 0.4% would // train people to ignore it. $rate_class = $rate['rate'] >= 10 ? 'bg-maroon' : ($rate['rate'] >= 5 ? 'bg-orange' : 'bg-green'); @endphp
@lang('lang_v1.bounce_rate') {{ $rate['rate'] }}% @lang('lang_v1.bounce_rate_of_requests', ['demand' => number_format($rate['demand'])])
@lang('lang_v1.total_bounces') {{ number_format($totals['bounces']) }} {{ number_format($totals['out_of_stock']) }} @lang('lang_v1.bounce_out_of_stock') · {{ number_format($totals['not_listed']) }} @lang('lang_v1.bounce_not_listed')
@lang('lang_v1.estimated_lost_sales') @format_currency($totals['lost_value']) @lang('lang_v1.lost_sales_help_short')
@lang('lang_v1.stores_affected') {{ $totals['stores'] }} {{ number_format($totals['products']) }} @lang('lang_v1.distinct_products')
{{-- What to do about it, in words. Generated only where the data supports it. --}} @if(!empty($insights)) @component('components.widget', ['class' => 'box-solid', 'title' => __('lang_v1.what_this_means')]) @foreach($insights as $i) @php $cls = ['high' => 'alert-danger', 'medium' => 'alert-warning', 'info' => 'alert-info'][$i['severity']] ?? 'alert-info'; $icon = ['high' => 'fa-exclamation-circle', 'medium' => 'fa-exclamation-triangle', 'info' => 'fa-info-circle'][$i['severity']] ?? 'fa-info-circle'; @endphp
{{ $i['text'] }}
@endforeach @endcomponent @endif @php // One empty-state block, reused wherever a chart has nothing to draw. $empty = '

'.__('lang_v1.chart_no_data').'

'; @endphp
@if($chart_has_data['trend']){!! $charts['trend']->container() !!}@else{!! $empty !!}@endif @unless($trend['sufficient']) {{-- Never let three points imply a direction of travel. --}}

@lang('lang_v1.bounce_insufficient_trend', ['days' => $trend['days_of_data']])

@endunless
@if($chart_has_data['split']){!! $charts['split']->container() !!}@else{!! $empty !!}@endif

@lang('lang_v1.bounce_split_note')

@if($chart_has_data['hour']){!! $charts['hour']->container() !!}@else{!! $empty !!}@endif

@lang('lang_v1.bounce_hour_note')

@if($chart_has_data['weekday']){!! $charts['weekday']->container() !!}@else{!! $empty !!}@endif
@if($chart_has_data['product']){!! $charts['product']->container() !!}@else{!! $empty !!}@endif
@if($chart_has_data['state']){!! $charts['state']->container() !!}@else{!! $empty !!}@endif
{{-- PREDICTIVE. The only part of the board that tells the buyer what to do BEFORE the sale is lost, rather than counting the ones already lost. --}} @component('components.widget', ['class' => 'box-danger', 'title' => __('lang_v1.bounce_forecast')])

@lang('lang_v1.bounce_forecast_help')

@if($forecast->isEmpty())

@lang('lang_v1.bounce_forecast_no_data')

@else
@foreach($forecast as $f) @endforeach
@lang('sale.product') @lang('lang_v1.weeks_observed') @lang('lang_v1.avg_per_week') @lang('lang_v1.direction') @lang('lang_v1.projected_bounces_30d') @lang('lang_v1.projected_lost_value') @lang('lang_v1.suggested_qty') @lang('lang_v1.confidence')
{{ $f->product_name }} {{ $f->observed_weeks }} {{ $f->avg_per_week }} @if($f->direction === 'rising') @lang('lang_v1.rising') @elseif($f->direction === 'falling') @lang('lang_v1.falling') @else @lang('lang_v1.flat') @endif {{ number_format($f->projected_bounces) }} @format_currency($f->projected_lost_value) {{ number_format($f->suggested_qty) }} {{-- Confidence shown as a bar, not just a number: it is the difference between an order and a guess. --}} @php $conf_cls = $f->confidence >= 60 ? 'progress-bar-green' : ($f->confidence >= 30 ? 'progress-bar-yellow' : 'progress-bar-red'); @endphp
{{ $f->confidence }}%
@endif @endcomponent {{-- STORE LEAGUE TABLE. Volume alone just ranks the busy shops; the rate is what makes two stores of different sizes comparable, so both are shown. --}} @component('components.widget', ['class' => 'box-primary', 'title' => __('lang_v1.bounce_by_store')])
@foreach($by_business as $b) @endforeach
@lang('lang_v1.store') @lang('business.state') @lang('lang_v1.bounces') @lang('lang_v1.served_requests') @lang('lang_v1.bounce_rate') @lang('lang_v1.estimated_lost_sales') @lang('lang_v1.distinct_products') @lang('messages.action')
{{ $b->business_name }} {{ $b->state }} {{ number_format($b->bounces) }} {{ number_format($b->served) }} {{ $b->rate }}% @format_currency($b->lost_value) {{ $b->products }} @lang('lang_v1.view_log')
@endcomponent {{-- STATE ROLL-UP. The geographic filter as a table, so a regional manager can read their own line without re-filtering. --}} @component('components.widget', ['class' => 'box-solid', 'title' => __('lang_v1.bounce_by_state')])
@foreach($by_state as $s) @endforeach
@lang('business.state') @lang('lang_v1.stores') @lang('lang_v1.bounces') @lang('lang_v1.distinct_products') @lang('lang_v1.estimated_lost_sales') @lang('messages.action')
{{ $s->state }} {{ $s->stores }} {{ number_format($s->bounces) }} {{ $s->products }} @format_currency($s->lost_value) @lang('lang_v1.drill_in')
@endcomponent {{-- THE BUYING LIST. Ranked by how often the chain had to say no. --}} @component('components.widget', ['class' => 'box-solid', 'title' => __('lang_v1.most_bounced_products')])
@foreach($by_product as $p) @endforeach
@lang('sale.product') @lang('product.sku') @lang('lang_v1.bounces') @lang('lang_v1.stores_affected') @lang('lang_v1.estimated_lost_sales') @lang('lang_v1.last_bounced')
{{ $p->product_name }} {{ $p->sku }} {{ number_format($p->bounces) }} {{ $p->stores }} @format_currency($p->lost_value) {{ $p->last_bounced_at ? \Carbon::parse($p->last_bounced_at)->format('d M Y H:i') : '' }}
@endcomponent {{-- RANGE EXTENSION. What customers ask for that the chain does not sell at all — a different decision to a reorder, so it gets its own table. --}} @if($unlisted->isNotEmpty()) @component('components.widget', ['class' => 'box-warning', 'title' => __('lang_v1.unlisted_demand')])

@lang('lang_v1.unlisted_demand_help')

@foreach($unlisted as $u) @endforeach
@lang('lang_v1.searched_for') @lang('lang_v1.times_asked') @lang('lang_v1.stores_affected') @lang('lang_v1.last_bounced')
{{ $u->term }} {{ number_format($u->bounces) }} {{ $u->stores }} {{ $u->last_bounced_at ? \Carbon::parse($u->last_bounced_at)->format('d M Y H:i') : '' }}
@endcomponent @endif @lang('lang_v1.view_full_bounce_log')
@endsection @section('javascript') @if($chart_has_data['trend']){!! $charts['trend']->script() !!}@endif @if($chart_has_data['split']){!! $charts['split']->script() !!}@endif @if($chart_has_data['hour']){!! $charts['hour']->script() !!}@endif @if($chart_has_data['weekday']){!! $charts['weekday']->script() !!}@endif @if($chart_has_data['product']){!! $charts['product']->script() !!}@endif @if($chart_has_data['state']){!! $charts['state']->script() !!}@endif @endsection