media menu
This commit is contained in:
86
app/Http/Controllers/MediaController.php
Normal file
86
app/Http/Controllers/MediaController.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Validator;
|
||||
use Hidehalo\Nanoid\Client as Nanoid;
|
||||
use Hidehalo\Nanoid\GeneratorInterface as NanoidInterface;
|
||||
use App\Responses;
|
||||
use App\Helper;
|
||||
use App\Models\Vehicles;
|
||||
use App\Models\UserLogs;
|
||||
use Auth;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
/**
|
||||
* View
|
||||
*/
|
||||
|
||||
public function view_media(Request $req)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
|
||||
$log = [
|
||||
"module" => "Media",
|
||||
"action" => "View",
|
||||
"desc" => "Open Media menu",
|
||||
];
|
||||
UserLogs::insert(Auth::user()->id, $log);
|
||||
return view("menu_v1.media", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* API
|
||||
*/
|
||||
|
||||
public function api_get_media(Request $req)
|
||||
{
|
||||
$a = $req->a;
|
||||
try {
|
||||
switch ($a) {
|
||||
case 'listVehicle':
|
||||
$list = Vehicles::listVehicles($req->auth, []);
|
||||
|
||||
$apiResp = Responses::success("success list media");
|
||||
$apiResp["count"] = count($list);
|
||||
$apiResp["data"] = $list;
|
||||
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||
break;
|
||||
case 'listMedia':
|
||||
$vhcId = $req->vhcId;
|
||||
$tgl0 = $req->tgl0 * 1000 ;
|
||||
$tgl1 = $req->tgl1 * 1000 ;
|
||||
$camera = $req->camera;
|
||||
|
||||
$list = DB::select("SELECT *
|
||||
FROM t_camera
|
||||
WHERE
|
||||
vhc_id = ?
|
||||
and cam = IFNULL(?, cam)
|
||||
ORDER BY crt_d desc
|
||||
", [$vhcId, $camera]);
|
||||
|
||||
$apiResp = Responses::success("success list media");
|
||||
$apiResp["count"] = count($list);
|
||||
$apiResp["data"] = $list;
|
||||
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||
break;
|
||||
default:
|
||||
$apiResp = Responses::error("invalid action");
|
||||
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||
break;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$apiResp = Responses::error($e->getMessage());
|
||||
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
434
resources/views/menu_v1/media.blade.php
Normal file
434
resources/views/menu_v1/media.blade.php
Normal file
@ -0,0 +1,434 @@
|
||||
@extends('app.app')
|
||||
|
||||
@section('title')
|
||||
Conf Pools
|
||||
@endsection
|
||||
|
||||
@section('customcss')
|
||||
<style>
|
||||
:root {
|
||||
--badge-i-color: #0d6efd; /* Bootstrap Primary Blue */
|
||||
--badge-f-color: #fd7e14; /* Bootstrap Orange */
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.gallery-card {
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.gallery-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
|
||||
}
|
||||
|
||||
/* Image container to ensure consistent aspect ratio */
|
||||
.img-container {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #e9ecef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.img-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.gallery-card:hover .img-container img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Custom Badge Styling - Updated for longer text */
|
||||
.media-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
/* Removed fixed width/height to accommodate text */
|
||||
padding: 0.15rem 0.45rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* font-weight: bold; */
|
||||
z-index: 2;
|
||||
/* font-family: sans-serif; Changed to sans-serif for better readability */
|
||||
font-size: 0.6rem;
|
||||
text-transform: uppercase;
|
||||
/* letter-spacing: 0.5px; */
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
border-radius: 50rem; /* Pill shape */
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-i {
|
||||
background-color: var(--badge-i-color);
|
||||
}
|
||||
|
||||
.badge-f {
|
||||
background-color: var(--badge-f-color);
|
||||
}
|
||||
|
||||
/* Timestamp Styling */
|
||||
.timestamp-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Icon simulation for timestamp */
|
||||
.clock-icon {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1.5px solid #6c757d;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
}
|
||||
.clock-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 4px;
|
||||
height: 1px;
|
||||
background: #6c757d;
|
||||
transform-origin: left;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background-color: #212529;
|
||||
color: white;
|
||||
border-color: #212529;
|
||||
}
|
||||
</style>@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-2 bg-white p-0 d-flex flex-column"
|
||||
id="leftSidebar"
|
||||
style="height: calc(100vh - 56px);">
|
||||
|
||||
<!-- Search -->
|
||||
<div class="p-3 border-bottom flex-shrink-0">
|
||||
<input type="text" id="filterVhcSearch" class="form-control form-control-sm" placeholder="Search Vehicle">
|
||||
</div>
|
||||
|
||||
<!-- Scrollable list - this is the real fix -->
|
||||
<div class="overflow-auto flex-fill">
|
||||
<ul id="listVehicle" class="list-group list-group-flush m-0">
|
||||
<!-- items -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col p-2" style="height: calc(-56px + 100vh); position: relative;">
|
||||
{{-- media content --}}
|
||||
<div class="card card-body h-100">
|
||||
<h5 class="card-title">Media Content <span id="nopol"></span></h5>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="form-group">
|
||||
<label class="text-muted">From</label>
|
||||
<!-- default today -->
|
||||
<!-- <input class="form-control" id="tgl0" value="02-09-2025 00:00"> -->
|
||||
<input class="form-control" id="tgl0" value="{{ date('d-m-Y 00:00') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div class="form-group">
|
||||
<label class="text-muted">To</label>
|
||||
<!-- <input class="form-control" id="tgl1" value="02-09-2025 23:00"> -->
|
||||
<input class="form-control" id="tgl1" value="{{ date('d-m-Y 23:59') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div class="form-group">
|
||||
<label class="text-muted">Camera</label>
|
||||
<select name="license_plate" class="form-control" id="filterCamera">
|
||||
<option value="">-- All --</option>
|
||||
<option value="F">Front</option>
|
||||
<option value="I">Inside</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1 d-flex align-items-end">
|
||||
<button class="btn btn-primary" id="submitFilter">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mb-2">
|
||||
{{-- media list --}}
|
||||
<!-- Gallery Grid -->
|
||||
<div class="row g-3 overflow-auto mt-0" id="listMedia">
|
||||
<!-- <div class="col-sm-6 col-md-4 col-lg-3 gallery-item" data-type="I">
|
||||
<div class="card h-100 gallery-card shadow-sm">
|
||||
<span class="media-badge badge-i">Inside</span>
|
||||
<div class="img-container">
|
||||
<img src="https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?auto=format&fit=crop&w=500&q=60" alt="Mountain">
|
||||
</div>
|
||||
<div class="card-body bg-white border-top">
|
||||
<h6 class="card-title text-truncate">Living Room 001</h6>
|
||||
<div class="timestamp-label mt-2">
|
||||
<span class="clock-icon"></span> 2023-10-24 09:30 AM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Modal -->
|
||||
<div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content bg-transparent border-0 shadow-none">
|
||||
<div class="modal-body p-0 text-center position-relative">
|
||||
<button type="button" class="btn-close btn-close-white position-absolute top-0 end-0 m-3 z-3" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<img src="" id="modalImage" class="img-fluid rounded shadow-lg" style="max-height: 90vh;" alt="Preview">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('customjs')
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
let STATE = {
|
||||
vhcId: null,
|
||||
defaultEmptyMedia: `
|
||||
<div class="col text-center text-muted">
|
||||
No media available
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const Wrapper = {
|
||||
activate: function() {
|
||||
Wrapper.event();
|
||||
Wrapper.init();
|
||||
},
|
||||
init: function() {
|
||||
$('#listMedia').html(STATE.defaultEmptyMedia);
|
||||
|
||||
// datetimepicker
|
||||
$('#tgl0, #tgl1').datetimepicker({
|
||||
format:'d-m-Y H:i',
|
||||
defaultTime:'00:00',
|
||||
closeOnDateSelect: true,
|
||||
// mask:true
|
||||
});
|
||||
|
||||
// vehicle list
|
||||
$.ajax({
|
||||
url: "{{ route('api_get_media', '') }}/",
|
||||
data: {
|
||||
a:'listVehicle'
|
||||
},
|
||||
method: 'GET',
|
||||
crossDomain: true,
|
||||
processData: true,
|
||||
headers: {
|
||||
'x-api-key': Helper.getCookie('_trtk'),
|
||||
},
|
||||
// data: params,
|
||||
success: (data, textStatus, jqXHR) => {
|
||||
// console.log("res data", data);
|
||||
|
||||
if (data.meta.type != 'success') {
|
||||
resolve({
|
||||
type: 'fail'
|
||||
});
|
||||
Helper.toast('Warning', 'just now', data.meta.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
const listVehicle = data.data;
|
||||
listVehicle.forEach(vhc => {
|
||||
const vehicleItem = `
|
||||
<li class="list-group-item vehicles-list-wrapper">
|
||||
<a href="#" class="text-dark vhcItem" data-id="${vhc.id}" data-nopol="${vhc.nopol1}">
|
||||
<div class="row d-flex align-items-center">
|
||||
<div class="col-3">
|
||||
<img src="{{ asset('storage') }}/${vhc.fvhc_img}"
|
||||
class="img-fluid" alt="">
|
||||
</div>
|
||||
<div class="col ps-0">
|
||||
<p class="text-bold mb-0">${vhc.nopol1}</p>
|
||||
<p class="text-muted mb-0">${vhc.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
$('#listVehicle').append(vehicleItem);
|
||||
});
|
||||
},
|
||||
error: (jqXHR, textStatus, error) => {
|
||||
if (jqXHR.status >= 500) {
|
||||
Helper.toast('Error', 'just now', 'please try again');
|
||||
} else {
|
||||
Helper.toast('Error', 'just now', jqXHR.responseJSON.meta
|
||||
.message);
|
||||
}
|
||||
resolve({
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
event: function() {
|
||||
// filter vehicle list
|
||||
$('#filterVhcSearch').on('keyup', function() {
|
||||
const filterValue = $(this).val().toLowerCase();
|
||||
$('#listVehicle .vehicles-list-wrapper').filter(function() {
|
||||
$(this).toggle($(this).text().toLowerCase().indexOf(filterValue) > -1)
|
||||
});
|
||||
});
|
||||
|
||||
// vehicle list item click
|
||||
$('#listVehicle').on('click', '.vhcItem', function() {
|
||||
STATE.vhcId = $(this).data('id');
|
||||
const nopol = $(this).data('nopol');
|
||||
$('#nopol').text(nopol);
|
||||
|
||||
// get media
|
||||
Wrapper.f.getMedia();
|
||||
});
|
||||
|
||||
// submit filter
|
||||
$('#submitFilter').on('click', function() {
|
||||
// get media
|
||||
Wrapper.f.getMedia();
|
||||
});
|
||||
|
||||
// image modal
|
||||
$('#listMedia').on('click', '.gallery-item', function() {
|
||||
// const src = $(this).data('src');
|
||||
// $('#modalImage').attr('src', src);
|
||||
// $('#imageModal').modal('show');
|
||||
|
||||
const imgUrl = $(this).find('img').attr('src');
|
||||
// const bigImgUrl = imgUrl.replace('w=500', 'w=1200');
|
||||
$('#modalImage').attr('src', imgUrl);
|
||||
$('#imageModal').modal('show');
|
||||
});
|
||||
// $('.gallery-card').click(function() {
|
||||
// // Get the image source from the clicked card
|
||||
// const imgUrl = $(this).find('img').attr('src');
|
||||
// const bigImgUrl = imgUrl.replace('w=500', 'w=1200');
|
||||
// $('#modalImage').attr('src', bigImgUrl);
|
||||
|
||||
// // Upgrade resolution for the modal (replace w=500 with w=1200 for better quality)
|
||||
|
||||
// // Set the modal image source
|
||||
|
||||
// // Show the modal using Bootstrap's jQuery interface
|
||||
// $('#imageModal').modal('show');
|
||||
// });
|
||||
|
||||
},
|
||||
f:{
|
||||
getMedia: () => {
|
||||
if (!STATE.vhcId) {
|
||||
Helper.toast('Warning', 'just now', 'please select a vehicle first');
|
||||
return false;
|
||||
}
|
||||
|
||||
const tgl0 = moment($('#tgl0').val(), "DD-MM-YYYY HH:mm").unix();
|
||||
const tgl1 = moment($('#tgl1').val(), "DD-MM-YYYY HH:mm").unix();
|
||||
const camera = $('#filterCamera').val();
|
||||
|
||||
$.ajax({
|
||||
url: "{{ route('api_get_media', '') }}/",
|
||||
data: {
|
||||
a:'listMedia',
|
||||
vhcId: STATE.vhcId,
|
||||
tgl0: tgl0,
|
||||
tgl1: tgl1,
|
||||
camera: camera,
|
||||
},
|
||||
method: 'GET',
|
||||
crossDomain: true,
|
||||
processData: true,
|
||||
headers: {
|
||||
'x-api-key': Helper.getCookie('_trtk'),
|
||||
},
|
||||
// data: params,
|
||||
success: (data, textStatus, jqXHR) => {
|
||||
// console.log("res data", data);
|
||||
|
||||
if (data.meta.type != 'success') {
|
||||
resolve({
|
||||
type: 'fail'
|
||||
});
|
||||
Helper.toast('Warning', 'just now', data.meta.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
$('#listMedia').html(''); // clear media list
|
||||
if (data.count == 0) {
|
||||
// no media
|
||||
$('#listMedia').html(STATE.defaultEmptyMedia);
|
||||
return false;
|
||||
}
|
||||
|
||||
const listMedia = data.data;
|
||||
listMedia.forEach(media => {
|
||||
const mediaItem = `
|
||||
<div class="col-sm-6 col-md-4 col-lg-2 gallery-item" data-type="${media.cam}">
|
||||
<div class="card gallery-card shadow-sm">
|
||||
<span class="media-badge badge-${media.cam.toLowerCase()}">${media.cam == 'I' ? 'Inside' : 'Front'}</span>
|
||||
<div class="img-container">
|
||||
<img src="${media.image}" alt="Media Image">
|
||||
</div>
|
||||
<div class="bg-white border-top p-2">
|
||||
<div class="timestamp-label">
|
||||
<span class="clock-icon"></span> ${moment.unix(media.crt_d/1000).format('DD MMM YYYY HH:mm:ss')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
$('#listMedia').append(mediaItem);
|
||||
});
|
||||
},
|
||||
error: (jqXHR, textStatus, error) => {
|
||||
if (jqXHR.status >= 500) {
|
||||
Helper.toast('Error', 'just now', 'please try again');
|
||||
} else {
|
||||
Helper.toast('Error', 'just now', jqXHR.responseJSON.meta
|
||||
.message);
|
||||
}
|
||||
resolve({
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}}
|
||||
};
|
||||
|
||||
|
||||
|
||||
Wrapper.activate();
|
||||
</script>
|
||||
@endsection
|
||||
@ -21,6 +21,13 @@
|
||||
</a>
|
||||
</li>
|
||||
@endcan
|
||||
@can('media.view')
|
||||
<li class="nav-item {{ Request::segment(1) == 'media' ? 'active' : '' }}">
|
||||
<a class="nav-link d-flex align-items-center text-capitalize" aria-current="page" href="{{ route('view_media') }}">
|
||||
Media
|
||||
</a>
|
||||
</li>
|
||||
@endcan
|
||||
@can('zone.view')
|
||||
<li class="nav-item {{ Request::segment(1) == 'zone' ? 'active' : '' }}">
|
||||
<a class="nav-link d-flex align-items-center text-capitalize" aria-current="page" href="{{ route('view_zone') }}">
|
||||
|
||||
@ -184,6 +184,12 @@ Route::middleware(["auth", "auth.user"])->group(function () {
|
||||
Route::get("/reports/abnormalities", "ReportsController@view_report_abnormalities")->name("view_report_abnormalities");
|
||||
Route::get("/reports/abnormalities-list", "ReportsController@api_report_abnormalities_list")->name("api_report_abnormalities_list");
|
||||
|
||||
// media
|
||||
Route::get("/media", "MediaController@view_media")
|
||||
->name("view_media")
|
||||
->middleware("permission:media.view");
|
||||
Route::get("/api/media", "MediaController@api_get_media")->name("api_get_media");
|
||||
|
||||
|
||||
Route::get("/user/vendor/transactions", "MenuController@view_user_vendor_transaction")->name(
|
||||
"view_user_vendor_transaction"
|
||||
|
||||
Reference in New Issue
Block a user