639 lines
23 KiB
PHP
639 lines
23 KiB
PHP
<?php
|
||
|
||
|
||
|
||
if (getisset("ajax")) {
|
||
?>
|
||
@include("admin-ajax.{$_GET['ajax']}")
|
||
<?php
|
||
exit();
|
||
} ?>
|
||
<?php if (getisset("ajax2")) { //blade ajax system
|
||
?>
|
||
@if(getisset("cache"))
|
||
@includeCache("{$_GET['ajax2']}")
|
||
@else
|
||
@include("{$_GET['ajax2']}")
|
||
@endif
|
||
|
||
<?php
|
||
exit();
|
||
} ?>
|
||
@php
|
||
$permissions = userPermissions();
|
||
@endphp
|
||
<?php $u = u(); ?>
|
||
<!DOCTYPE HTML>
|
||
<html lang="{{App::getLocale()}}">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||
<title>{!! strip_tags($__env->yieldContent('title', '')) !!}</title>
|
||
<link rel="manifest" href="/assets/manifest.json">
|
||
<meta name="author" content="Truncgil Technology">
|
||
<meta property="og:title" content="">
|
||
<meta property="og:site_name" content="https://www.truncgil.com.tr/">
|
||
<meta property="og:description" content="">
|
||
<meta property="og:type" content="app">
|
||
<meta property="og:url" content>
|
||
<meta property="og:image" content>
|
||
<div class="header-zone">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="{{url("assets/favicon.png")}}">
|
||
<link rel="shortcut icon" href="{{url("assets/favicon.png")}}" type="image/png">
|
||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||
<meta name="theme-color" content="#fff" />
|
||
<meta name="description" content="{!! strip_tags($__env->yieldContent('title', '')) !!}">
|
||
|
||
<!-- Favicon -->
|
||
<link rel="icon" href="/assets/favicon.png" type="image/png">
|
||
<script>
|
||
if ('serviceWorker' in navigator) {
|
||
navigator.serviceWorker.register('/service-worker.js')
|
||
.then(function (registration) {
|
||
console.log('Service Worker registered with scope:', registration.scope);
|
||
})
|
||
.catch(function (error) {
|
||
console.log('Service Worker registration failed:', error);
|
||
});
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
@include("admin.inc.plugins")
|
||
@include("admin.inc.citrus-design")
|
||
|
||
@yield('scripts')
|
||
|
||
</div>
|
||
</head>
|
||
|
||
<body>
|
||
@guest
|
||
@yield("content")
|
||
@else
|
||
<div id="page-container" @if(isset($contents))
|
||
class="sidebar-o enable-page-overlay side-scroll page-header-fixed page-header-glass side-trans-enabled sidebar-inverse"
|
||
@endif>
|
||
|
||
|
||
<aside id="side-overlay">
|
||
<div class="content-header content-header-fullrow">
|
||
<div class="content-header-section align-parent">
|
||
<button type="button" class="btn btn-circle btn-dual-secondary align-v-r" data-toggle="layout"
|
||
data-action="side_overlay_close">
|
||
<i class="fa fa-times text-danger"></i>
|
||
</button>
|
||
<div class="content-header-item">
|
||
<a class="align-middle text-primary-dark font-w600" href="#">
|
||
{{ Auth::user()->name }} {{ Auth::user()->surname }}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@include("admin.inc.side-overlay")
|
||
</aside>
|
||
|
||
@if(isset($contents))
|
||
<nav id="sidebar">
|
||
<div class="sidebar-content">
|
||
|
||
<div class="content-side content-side-full content-side-user px-10 align-parent">
|
||
<!-- Visible only in mini mode -->
|
||
<div class="sidebar-mini-visible-b align-v animated fadeIn">
|
||
<a href="{{url("admin")}}">
|
||
<img class="img-fluid" src="{{ url("assets/citrus-logos/citrus-yatay.svg") }}" alt="">
|
||
</a>
|
||
</div>
|
||
<!-- END Visible only in mini mode -->
|
||
|
||
<!-- Visible only in normal mode -->
|
||
<div class="sidebar-mini-hidden-b text-center">
|
||
<a class="img-link" href="{{url("admin")}}">
|
||
<img class="img-fluid" src="{{ url("assets/citrus-logos/citrus-yatay.svg") }}" alt="">
|
||
</a>
|
||
<a class="text-center text-dual-primary-dark font-size-xs font-w600"
|
||
href="#">{{ Auth::user()->name }} {{ Auth::user()->surname }}</a>
|
||
|
||
<div class="text-center badge badge-success">{{ Auth::user()->level }}</div>
|
||
<div class="text-center badge badge-info">{{ Auth::user()->subcontructer }}</div>
|
||
|
||
<ul class="list-inline mt-10">
|
||
<li class="list-inline-item">
|
||
|
||
</li>
|
||
<li class="list-inline-item">
|
||
<!-- Layout API, functionality initialized in Template._uiApiLayout() -->
|
||
<a class="text-dual-primary-dark" data-toggle="layout"
|
||
data-action="sidebar_style_inverse_toggle" href="javascript:void(0)">
|
||
<i class="si si-drop"></i>
|
||
</a>
|
||
</li>
|
||
<li class="list-inline-item">
|
||
<a class="text-dual-primary-dark" href="{{url("logout")}}">
|
||
<i class="si si-logout"></i>
|
||
</a>
|
||
|
||
</li>
|
||
<li class="list-inline-item">
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</div>
|
||
<!-- END Visible only in normal mode -->
|
||
</div>
|
||
|
||
|
||
@include("admin.inc.menu")
|
||
|
||
</div>
|
||
</nav>
|
||
|
||
|
||
@include("admin.inc.header")
|
||
@endif
|
||
|
||
<div class="main-container">
|
||
<div class="">
|
||
|
||
@if (View::hasSection('title'))
|
||
<div class="bg-image d-none">
|
||
<div class="bg-white-op-90">
|
||
<div class="content content-full content-top">
|
||
<h1 class="text-center">@yield("title")<br /> </h1>
|
||
<div class="text-center d-none">@yield("desc")</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
<div class="content-ajax" style="margin-top:70px">
|
||
@yield("content")
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="clearfix"></div>
|
||
@include('admin.inc.feedback-modal')
|
||
<footer id="page-footer" class="t-center" style=" background: gray;
|
||
color: white;
|
||
padding: 5px 10px;
|
||
position: fixed;
|
||
bottom: 0px;
|
||
right: 0px;
|
||
width: 100%;
|
||
text-align: right;
|
||
font-size: 12px;">
|
||
<div class="footer-status"></div>
|
||
</footer>
|
||
</div>
|
||
@endguest
|
||
|
||
<!--
|
||
<link rel="stylesheet" href="//cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css" />
|
||
<script type="text/javascript" src="//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
|
||
-->
|
||
|
||
@include("admin.inc.script")
|
||
|
||
|
||
|
||
<style type="text/css">
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
.visible {
|
||
display: block !important;
|
||
}
|
||
|
||
.hidden-upload {
|
||
display: none;
|
||
|
||
}
|
||
|
||
.table-responsive {
|
||
/* background: url(back.png) white center center / contain no-repeat !important; */
|
||
/* background-attachment: fixed !important; */
|
||
background-size: 20% !important;
|
||
background-position: center !important;
|
||
}
|
||
|
||
.dz-filename {
|
||
white-space: normal !important;
|
||
height: 74px;
|
||
|
||
}
|
||
</style>
|
||
<div class="modal fade" id="modal-popin" tabindex="-1" role="dialog" aria-labelledby="modal-popin"
|
||
aria-hidden="true">
|
||
<div class="modal-dialog modal-lg modal-dialog-popin" role="document">
|
||
<div class="modal-content">
|
||
<div class="block block-themed block-transparent mb-0">
|
||
<div class="block-header bg-primary-dark">
|
||
<h3 class="block-title"></h3>
|
||
<div class="block-options">
|
||
<button type="button" class="btn-block-option" data-dismiss="modal" aria-label="Close">
|
||
<i class="si si-close"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="block-content">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-alt-secondary" data-dismiss="modal">{{__('Hayır')}}</button>
|
||
<a href="#" class="btn btn-alt-success tamam" data-dismiss="modal">
|
||
<i class="fa fa-check"></i> {{__('Evet')}}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notification System CSS -->
|
||
<style>
|
||
.notification-dropdown .notification-badge {
|
||
min-width: 18px;
|
||
height: 18px;
|
||
line-height: 14px;
|
||
}
|
||
|
||
.notification-menu {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.notification-list {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.notification-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding: 14px 18px;
|
||
border-bottom: 1px solid rgba(17, 25, 40, 0.08);
|
||
cursor: pointer;
|
||
position: relative;
|
||
background: #ffffff;
|
||
transition: background 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.notification-item:hover {
|
||
background: #e8f1ff;
|
||
}
|
||
|
||
.notification-item.unread {
|
||
background: #eef6ff;
|
||
}
|
||
|
||
.notification-item.unread::after {
|
||
content: '';
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #3a7bd5;
|
||
position: absolute;
|
||
top: 18px;
|
||
right: 18px;
|
||
box-shadow: 0 0 0 2px rgba(58, 123, 213, 0.15);
|
||
}
|
||
|
||
.notification-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.notification-item .icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(58, 123, 213, 0.1);
|
||
color: #3a7bd5;
|
||
margin-right: 12px;
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.notification-item .icon i {
|
||
line-height: 1;
|
||
}
|
||
|
||
.notification-item .content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.notification-item .content .title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #111928;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.notification-item .content .message {
|
||
font-size: 13px;
|
||
color: #4a5568;
|
||
margin-bottom: 6px;
|
||
line-height: 1.4;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.notification-item .content .meta {
|
||
font-size: 12px;
|
||
color: #6c757d;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.notification-item .content .meta i {
|
||
font-size: 11px;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.notification-empty {
|
||
padding: 48px 24px;
|
||
text-align: center;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.notification-empty i {
|
||
font-size: 36px;
|
||
margin-bottom: 12px;
|
||
color: rgba(58, 123, 213, 0.55);
|
||
}
|
||
</style>
|
||
|
||
<!-- Notification System JavaScript -->
|
||
<script>
|
||
// Performance: Debounce function to limit API calls
|
||
function debounce(func, wait) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func(...args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
}
|
||
|
||
// Performance: Cache for notification count to avoid repeated API calls
|
||
let notificationCountCache = null;
|
||
let notificationCountCacheTime = 0;
|
||
const CACHE_DURATION = 30000; // 30 seconds
|
||
|
||
// Load notifications for dropdown
|
||
function loadNotifications() {
|
||
$.ajax({
|
||
url: '/api/notifications/recent',
|
||
method: 'GET',
|
||
success: function (response) {
|
||
displayNotifications(response.notifications);
|
||
updateNotificationCount(response.unread_count);
|
||
},
|
||
error: function () {
|
||
$('#notification-list').html(`
|
||
<li class="notification-placeholder">
|
||
<i class="fa fa-exclamation-triangle text-danger"></i>
|
||
<div class="font-weight-bold">{{e2("Unable to load notifications")}}</div>
|
||
<div class="small">{{e2("Please try again shortly")}}</div>
|
||
</li>
|
||
`);
|
||
$('#notification-unread-indicator').text('{{e2("Error")}}');
|
||
}
|
||
});
|
||
}
|
||
|
||
function getNotificationIcon(notificationCode, isRead) {
|
||
if (!isRead) {
|
||
return 'fa-bell';
|
||
}
|
||
|
||
if (!notificationCode) return 'fa-bell';
|
||
|
||
const code = notificationCode.toLowerCase();
|
||
if (code.includes('success') || code.includes('complete') || code.includes('created')) return 'fa-check-circle';
|
||
if (code.includes('error') || code.includes('fail')) return 'fa-exclamation-circle';
|
||
if (code.includes('warning') || code.includes('alert')) return 'fa-exclamation-triangle';
|
||
if (code.includes('info') || code.includes('update')) return 'fa-info-circle';
|
||
if (code.includes('test') || code.includes('inspection')) return 'fa-clipboard-check';
|
||
if (code.includes('document') || code.includes('file')) return 'fa-file-alt';
|
||
if (code.includes('user') || code.includes('account')) return 'fa-user';
|
||
if (code.includes('system') || code.includes('maintenance')) return 'fa-cog';
|
||
|
||
return 'fa-bell';
|
||
}
|
||
|
||
// Display notifications in dropdown
|
||
function displayNotifications(notifications) {
|
||
const listContainer = $('#notification-list');
|
||
const defaultTitle = '{{e2("Notification")}}';
|
||
const defaultMessage = '{{e2("No additional details")}}';
|
||
|
||
if (!notifications || notifications.length === 0) {
|
||
listContainer.html(`
|
||
<li class="notification-placeholder">
|
||
<i class="fa fa-bell-slash"></i>
|
||
<div class="font-weight-bold">{{e2("No notifications")}}</div>
|
||
<div class="small">{{e2("You are all caught up")}}</div>
|
||
</li>
|
||
`);
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
notifications.forEach(function (notif) {
|
||
const isUnread = !notif.is_read;
|
||
const unreadClass = isUnread ? 'unread' : '';
|
||
const timeAgo = formatTimeAgo(notif.created_at);
|
||
const link = notif.link || '#';
|
||
const safeTitle = notif.title ? escapeHtml(notif.title) : defaultTitle;
|
||
const safeMessage = notif.message ? escapeHtml(notif.message) : defaultMessage;
|
||
const iconClass = getNotificationIcon(notif.notification_code, !isUnread);
|
||
|
||
const truncatedMessage = safeMessage.length > 100
|
||
? safeMessage.substring(0, 100) + '...'
|
||
: safeMessage;
|
||
|
||
html += `
|
||
<li class="notification-item ${unreadClass}"
|
||
data-id="${notif.id}"
|
||
onclick="handleNotificationClick(${notif.id}, '${escapeHtml(link)}')"
|
||
title="${safeMessage}">
|
||
<div class="icon">
|
||
<i class="fa ${iconClass}"></i>
|
||
</div>
|
||
<div class="content">
|
||
<div class="title">${safeTitle}</div>
|
||
<div class="message">${truncatedMessage}</div>
|
||
<div class="meta">
|
||
<i class="fa fa-clock"></i>
|
||
${timeAgo}
|
||
</div>
|
||
</div>
|
||
</li>
|
||
`;
|
||
});
|
||
|
||
listContainer.html(html);
|
||
}
|
||
|
||
// Handle notification click
|
||
function handleNotificationClick(notificationId, link) {
|
||
// Mark as read and wait for response before navigation
|
||
$.ajax({
|
||
url: '/api/notifications/mark-read/' + notificationId,
|
||
method: 'POST',
|
||
data: { _token: '{{ csrf_token() }}' },
|
||
async: false, // Wait for mark-read to complete
|
||
success: function (response) {
|
||
if (response.success) {
|
||
updateNotificationCount(response.unread_count);
|
||
$('[data-id="' + notificationId + '"]').removeClass('unread');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Navigate to link if provided
|
||
if (link && link !== '#') {
|
||
window.location.href = link;
|
||
}
|
||
}
|
||
|
||
// Mark all notifications as read
|
||
function markAllAsRead() {
|
||
$.ajax({
|
||
url: '/api/notifications/mark-all-read',
|
||
method: 'POST',
|
||
data: { _token: '{{ csrf_token() }}' },
|
||
success: function (response) {
|
||
if (response.success) {
|
||
updateNotificationCount(0);
|
||
$('.notification-item').removeClass('unread');
|
||
Swal.fire({
|
||
icon: 'success',
|
||
title: '{{e2("Success")}}',
|
||
text: '{{e2("All notifications marked as read")}}',
|
||
timer: 2000,
|
||
showConfirmButton: false
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Update notification count badge
|
||
function updateNotificationCount(count) {
|
||
notificationCountCache = count;
|
||
notificationCountCacheTime = Date.now();
|
||
|
||
const badge = $('#notification-count');
|
||
const indicator = $('#notification-unread-indicator');
|
||
if (count > 0) {
|
||
const displayCount = count > 99 ? '99+' : count;
|
||
badge.text(displayCount).show();
|
||
if (indicator.length) {
|
||
indicator.text(`${displayCount} {{e2("new")}}`).removeClass('badge-secondary').addClass('badge-primary');
|
||
}
|
||
} else {
|
||
badge.hide();
|
||
if (indicator.length) {
|
||
indicator.text('{{e2("All caught up")}}').removeClass('badge-primary').addClass('badge-success');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fetch notification count (with caching)
|
||
function fetchNotificationCount() {
|
||
// Use cache if valid
|
||
if (notificationCountCache !== null &&
|
||
(Date.now() - notificationCountCacheTime) < CACHE_DURATION) {
|
||
updateNotificationCount(notificationCountCache);
|
||
return;
|
||
}
|
||
|
||
// Performance: Only fetch count, not full notifications
|
||
$.ajax({
|
||
url: '/api/notifications/unread-count',
|
||
method: 'GET',
|
||
success: function (response) {
|
||
updateNotificationCount(response.unread_count);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Format time ago
|
||
function formatTimeAgo(dateString) {
|
||
const date = new Date(dateString);
|
||
const now = new Date();
|
||
const seconds = Math.floor((now - date) / 1000);
|
||
|
||
if (seconds < 60) return '{{e2("Just now")}}';
|
||
if (seconds < 3600) return Math.floor(seconds / 60) + ' {{e2("min ago")}}';
|
||
if (seconds < 86400) return Math.floor(seconds / 3600) + ' {{e2("h ago")}}';
|
||
if (seconds < 604800) return Math.floor(seconds / 86400) + ' {{e2("d ago")}}';
|
||
return date.toLocaleDateString();
|
||
}
|
||
|
||
// Escape HTML to prevent XSS
|
||
function escapeHtml(text) {
|
||
const map = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
};
|
||
return text.replace(/[&<>"']/g, function (m) { return map[m]; });
|
||
}
|
||
|
||
// Initialize notification system on page load
|
||
$(document).ready(function () {
|
||
// Fetch initial count
|
||
fetchNotificationCount();
|
||
|
||
// Poll for new notifications every 15 seconds (for testing - can be increased to 60s in production)
|
||
setInterval(fetchNotificationCount, 15000);
|
||
|
||
// Prevent dropdown from closing when clicking inside
|
||
$('.notification-menu').on('click', function (e) {
|
||
e.stopPropagation();
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<div id="screenshotOverlay" style="
|
||
display:none;
|
||
position:fixed;
|
||
inset:0;
|
||
background:rgba(0,0,0,.7);
|
||
z-index:9999;
|
||
padding:20px;
|
||
overflow:auto;">
|
||
<div style="background:#fff; padding:10px; max-width:90%; margin:0 auto;">
|
||
<h5 style="margin-bottom:10px">Crop your screenshot</h5>
|
||
<div id="overlayPreview"></div>
|
||
<button id="useCropped" class="btn btn-sm btn-primary mt-2">Use this & open feedback</button>
|
||
<button id="cancelCrop" class="btn btn-sm btn-secondary mt-2">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
</body>
|
||
|
||
</html>
|
||
<style type="text/css">
|
||
.cke_button__easyimageupload {
|
||
display: none !important;
|
||
}
|
||
</style>
|