@extends('layouts.dashboard') @section('content') @php /** * Vendor advertising — this restaurant's campaigns, and how they are actually * performing. Impressions and clicks are read from the ad event log, so the * click-through rate is measured rather than estimated (CampaignController::index()). * * @var array $campaigns the current pager window of campaign rows * @var array $pager paginate() metadata * @var array $placements AdCampaign::placements * @var array $prices [placement => monthly price] * @var array $rangeLinks ['options' => [days => label], 'selected' => string] * @var int $rangeDays the selected window, in days * @var bool $showMoney false for a manager: ad spend stays with the owner * @var int $impressions @var int $clicks * @var float|null $ctr click-through rate, or null when nothing has been served * @var float $spend lifetime paid ad spend * @var int $liveCount campaigns currently running * @var array $perCampaign campaign_id => ['impressions' => int, 'clicks' => int] * @var array $deliveryChart @var array $deliveryMeta impressions + clicks per day * @var array $placementChart @var array $placementMeta impressions by placement * @var array $occupancyChart @var array $occupancyMeta booked periods per placement * @var bool $canPayByGateway this install can take a placement's money at all */ use App\Models\AdCampaign; /** Status pill colour map. */ $statusPill = [ 'pending' => 'qm-pill-amber', 'approved' => 'qm-pill-blue', 'active' => 'qm-pill-green', 'paused' => 'qm-pill-grey', 'rejected' => 'qm-pill-red', 'expired' => 'qm-pill-grey', ]; $showMoney = $showMoney ?? true; $perCampaign = $perCampaign ?? []; $canPayByGateway = (bool) ($canPayByGateway ?? false); @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp
@php // Page primary action → top-bar subheader slot (leaves the table card header for the // title + any table-scoped utility). ob_start captures the button HTML for the layout slot. ob_start(); @endphp {{ t_raw('vendor.campaigns.add_btn') }} @php \App\Support\View::slot('subhead', ob_get_clean()); @endphp
@partial('kpi-card', ['variant' => 'panel', 'accent' => 'blue', 'icon' => 'fa-eye', 'label' => t_raw('vendor.campaigns.kpi_impressions'), 'value' => number_format((int) $impressions), 'spark' => $kpiSparks['impressions'] ?? [], 'meta' => '' . t('vendor.campaigns.kpi_impressions_meta', [':days' => (int) $rangeDays]) . '']) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'violet', 'icon' => 'fa-mouse-pointer', 'label' => t_raw('vendor.campaigns.kpi_clicks'), 'value' => number_format((int) $clicks), 'spark' => $kpiSparks['clicks'] ?? [], 'meta' => '' . t('vendor.campaigns.kpi_clicks_meta') . '']) @php /* Click-through rate needs impressions to divide by. Until an ad has actually been served there is no rate — reporting 0% would claim nobody clicked when in fact nothing was shown, so the card says the measurement is unavailable instead. */ @endphp @partial('kpi-card', ['variant' => 'panel', 'accent' => 'green', 'icon' => 'fa-percentage', 'label' => t_raw('vendor.campaigns.kpi_ctr'), 'value' => $ctr !== null ? number_format($ctr, 2) . '%' : '—', 'meta' => '' . ($ctr !== null ? t('vendor.campaigns.kpi_ctr_meta', [':clicks' => number_format((int) $clicks), ':impressions' => number_format((int) $impressions)]) : t('vendor.campaigns.kpi_ctr_meta_none')) . '']) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'teal', 'icon' => 'fa-bullhorn', 'label' => t_raw('vendor.campaigns.kpi_live'), 'value' => number_format((int) $liveCount), 'meta' => '' . t('vendor.campaigns.kpi_live_meta') . '']) @if ($showMoney) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'amber', 'icon' => 'fa-wallet', 'label' => t_raw('vendor.campaigns.kpi_spend'), 'value' => money($spend), 'meta' => '' . t('vendor.campaigns.kpi_spend_meta') . '']) @endif
@php /* Trends beside the breakdown: how the ads were delivered day by day, and which slot carried them. Each card's caption and its accessible summary are the same sentence. */ @endphp
@partial('chart-card', ['heading' => t_raw('vendor.campaigns.chart_delivery'), 'sub' => $deliveryMeta['label'], 'cfg' => $deliveryChart, 'meta' => $deliveryMeta, 'accent' => 'blue', 'icon' => 'fa-chart-line'])
@partial('chart-card', ['heading' => t_raw('vendor.campaigns.chart_by_placement'), 'sub' => $placementMeta['label'], 'cfg' => $placementChart, 'meta' => $placementMeta, 'accent' => 'violet', 'icon' => 'fa-eye'])
@php /* A schedule question — what is booked, and where the gaps are — which no cumulative chart type can answer, so it uses the timeline (range bar) shape. */ @endphp
@partial('chart-card', ['heading' => t_raw('vendor.campaigns.chart_occupancy'), 'sub' => $occupancyMeta['label'], 'cfg' => $occupancyChart, 'meta' => $occupancyMeta, 'accent' => 'teal', 'icon' => 'fa-calendar-alt'])
@php /* WHEN THE SLOTS ARE BOOKED, and WHAT IS RUNNING IN THEM — side by side, because they are two halves of the same question. The occupancy timeline had all twelve columns to itself and the answer to "is my money working" was nowhere on the page. */ @endphp
@partial('active-placements', ['active' => $active ?? []])

{{ t_raw('vendor.campaigns.heading') }}

{{ t_raw('vendor.campaigns.subtitle') }}

@foreach ($rangeLinks['options'] as $val => $text) @php $on = ((string) $val === $rangeLinks['selected']); @endphp {{ $text }} @endforeach
@if (count($campaigns))
@foreach ($campaigns as $c) @php $status = (string) $c['status']; $pay = (string) $c['payment_status']; $target = $c['target_type'] === 'none' || empty($c['target_slug']) ? '' : status_label('ad_target_type', (string) $c['target_type']) . ': ' . e($c['target_slug']); $months = (int) $c['months']; // Delivery for this campaign in the selected window. A campaign that has // served nothing yet shows an em-dash rather than a zero, which would read // as "shown but ignored". $ev = $perCampaign[(int) $c['id']] ?? null; $rowCtr = ($ev && $ev['impressions'] > 0) ? $ev['clicks'] / $ev['impressions'] * 100 : null; @endphp @php /* Reach used to sit here. It belongs to a placement that is RUNNING, and a running placement is now on the Active placements card beside the occupancy chart — where its impressions, clicks and rate are shown together. This list is the work queue: nothing in it has been served yet, so every row read a dash. */ @endphp @endforeach
{{ t_raw('vendor.campaigns.col_headline') }}{{ t_raw('common.period') }}{{ t_raw('common.price') }}{{ t_raw('common.status') }}{{ t_raw('admin.campaigns.col_payment') }}{{ t_raw('common.actions') }}
{!! media_thumb($c['image'] ?? '', 'assets/client/covers/shop-1.png', 44, 'qm-tbl-thumb') !!}
{{ (string) $c['headline'] }} {{ AdCampaign::placementLabel((string) $c['placement']) }} @if ($target !== ''){{ $target }}@endif
@if (!empty($c['starts_at'])) {!! fmt_day($c['starts_at']) !!} – {!! fmt_day($c['ends_at'] ?? null) !!} @else {{ t_raw('admin.campaigns.months_suffix', [':n' => $months]) }} @endif {{ money($c['price']) }} {{ status_label('campaign', $status) }} @if (!empty($c['admin_note']))
{{ $c['admin_note'] }}
@endif
@php /* ONCE IT IS PAID, THIS IS NOT THE PLACE. A settled placement's billing lives in the payment record under My Account, with its invoice; saying "paid" again on the operational list only repeats it. While it is UNPAID this cell still matters — it is where the owner learns the placement is waiting for them, and where they pay. */ @endphp @if ($pay !== 'paid') @php /* The state and the thing you do about it, on one line. They were stacked only because the column was narrow; dropping reach gave it the width, and "Unpaid" reads as the label on its own button. */ @endphp {{ status_label('payment', $pay) }} @else {!! dash() !!} @endif @php /* Unpaid is the moment the placement is bought — it is created that way and nothing else has to happen first. The resume button lives here rather than behind another screen, because this cell is where the owner learns it is waiting for them. 'approved' is still accepted for rows created while payment came second. ONE button, not one per gateway. This cell used to offer "Pay with PayPal" beside "Pay by card", which asked the owner to commit to a provider from a table row — before the checkout page had shown them the amount or the tax. Which gateway is now asked at checkout, next to the figure it applies to, so this only has to say what is owed. */ @endphp @if (in_array($status, ['pending', 'approved'], true) && $pay !== 'paid' && $canPayByGateway)
@csrf
@endif
@php /* The invoice for a placement, exactly as Billing offers one for a plan — same icon, same wording, same row-actions partial. It appears only once a number has been issued, which happens when the money is captured; a placement that was never paid for has nothing to invoice. */ @endphp @partial('row-actions', ['actions' => [ [ 'label' => t_raw('common.view'), 'icon' => 'fa-eye', 'href' => '/vendor/campaigns/' . (int) $c['id'], 'primary' => true, ], (int) ($c['invoice_no'] ?? 0) > 0 ? [ 'label' => t_raw('vendor.billing.view_invoice'), 'icon' => 'fa-file-invoice', 'href' => '/vendor/campaigns/invoice/' . (int) $c['id'], 'primary' => true, ] : null, $status === 'pending' ? [ 'label' => t_raw('common.cancel'), 'icon' => 'fa-ban', 'danger' => true, 'form' => [ 'action' => '/vendor/campaigns/cancel', 'fields' => ['id' => (int) $c['id']], 'confirm' => t_raw('vendor.campaigns.confirm_cancel'), ], ] : null, ]])
@partial('table-toolbar', ['pager' => $pager, 'base' => '/vendor/campaigns']) @else
@if (!empty($active)) @php /* Campaigns EXIST — they are all live, shown with figures on the Active placements card. Saying "no campaigns yet" here contradicted it. */ @endphp

{{ t_raw('vendor.campaigns.queue_clear_title') }}

{{ t_raw('vendor.campaigns.queue_clear_text') }}

{{ t_raw('vendor.campaigns.empty_cta') }} @else

{{ t_raw('vendor.campaigns.empty_title') }}

{{ t_raw('vendor.campaigns.empty_text') }}

{{ t_raw('vendor.campaigns.empty_cta') }} @endif
@endif
@php /* WHERE THE RECEIPT WENT. A placement is paid for like a plan term is, and both are billed by this platform from one invoice sequence, so the record of what has been paid lives in one place — beside the payment methods, under My Account. This page keeps the work: what is drafted, waiting for approval, or still to be paid. Once a campaign goes live it leaves the table above for the running list, and its invoice is in the book -- the only place an owner has to look for it. The same card, worded for placements, sits at the foot of the plan page. */ @endphp
@php /* THE HEAD EVERY OTHER SECTION USES. .qm-ops-sub is styled as the sub-line OF an .qm-ops-head — dropped into .qm-card-head it inherited body type instead, so this caption rendered at 16px in full ink against the 13px muted one on "Your Campaigns" two cards above it. Same wrapper, same result, nothing new. */ @endphp

{{ t_raw('vendor.billing.history_title') }}

{{ t_raw('vendor.campaigns.history_moved') }}

{{ t_raw('vendor.billing.history_open') }}
@php /* /.qm-d2 */ @endphp @endsection