@extends('layouts.app') @section('content')
@php $formatIsk = fn($amount) => number_format((float) $amount, 0, ',', '.') . ' ISK'; $latestMonthTotal = $billing['month_groups'][0]['month_total'] ?? 0; $averageMonthTotal = count($billing['month_groups']) > 0 ? (int) round($billing['grand_total'] / count($billing['month_groups'])) : 0; $activeFilterCount = 0; $monthsTracked = $stats->count(); $latestUniqueLocks = (int) ($stats->last()['count'] ?? 0); $totalNewLocksInRange = (int) $stats->sum('new_locks'); $totalNewPropertiesInRange = (int) $stats->sum('new_properties'); $netGrowthRate = $latestUniqueLocks > 0 && $totalNewLocksInRange > 0 ? round(($totalNewLocksInRange / max($latestUniqueLocks, 1)) * 100, 1) : 0; $billableMonths = count($billing['month_groups']); $propertyGrowthRows = $stats ->flatMap(fn($row) => collect($row['properties'] ?? [])->map(fn($property) => [ 'month' => $row['month'], 'name' => $property['name'], 'locks' => $property['locks'], ])) ->values(); $propertyCoverageRate = $monthsTracked > 0 ? round(($propertyGrowthRows->count() / max($monthsTracked, 1)), 1) : 0; if (!empty($filters['from_month'])) $activeFilterCount++; if (!empty($filters['to_month'])) $activeFilterCount++; if (!empty($filters['property_id'])) $activeFilterCount++; if (!empty($filters['property_name'])) $activeFilterCount++; if (!empty($filters['only_billable'])) $activeFilterCount++; if (count($selectedApis) !== count($apis)) $activeFilterCount++; $totalPendingAcrossMonths = collect($billing['month_groups'])->reduce(function ($carry, $mg) use ($invoiceMap, $propertyEmailMap) { return $carry + collect($mg['api_groups'])->flatMap(fn($ag) => collect($ag['rows']))->filter(function ($r) use ($invoiceMap, $propertyEmailMap) { $hasCustomer = !empty($propertyEmailMap[(string) $r['property_id']]?->dkplus_customer_id); $hasInvoice = !empty($invoiceMap[(string) $r['property_id'] . '|' . $r['month']]?->dkplus_invoice_id); return $hasCustomer && !$hasInvoice; })->count(); }, 0); $unlinkedProperties = collect($billing['rows'] ?? [])->unique('property_id')->filter(function ($row) use ($propertyEmailMap) { return empty($propertyEmailMap[(string) $row['property_id']]?->dkplus_customer_id); })->values(); @endphp {{-- Flash messages rendered as toasts (see container at bottom of page) --}} {{-- ══════════════════ HERO HEADER ═══════════════════════════════════════ --}}
Analytics Superadmin

Active Locks by Month

Track lock growth, property onboarding, and monthly billing performance with API-level visibility.

{{-- ══════════════════ COLLAPSIBLE FILTER ════════════════════════════════ --}}
@if($activeFilterCount > 0) {{ $activeFilterCount }} active @if(!empty($filters['from_month'])) From: {{ $filters['from_month'] }} @endif @if(!empty($filters['to_month'])) To: {{ $filters['to_month'] }} @endif @if(!empty($filters['property_id'])) ID: {{ $filters['property_id'] }} @endif @if(!empty($filters['property_name'])) Name: {{ $filters['property_name'] }} @endif @if(!empty($filters['only_billable'])) Billable > 0 @endif @if(count($selectedApis) !== count($apis)) APIs: {{ implode(', ', $selectedApis) }} @endif @else No active filters — showing all data @endif
clearReset
{{-- Month inputs always use is-filled: the browser renders a date widget regardless of value --}}
{{-- Selects always use is-filled: they always show a selected option --}}
/
@foreach($apis as $api)
@endforeach
Export CSV Export Excel
{{-- ══════════════════ MAIN TABS ══════════════════════════════════════════ --}}
{{-- ─────────────────────── BILLING TAB ──────────────────────────────── --}}
{{-- Unlinked properties callout --}} @if($unlinkedProperties->count() > 0) @endif {{-- Monthly Billing card --}}
@php $monthOptions = collect($billing['month_groups'])->map(fn($mg) => [ 'value' => 'billing_month_' . str_replace('-', '_', $mg['month']), 'label' => $mg['month'], ]); @endphp
Monthly Billing

Expand a month to review charges. Bulk-create or sync invoices per month.

@if($monthOptions->count() > 1) @endif @if($totalPendingAcrossMonths > 0) {{ $totalPendingAcrossMonths }} pending @endif
@forelse($billing['month_groups'] as $monthGroup) @php $collapseId = 'billing_month_' . str_replace('-', '_', $monthGroup['month']); $headingId = $collapseId . '_heading'; $allMonthRows = collect($monthGroup['api_groups'])->flatMap(fn($ag) => collect($ag['rows'])); $totalRows = $allMonthRows->count(); $invoicedRows = $allMonthRows->filter(function ($r) use ($invoiceMap) { return !empty($invoiceMap[(string) $r['property_id'] . '|' . $r['month']]?->dkplus_invoice_id); })->count(); $pendingInvoiceCount = $allMonthRows->filter(function ($r) use ($invoiceMap, $propertyEmailMap) { $hasCustomer = !empty($propertyEmailMap[(string) $r['property_id']]?->dkplus_customer_id); $hasInvoice = !empty($invoiceMap[(string) $r['property_id'] . '|' . $r['month']]?->dkplus_invoice_id); return $hasCustomer && !$hasInvoice; })->count(); $progressClass = ($totalRows > 0 && $invoicedRows === $totalRows) ? 'bg-gradient-success' : ($invoicedRows > 0 ? 'bg-gradient-warning' : 'bg-gradient-secondary'); @endphp
{{ $formatIsk($monthGroup['month_total']) }} @if($totalRows > 0) {{ $invoicedRows }}/{{ $totalRows }} invoiced @endif
@if($pendingInvoiceCount > 0)
@csrf
@endif @if($invoicedRows > 0)
@csrf
@endif
@foreach($monthGroup['api_groups'] as $apiGroup) @foreach($apiGroup['rows'] as $row) @php $propertyEmail = $propertyEmailMap[(string) $row['property_id']] ?? null; $invoiceRecord = $invoiceMap[(string) $row['property_id'] . '|' . $row['month']] ?? null; $invoiceStatus = strtolower((string) ($invoiceRecord?->status ?? '')); $statusBadge = match($invoiceStatus) { 'finalized', 'sent' => 'bg-gradient-success', 'ready' => 'bg-gradient-info', 'submitted' => 'bg-gradient-warning', default => 'bg-gradient-secondary', }; @endphp {{-- Property + API --}} {{-- dkPlus customer --}} {{-- Locks --}} {{-- Pricing --}} {{-- Subtotal --}} {{-- Invoice actions --}} @endforeach @endforeach
Property dkPlus Customer Locks Pricing Subtotal Invoice help_outline
{{ $row['property_name'] }}
{{ $row['api'] }}
@if($propertyEmail?->dkplus_customer_id)
{{ $propertyEmail->dkplus_customer_name ?: '—' }}
{{ $propertyEmail->dkplus_customer_id }}
Edit
@csrf
@else No customer linked
@csrf
@endif
{{ $row['billable_unique_lock_count'] }}
{{ $row['pricing_rule_label'] }}
{{ $row['pricing_badge'] }} {{ $formatIsk($row['unit_price']) }}/u
Formula
{{ $row['calculation_formula'] }}
Qty: {{ $row['input_quantity'] }} {{ $row['input_quantity_label'] }}
{{ $formatIsk($row['subtotal']) }}
@if($invoiceRecord?->dkplus_invoice_id) ID: {{ $invoiceRecord->dkplus_invoice_id }} @endif @if($invoiceStatus) {{ strtoupper($invoiceStatus) }} @endif @if($invoiceRecord?->synced_at)
access_time {{ $invoiceRecord->synced_at->diffForHumans() }}
@endif @if(!$invoiceRecord?->dkplus_invoice_id)
@csrf
@else {{-- Compact icon-button row: Sync | PDF | Email --}}
@csrf
picture_as_pdf
@csrf
@endif
API subtotal — {{ $apiGroup['api'] }} {{ $formatIsk($apiGroup['api_total']) }}
Month grand total {{ $formatIsk($monthGroup['month_total']) }}
@if(auth()->id() === 4) {{-- Per-month commission breakdown — owner-only (user_id 4) --}}

account_balance_walletCommission breakdown

Gross revenue
{{ $formatIsk($monthGroup['month_total']) }}
Godo (100 ISK/lock) @if($monthGroup['godo_lock_count'] > 0) — {{ $monthGroup['godo_lock_count'] }} Godo lock(s) @endif
{{ $formatIsk($monthGroup['godo_commission']) }}
Sturla (10%)
{{ $formatIsk($monthGroup['sturla_bonus']) }}
Brynjar (10%)
{{ $formatIsk($monthGroup['brynjar_bonus']) }}
=
Net to company
{{ $formatIsk($monthGroup['net_revenue']) }}
@endif
@empty

No billing data for the selected filters.

@endforelse
{{-- /tab-billing --}} {{-- ─────────────────────── BY PROPERTY TAB ──────────────────────────── --}}
@php $unbilledOnly = (bool) request()->boolean('by_property_unbilled'); $byPropertyFiltered = $unbilledOnly ? $byProperty->where('unbilled_count', '>', 0)->values() : $byProperty; $bulkCandidateCount = $byProperty->where('unbilled_count', '>', 0)->count(); $bulkCandidateAmount = (int) $byProperty->where('unbilled_count', '>', 0)->sum('unbilled_total'); @endphp {{-- Sticky toolbar — search, sort, filter, bulk --}}
{{-- Live count + bulk-candidate summary --}}
{{ $byPropertyFiltered->count() }} {{ $byPropertyFiltered->count() === 1 ? 'property' : 'properties' }} @if($bulkCandidateCount > 0) · {{ $bulkCandidateCount }} with unbilled ({{ $formatIsk($bulkCandidateAmount) }}) @endif
{{-- Search --}}
{{-- Sort --}}
{{-- Unbilled-only toggle --}}
@foreach(request()->except('by_property_unbilled', 'page') as $k => $v) @if(is_array($v)) @foreach($v as $vv)@endforeach @else @endif @endforeach
{{-- Bulk close --}} @if($bulkCandidateCount > 0)
@csrf
@endif
{{-- Property cards --}} @forelse($byPropertyFiltered as $prop) @php $propertyEmail = $propertyEmailMap[$prop['property_id']] ?? null; $hasCustomer = !empty($propertyEmail?->dkplus_customer_id); $unbilledList = collect($prop['months'])->filter(fn($m) => empty($m['is_invoiced']) && empty($m['is_skipped']))->values(); @endphp
{{ $prop['unbilled_count'] > 0 ? 'expand_more' : 'chevron_right' }}
{{ $prop['property_name'] }} {{ strtoupper($prop['api'] ?: 'unknown') }} @if(!$hasCustomer) link_off No customer @endif {{-- Selection-summary chip — visible when ≥1 month is checked --}} 0 selected · 0 ISK
ID {{ $prop['property_id'] }} · @if($prop['latest_invoice_id']) last invoice {{ $prop['latest_invoice_id'] }} ({{ $prop['latest_invoice_month'] }}) @else no invoices yet @endif
{{-- Mini month strip — one chip per month, status-coloured. Visible even when card is collapsed. --}}
@foreach($prop['months'] as $m) @php $st = !empty($m['is_invoiced']) ? 'invoiced' : (!empty($m['is_skipped']) ? 'skipped' : 'pending'); @endphp @endforeach
Unbilled · {{ $prop['unbilled_count'] }} {{ $prop['unbilled_count'] === 1 ? 'month' : 'months' }}
{{ $formatIsk($prop['unbilled_total']) }}
Lifetime · {{ $formatIsk($prop['all_time_total']) }} · {{ $prop['skipped_count'] }} skipped
{{-- /.by-property-toggle --}}
0 ? '' : 'hidden' }}>
{{-- Inline customer editor — link / edit a dkPlus customer without leaving the page. --}}
@if($hasCustomer)
Customer {{ $propertyEmail->dkplus_customer_name ?: '—' }} #{{ $propertyEmail->dkplus_customer_id }}
Edit
@csrf
@else
link_off No dkPlus customer linked — enter the customer ID and click Save.
@csrf
@endif
@csrf
@foreach($prop['months'] as $m) @php $rowStatus = !empty($m['is_invoiced']) ? 'invoiced' : (!empty($m['is_skipped']) ? 'skipped' : 'pending'); $origSubtotal = (int) ($m['original_subtotal'] ?? $m['subtotal']); $displaySubtotal = $rowStatus === 'skipped' ? 0 : $origSubtotal; @endphp @endforeach
Month Locks Subtotal Status Actions
@if($rowStatus === 'pending') @else @endif {{ $m['month'] }} {{ $m['billable_unique_lock_count'] }} {{ $formatIsk($displaySubtotal) }} @if($rowStatus === 'invoiced') Invoiced @if(!empty($m['invoice_id'])) #{{ $m['invoice_id'] }} @endif @elseif($rowStatus === 'skipped') Skipped @if(!empty($m['skip_reason'])) — {{ $m['skip_reason'] }} @endif @else Pending @endif @if($rowStatus !== 'invoiced') @endif
{{-- Action bar — always rendered so JS can show/hide as state changes inline --}}
0 ISK
0 of {{ $unbilledList->count() }} unbilled selected
check_circle All months for this property are either invoiced or marked as non-billable.
{{-- /.by-property-collapse --}}
@empty
@if($unbilledOnly) No properties with unbilled months for the current filters. @else No billing data for the current filters. @endif
@endforelse {{-- Search empty-state — rendered ONCE outside the @forelse and toggled by JS. --}}
search_off
No properties match your search.
{{-- /tab-by-property --}} {{-- ─────────────────────── CUSTOMERS TAB ────────────────────────────── --}} {{-- Quick-fill dashboard for the dkPlus customer linkage on every property. Default sort: missing first (so the user lands on what needs work). Default filter: "Missing only" ON. --}}
@php // Sort: missing customer first, then by property name. $customersList = $properties ->map(fn($p) => [ 'property' => $p, 'email' => $propertyEmailMap[(string) $p->PropertyID] ?? null, 'has_customer' => !empty($propertyEmailMap[(string) $p->PropertyID]?->dkplus_customer_id), ]) ->sortBy(fn($r) => sprintf('%d|%s', $r['has_customer'] ? 1 : 0, mb_strtolower((string) $r['property']->PropertyName) )) ->values(); $linkedCount = $customersList->where('has_customer', true)->count(); $missingCount = $customersList->where('has_customer', false)->count(); @endphp {{-- Sticky toolbar --}}
{{ $missingCount }} visible · @if($missingCount > 0) {{ $missingCount }} missing @endif · {{ $linkedCount }} linked
{{-- Customers table --}}
@foreach($customersList as $entry) @php $property = $entry['property']; $email = $entry['email']; $hasCustomer = $entry['has_customer']; @endphp @endforeach
Property API Status dkPlus customer
{{ $property->PropertyName }}
{{ $property->PropertyID }}
{{ strtoupper($property->API ?: 'unknown') }} @if($hasCustomer) Linked @else Missing @endif
@csrf
search_off
No properties match the current filters.
{{-- /tab-customers --}} @if(auth()->id() === 4) {{-- ─────────────────────── COMMISSIONS TAB ──────────────────────────── --}} {{-- Owner-only (Sturla, user_id 4). Other users never see this pane. --}}
{{-- Grand-total summary cards --}}

Gross Revenue

{{ $formatIsk($billing['grand_total']) }}

All subscription billing

Godo Commission

{{ $formatIsk($billing['total_godo_commission']) }}

100 ISK × {{ $billing['total_godo_lock_count'] }} Godo lock(s) × month

Sturla Bonus

{{ $formatIsk($billing['total_sturla_bonus']) }}

10% of gross revenue

Brynjar Bonus

{{ $formatIsk($billing['total_brynjar_bonus']) }}

10% of gross revenue

Net to Company

{{ $formatIsk($billing['total_net_revenue']) }}

After commissions & bonuses

{{-- Month-by-month breakdown --}}
account_balance_wallet Monthly Commission Breakdown
Godo: 100 ISK/lock  |  Sturla & Brynjar: 10% each
@foreach($billing['month_groups'] as $mg) @endforeach
Month Gross Revenue Godo Commission Sturla (10%) Brynjar (10%) Net to Company
{{ $mg['month'] }} {{ $formatIsk($mg['month_total']) }} {{ $formatIsk($mg['godo_commission']) }} @if($mg['godo_lock_count'] > 0) {{ $mg['godo_lock_count'] }} Godo lock(s) @elseif($mg['godo_commission'] === 0) no Godo properties @endif {{ $formatIsk($mg['sturla_bonus']) }} {{ $formatIsk($mg['brynjar_bonus']) }} {{ $formatIsk($mg['net_revenue']) }}
Grand Total {{ $formatIsk($billing['grand_total']) }} {{ $formatIsk($billing['total_godo_commission']) }} @if($billing['total_godo_lock_count'] > 0) {{ $billing['total_godo_lock_count'] }} Godo lock(s) @endif {{ $formatIsk($billing['total_sturla_bonus']) }} {{ $formatIsk($billing['total_brynjar_bonus']) }} {{ $formatIsk($billing['total_net_revenue']) }}
{{-- /tab-commissions --}} @endif {{-- ─────────────────────── ANALYTICS TAB ────────────────────────────── --}}
{{-- 8 KPI cards --}}

Grand total billed

{{ $formatIsk($billing['grand_total']) }}

Latest month total

{{ $formatIsk($latestMonthTotal) }}

Average monthly total

{{ $formatIsk($averageMonthTotal) }}

Active filter groups

{{ $activeFilterCount }}

Latest unique locks

{{ $latestUniqueLocks }}

New locks in range

{{ $totalNewLocksInRange }}

New properties in range

{{ $totalNewPropertiesInRange }}

Months tracked

{{ $monthsTracked }}
{{-- 3 insight cards --}}
Growth Health

How fast the selected period expanded.

{{ $netGrowthRate }}%

Net lock velocity

New locks in range vs. latest unique lock base.

Property Activity

Average additions per tracked month.

{{ $propertyCoverageRate }}

Properties / month

Monthly onboarding consistency in current filters.

Billing Signal

Snapshot of active invoice periods.

{{ $billableMonths }}

Billable months
Grand: {{ $formatIsk($billing['grand_total']) }} Avg: {{ $formatIsk($averageMonthTotal) }}

Verify billing continuity across the selected range.

{{-- Lock growth chart + table --}}
Monthly Lock Growth

Unique locks and property additions per month.

@forelse($stats as $statRow) @empty @endforelse
Month Unique Locks New Locks New Properties
{{ $statRow['month'] }} {{ $statRow['count'] }} {{ $statRow['new_locks'] }} {{ $statRow['new_properties'] }}
No rows for selected filters.
{{-- Property growth chart + table --}}
Monthly Property Growth

Property additions per month.

@forelse($propertyGrowthRows as $propertyRow) @empty @endforelse
Month Property Added Locks Added
{{ $propertyRow['month'] }} {{ strip_tags((string) $propertyRow['name']) }} {{ $propertyRow['locks'] }}
No new properties in the selected range.
{{-- /tab-analytics --}}
{{-- /tab-content --}} {{-- Flash data passed to SweetAlert (no Bootstrap toast HTML needed) --}} @if(session('status')) @elseif($errors->has('dkplus')) @endif
@endsection @push('css') @endpush @push('js') @endpush