Added charts with date range picker

This commit is contained in:
snipe
2026-05-11 11:45:34 +01:00
parent 32717c67c7
commit 37f60993ca
+375 -74
View File
@@ -2,89 +2,390 @@
{{-- Page title --}}
@section('title')
{{ trans('general.depreciation_report') }}
@parent
{{ trans('general.reports') }}
@parent
@stop
{{-- Page content --}}
@section('content')
<div class="page-header">
<div class="pull-right">
<a href="{{ route('reports/export') }}" class="btn btn-flat gray pull-right"><i class="fas fa-download icon-white" aria-hidden="true"></i>
{{ trans('admin/hardware/table.dl_csv') }}</a>
{{-- Row 1: Stat Alert Cards --}}
<div class="row">
<div class="col-lg-3 col-sm-6">
<a href="{{ route('reports.audit') }}">
<div class="small-box {{ $audit_alert_count > 0 ? 'bg-red' : 'bg-green' }}">
<div class="inner">
<h3>{{ number_format($audit_alert_count) }}</h3>
<p>{{ trans('general.audit_due') }} / {{ trans('general.audit_overdue') }}</p>
</div>
<div class="icon" aria-hidden="true"><x-icon type="audit" /></div>
<span class="small-box-footer">
{{ trans('general.viewall') }} <x-icon type="arrow-circle-right" />
</span>
</div>
</a>
</div>
<h2>{{ trans('general.depreciation_report') }}</h2>
</div>
<div class="row">
<div class="col-lg-3 col-sm-6">
<a href="{{ route('hardware.index') }}">
<div class="small-box {{ $checkin_alert_count > 0 ? 'bg-red' : 'bg-green' }}">
<div class="inner">
<h3>{{ number_format($checkin_alert_count) }}</h3>
<p>{{ trans('general.checkin_due') }} / {{ trans('general.checkin_overdue') }}</p>
</div>
<div class="icon" aria-hidden="true"><x-icon type="assets" /></div>
<span class="small-box-footer">
{{ trans('general.viewall') }} <x-icon type="arrow-circle-right" />
</span>
</div>
</a>
</div>
<table id="example">
<thead>
<tr role="row">
<th class="col-sm-1">{{ trans('admin/hardware/table.asset_tag') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.title') }}</th>
@if ($snipeSettings->display_asset_name)
<th class="col-sm-1">{{ trans('general.name') }}</th>
@endif
<th class="col-sm-1">{{ trans('admin/hardware/table.serial') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.checkoutto') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.location') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.purchase_date') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.eol') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.purchase_cost') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.book_value') }}</th>
<th class="col-sm-1">{{ trans('admin/hardware/table.diff') }}</th>
</tr>
</thead>
<tbody>
@foreach ($assets as $asset)
<tr>
<td>{{ $asset->asset_tag }}</td>
<td>{{ $asset->model->name }}</td>
@if ($snipeSettings->display_asset_name)
<td>{{ $asset->name }}</td>
@endif
<td>{{ $asset->serial }}</td>
<td>
@if ($asset->assigned_to != '')
{!! $asset->assignedTo->present->nameUrl() !!}
@endif
</td>
<td>
@if (($asset->checkedOutToUser()) && ($asset->assignedTo->assetLoc))
{{ $asset->assignedTo->assetLoc->city }}, {{ $asset->assignedTo->assetLoc->state}}
@endif
</td>
<td>{{ $asset->purchase_date }}</td>
<div class="col-lg-3 col-sm-6">
<a href="{{ route('reports/unaccepted_assets') }}">
<div class="small-box {{ $pending_acceptance_count > 0 ? 'bg-yellow' : 'bg-green' }}">
<div class="inner">
<h3>{{ number_format($pending_acceptance_count) }}</h3>
<p>{{ trans('general.unaccepted_asset_report') }}</p>
</div>
<div class="icon" aria-hidden="true"><x-icon type="assets" /></div>
<span class="small-box-footer">
{{ trans('general.viewall') }} <x-icon type="arrow-circle-right" />
</span>
</div>
</a>
</div>
<td>
@if ($asset->model->eol) {{ $asset->present()->eol_date() }}
@endif
</td>
<div class="col-lg-3 col-sm-6">
<a href="{{ url('reports/licenses') }}">
<div class="small-box {{ $licenses_low_count > 0 ? 'bg-red' : 'bg-green' }}">
<div class="inner">
<h3>{{ number_format($licenses_low_count) }}</h3>
<p>{{ trans('general.licenses_with_no_seats') }}</p>
</div>
<div class="icon" aria-hidden="true"><x-icon type="licenses" /></div>
<span class="small-box-footer">
{{ trans('general.viewall') }} <x-icon type="arrow-circle-right" />
</span>
</div>
</a>
</div>
@if ($asset->purchase_cost > 0)
<td class="align-right">
{{ $snipeSettings->default_currency }}
{{ Helper::formatCurrencyOutput($asset->purchase_cost) }}
</td>
<td class="align-right">
{{ $snipeSettings->default_currency }}
{{ number_format($asset->depreciate()) }}
</td>
<td class="align-right">
{{ $snipeSettings->default_currency }}
-{{ number_format(($asset->purchase_cost - $asset->depreciate())) }}
</td>
@else {{-- purchase_cost > 0 --}}
<td></td>
<td></td>
<td></td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
{{-- Charts: all inside one box with the date-range control in the header --}}
<div class="row">
<div class="col-md-12">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title">{{ trans('general.activity_overview') }}</h2>
<div class="box-tools pull-right" style="display:flex; align-items:center; gap:8px;">
<label style="margin:0; font-weight:normal; white-space:nowrap;">{{ trans('general.time_range') }}:</label>
<select id="chartTimeRange" class="form-control input-sm" style="width:auto;">
<option value="7">{{ trans('general.last_7_days') }}</option>
<option value="14">{{ trans('general.last_14_days') }}</option>
<option value="30" selected>{{ trans('general.last_30_days') }}</option>
<option value="60">{{ trans('general.last_60_days') }}</option>
<option value="90">{{ trans('general.last_90_days') }}</option>
<option value="180">{{ trans('general.last_180_days') }}</option>
<option value="365">{{ trans('general.last_365_days') }}</option>
<option value="custom">{{ trans('general.custom_range') }}</option>
</select>
<div id="customRangePicker" class="input-daterange input-group" style="display:none; width:auto;">
<input type="text" id="chartStartDate" class="form-control input-sm" placeholder="{{ trans('general.select_date') }}" style="width:110px;" autocomplete="off">
<span class="input-group-addon"></span>
<input type="text" id="chartEndDate" class="form-control input-sm" placeholder="{{ trans('general.select_date') }}" style="width:110px;" autocomplete="off">
</div>
<button type="button" class="btn btn-box-tool" data-widget="collapse" aria-hidden="true"><x-icon type="minus" /></button>
</div>
</div>
<div class="box-body">
{{-- Row A: Checkouts & Check-ins | New Assets --}}
<div class="row">
<div class="col-md-6">
<p class="text-muted" style="margin:0 0 4px; font-size:12px; text-transform:uppercase; letter-spacing:.05em;">{!! trans('general.checkouts_checkins') !!}</p>
<div style="position:relative; height:160px;">
<canvas id="chart-checkouts"></canvas>
</div>
</div>
<div class="col-md-6">
<p class="text-muted" style="margin:0 0 4px; font-size:12px; text-transform:uppercase; letter-spacing:.05em;">{!! trans('general.new_assets_created') !!}</p>
<div style="position:relative; height:160px;">
<canvas id="chart-assets"></canvas>
</div>
</div>
</div>
<hr style="margin: 16px 0;">
{{-- Row B: New Maintenances | New Users --}}
<div class="row">
<div class="col-md-6">
<p class="text-muted" style="margin:0 0 4px; font-size:12px; text-transform:uppercase; letter-spacing:.05em;">{!! trans('general.new_maintenances_created') !!}</p>
<div style="position:relative; height:160px;">
<canvas id="chart-maintenances"></canvas>
</div>
</div>
<div class="col-md-6">
<p class="text-muted" style="margin:0 0 4px; font-size:12px; text-transform:uppercase; letter-spacing:.05em;">{!! trans('general.new_users_created') !!}</p>
<div style="position:relative; height:160px;">
<canvas id="chart-users"></canvas>
</div>
</div>
</div>
<hr style="margin: 16px 0;">
{{-- Row C: New Accessories | New Components | New Consumables --}}
<div class="row">
<div class="col-md-4">
<p class="text-muted" style="margin:0 0 4px; font-size:12px; text-transform:uppercase; letter-spacing:.05em;">{!! trans('general.new_accessories_created') !!}</p>
<div style="position:relative; height:160px;">
<canvas id="chart-accessories"></canvas>
</div>
</div>
<div class="col-md-4">
<p class="text-muted" style="margin:0 0 4px; font-size:12px; text-transform:uppercase; letter-spacing:.05em;">{!! trans('general.new_components_created') !!}</p>
<div style="position:relative; height:160px;">
<canvas id="chart-components"></canvas>
</div>
</div>
<div class="col-md-4">
<p class="text-muted" style="margin:0 0 4px; font-size:12px; text-transform:uppercase; letter-spacing:.05em;">{!! trans('general.new_consumables_created') !!}</p>
<div style="position:relative; height:160px;">
<canvas id="chart-consumables"></canvas>
</div>
</div>
</div>
</div>{{-- /.box-body --}}
</div>{{-- /.box --}}
</div>
</div>
{{-- Row: Report Links --}}
<div class="row">
<div class="col-md-12">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title">{{ trans('general.reports') }}</h2>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-3 col-sm-6">
<a href="{{ route('reports.activity') }}" class="btn btn-default btn-block" style="margin-bottom: 10px; white-space: normal;">
<x-icon type="reports" /> {{ trans('general.activity_report') }}
</a>
</div>
<div class="col-md-3 col-sm-6">
<a href="{{ url('reports/custom') }}" class="btn btn-default btn-block" style="margin-bottom: 10px; white-space: normal;">
<x-icon type="reports" /> {{ trans('general.custom_report') }}
</a>
</div>
<div class="col-md-3 col-sm-6">
<a href="{{ route('reports.audit') }}" class="btn btn-default btn-block" style="margin-bottom: 10px; white-space: normal;">
<x-icon type="audit" /> {{ trans('general.audit_report') }}
</a>
</div>
<div class="col-md-3 col-sm-6">
<a href="{{ url('reports/depreciation') }}" class="btn btn-default btn-block" style="margin-bottom: 10px; white-space: normal;">
<x-icon type="reports" /> {{ trans('general.depreciation_report') }}
</a>
</div>
<div class="col-md-3 col-sm-6">
<a href="{{ url('reports/licenses') }}" class="btn btn-default btn-block" style="margin-bottom: 10px; white-space: normal;">
<x-icon type="licenses" /> {{ trans('general.license_report') }}
</a>
</div>
<div class="col-md-3 col-sm-6">
<a href="{{ route('ui.reports.maintenances') }}" class="btn btn-default btn-block" style="margin-bottom: 10px; white-space: normal;">
<x-icon type="maintenances" /> {{ trans('general.asset_maintenance_report') }}
</a>
</div>
<div class="col-md-3 col-sm-6">
<a href="{{ url('reports/unaccepted_assets') }}" class="btn btn-default btn-block" style="margin-bottom: 10px; white-space: normal;">
<x-icon type="assets" /> {{ trans('general.unaccepted_asset_report') }}
</a>
</div>
<div class="col-md-3 col-sm-6">
<a href="{{ url('reports/accessories') }}" class="btn btn-default btn-block" style="margin-bottom: 10px; white-space: normal;">
<x-icon type="accessories" /> {{ trans('general.accessory_report') }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
@stop
@push('js')
<script src="{{ url(mix('js/dist/Chart.min.js')) }}"></script>
<script nonce="{{ csrf_token() }}">
// Chart instances keyed by canvas ID for clean destroy/recreate
var charts = {};
var lineOptions = {
responsive: true,
maintainAspectRatio: false,
legend: { position: 'bottom', labels: { boxWidth: 12 } },
scales: {
xAxes: [{ gridLines: { display: false }, ticks: { maxTicksLimit: 10 } }],
yAxes: [{ ticks: { beginAtZero: true, precision: 0 } }]
}
};
function ds(label, data, color, isPrev) {
return {
label: label,
data: data,
borderColor: color,
backgroundColor: color,
borderWidth: isPrev ? 1.5 : 2,
borderDash: isPrev ? [5, 4] : [],
pointRadius: isPrev ? 0 : 3,
pointHoverRadius: isPrev ? 3 : 5,
fill: false,
tension: 0.3,
};
}
function makeChart(id, labels, current, previous, label, prevPeriod, color) {
if (charts[id]) { charts[id].destroy(); }
charts[id] = new Chart(document.getElementById(id), {
type: 'line',
data: {
labels: labels,
datasets: [
ds(label, current, color, false),
ds(label + ' (' + prevPeriod + ')', previous, hexToRgba(color, 0.5), true),
]
},
options: lineOptions
});
}
function makeChart2(id, labels, d1, d2, prev1, prev2, label1, label2, color1, color2, prevPeriod) {
if (charts[id]) { charts[id].destroy(); }
charts[id] = new Chart(document.getElementById(id), {
type: 'line',
data: {
labels: labels,
datasets: [
ds(label1, d1, color1, false),
ds(label2, d2, color2, false),
ds(label1 + ' (' + prevPeriod + ')', prev1, hexToRgba(color1, 0.5), true),
ds(label2 + ' (' + prevPeriod + ')', prev2, hexToRgba(color2, 0.5), true),
]
},
options: lineOptions
});
}
function hexToRgba(hex, alpha) {
var r = parseInt(hex.slice(1,3), 16);
var g = parseInt(hex.slice(3,5), 16);
var b = parseInt(hex.slice(5,7), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
}
function loadCharts(params) {
$.ajax({
type: 'GET',
url: '{{ route('api.reports.activity.chart') }}',
data: params,
headers: { "X-Requested-With": 'XMLHttpRequest', "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content') },
dataType: 'json',
success: function(d) {
var p = d.prev_label;
makeChart2('chart-checkouts',
d.labels,
d.checkouts, d.checkins,
d.prev_checkouts, d.prev_checkins,
'{!! trans('general.checkouts') !!}', '{!! trans('general.checkins') !!}',
'#3c8dbc', '#00a65a', p
);
makeChart('chart-assets',
d.labels, d.new_assets, d.prev_new_assets,
'{!! trans('general.assets') !!}', p, '#f39c12'
);
makeChart('chart-maintenances',
d.labels, d.new_maintenances, d.prev_new_maintenances,
'{!! trans('general.maintenances') !!}', p, '#dd4b39'
);
makeChart('chart-users',
d.labels, d.new_users, d.prev_new_users,
'{!! trans('general.users') !!}', p, '#605ca8'
);
makeChart('chart-accessories',
d.labels, d.new_accessories, d.prev_new_accessories,
'{!! trans('general.accessories') !!}', p, '#00c0ef'
);
makeChart('chart-components',
d.labels, d.new_components, d.prev_new_components,
'{!! trans('general.components') !!}', p, '#39cccc'
);
makeChart('chart-consumables',
d.labels, d.new_consumables, d.prev_new_consumables,
'{!! trans('general.consumables') !!}', p, '#ff851b'
);
}
});
}
// Preset dropdown
$('#chartTimeRange').on('change', function() {
if ($(this).val() === 'custom') {
$('#customRangePicker').css('display', 'flex');
} else {
$('#customRangePicker').hide();
loadCharts({ days: $(this).val() });
}
});
// Bootstrap datepicker — same options as reports/custom.blade.php
$('#customRangePicker').datepicker({
clearBtn: true,
todayHighlight: true,
endDate: '0d',
format: 'yyyy-mm-dd',
keepEmptyValues: true,
});
$('#customRangePicker').on('changeDate', function() {
var start = $('#chartStartDate').val();
var end = $('#chartEndDate').val();
if (start && end && start <= end) {
loadCharts({ start_date: start, end_date: end });
}
});
loadCharts({ days: 30 });
</script>
@endpush