1075 lines
34 KiB
PHP
1075 lines
34 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ $title ?? 'Absensi' }}</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">
|
|
<style>
|
|
:root {
|
|
--font-sans: 'Plus Jakarta Sans', 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
--bg: #030712;
|
|
--bg-alt: #060c1a;
|
|
--bg-soft: rgba(6, 12, 26, 0.78);
|
|
--card: rgba(11, 19, 40, 0.82);
|
|
--card-strong: rgba(13, 23, 50, 0.92);
|
|
--border: rgba(129, 140, 248, 0.26);
|
|
--border-soft: rgba(148, 163, 184, 0.16);
|
|
--border-strong: rgba(59, 130, 246, 0.3);
|
|
--text: #f8fafc;
|
|
--text-muted: #9ca3af;
|
|
--muted-chip: rgba(148, 163, 184, 0.16);
|
|
--primary: #6366f1;
|
|
--primary-strong: #2563eb;
|
|
--primary-soft: rgba(99, 102, 241, 0.12);
|
|
--danger: #ef4444;
|
|
--danger-accent: #f97316;
|
|
--success: #22c55e;
|
|
--warning: #f59e0b;
|
|
--header-height: 76px;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
::selection {
|
|
background: rgba(99, 102, 241, 0.35);
|
|
color: #f8fafc;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
font-family: var(--font-sans);
|
|
background:
|
|
radial-gradient(circle at 20% -10%, rgba(59, 130, 246, 0.24), transparent 45%),
|
|
radial-gradient(circle at 85% 0%, rgba(129, 140, 248, 0.2), transparent 55%),
|
|
linear-gradient(160deg, #030712 0%, #020617 38%, #020819 100%);
|
|
color: var(--text);
|
|
color-scheme: dark;
|
|
}
|
|
|
|
header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 40;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 18px 32px;
|
|
min-height: var(--header-height);
|
|
backdrop-filter: blur(24px);
|
|
background: radial-gradient(120% 120% at 0% 0%, rgba(59, 130, 246, 0.16), transparent 55%), rgba(7, 13, 29, 0.84);
|
|
border-bottom: 1px solid rgba(148, 163, 184, 0.16);
|
|
box-shadow: 0 24px 50px rgba(3, 7, 18, 0.55);
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18px;
|
|
}
|
|
|
|
header .brand {
|
|
font-weight: 700;
|
|
font-size: 21px;
|
|
letter-spacing: -0.015em;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
header nav,
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
header nav form {
|
|
margin: 0;
|
|
}
|
|
|
|
.sidebar-toggle {
|
|
display: inline-flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
width: 46px;
|
|
height: 46px;
|
|
border-radius: 14px;
|
|
border: 1px solid rgba(99, 102, 241, 0.28);
|
|
background: linear-gradient(145deg, rgba(30, 41, 70, 0.96), rgba(15, 23, 42, 0.78));
|
|
cursor: pointer;
|
|
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
box-shadow: 0 12px 26px rgba(15, 23, 42, 0.45);
|
|
}
|
|
|
|
.sidebar-toggle span {
|
|
display: block;
|
|
width: 22px;
|
|
height: 3px;
|
|
border-radius: 999px;
|
|
background: linear-gradient(90deg, rgba(226, 232, 240, 0.95), rgba(148, 163, 184, 0.8));
|
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
box-shadow: 0 2px 8px rgba(30, 64, 175, 0.25);
|
|
transform-origin: center;
|
|
}
|
|
|
|
.sidebar-toggle span + span {}
|
|
|
|
.sidebar-toggle:focus-visible {
|
|
outline: 3px solid rgba(99, 102, 241, 0.35);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.sidebar-toggle:hover {
|
|
border-color: rgba(129, 140, 248, 0.65);
|
|
background: linear-gradient(145deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.45));
|
|
box-shadow: 0 18px 36px rgba(37, 99, 235, 0.28);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
body.sidebar-open .sidebar-toggle span:nth-child(1) {
|
|
transform: translateY(6px) rotate(45deg);
|
|
}
|
|
|
|
body.sidebar-open .sidebar-toggle span:nth-child(2) {
|
|
opacity: 0;
|
|
}
|
|
|
|
body.sidebar-open .sidebar-toggle span:nth-child(3) {
|
|
transform: translateY(-6px) rotate(-45deg);
|
|
}
|
|
|
|
.sidebar-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(3, 7, 18, 0.58);
|
|
backdrop-filter: blur(6px);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.25s ease;
|
|
z-index: 55;
|
|
}
|
|
|
|
body.sidebar-open .sidebar-overlay {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
body.sidebar-open {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.layout {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: stretch;
|
|
gap: 28px;
|
|
padding: 32px 36px 40px;
|
|
min-height: calc(100vh - 90px);
|
|
}
|
|
|
|
.layout.no-sidebar {
|
|
display: block;
|
|
padding: 32px;
|
|
}
|
|
|
|
.sidebar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: min(320px, 88vw);
|
|
padding: calc(var(--header-height) + 16px) 22px 32px;
|
|
border-radius: 0 24px 24px 0;
|
|
background: rgba(4, 10, 24, 0.94);
|
|
border: 1px solid rgba(129, 140, 248, 0.18);
|
|
box-shadow: 0 32px 60px rgba(3, 7, 18, 0.6);
|
|
backdrop-filter: blur(24px);
|
|
transform: translateX(-110%);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
z-index: 60;
|
|
overflow-y: auto;
|
|
transition: transform 0.28s ease, opacity 0.28s ease;
|
|
}
|
|
|
|
body.sidebar-open .sidebar {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.sidebar .menu {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0 0 30px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.sidebar .menu-heading {
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.14em;
|
|
color: rgba(226, 232, 240, 0.58);
|
|
margin-bottom: 6px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.sidebar .menu li {
|
|
margin: 0;
|
|
}
|
|
|
|
.sidebar .menu a {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-radius: 16px;
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
letter-spacing: 0.01em;
|
|
border: 1px solid rgba(148, 163, 184, 0.08);
|
|
background: radial-gradient(circle at 0 50%, rgba(59, 130, 246, 0.12), transparent 55%), rgba(14, 23, 44, 0.65);
|
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
|
box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.1);
|
|
}
|
|
|
|
.sidebar .menu a::before {
|
|
content: "";
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 999px;
|
|
background: rgba(148, 163, 184, 0.4);
|
|
transition: background 0.2s ease, transform 0.2s ease;
|
|
}
|
|
|
|
.sidebar .menu a:hover {
|
|
transform: translateX(6px);
|
|
border-color: rgba(129, 140, 248, 0.3);
|
|
background: linear-gradient(135deg, rgba(37, 99, 235, 0.22), rgba(99, 102, 241, 0.18));
|
|
box-shadow: 0 18px 30px rgba(14, 23, 44, 0.45);
|
|
}
|
|
|
|
.sidebar .menu a:hover::before {
|
|
background: rgba(96, 165, 250, 0.75);
|
|
transform: scale(1.4);
|
|
}
|
|
|
|
.sidebar .menu a .menu-label {
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.sidebar .menu a .menu-label small {
|
|
font-size: 11px;
|
|
color: rgba(203, 213, 225, 0.62);
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.sidebar .menu a.active {
|
|
border-color: rgba(129, 140, 248, 0.55);
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(56, 189, 248, 0.18));
|
|
box-shadow: 0 18px 32px rgba(37, 99, 235, 0.28), inset 0 0 0 1px rgba(148, 163, 184, 0.22);
|
|
}
|
|
|
|
.sidebar .menu a.active::before {
|
|
background: rgba(129, 140, 248, 0.95);
|
|
transform: scale(1.6);
|
|
box-shadow: 0 0 12px rgba(129, 140, 248, 0.55);
|
|
}
|
|
|
|
.sidebar .menu .notif-link {
|
|
position: relative;
|
|
}
|
|
|
|
.sidebar .menu .notif-link .notif-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 18px;
|
|
padding: 2px 6px;
|
|
border-radius: 999px;
|
|
background: linear-gradient(135deg, #f97316, #ef4444);
|
|
color: #fff7ed;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.sidebar .menu .notif-link .notif-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
background: #f97316;
|
|
}
|
|
|
|
.sidebar-users {
|
|
padding: 18px;
|
|
border-radius: 18px;
|
|
background: rgba(9, 16, 34, 0.82);
|
|
border: 1px solid rgba(129, 140, 248, 0.22);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.28);
|
|
}
|
|
|
|
.sidebar-users-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #cbd5f5;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.sidebar-users-total {
|
|
background: rgba(59, 130, 246, 0.18);
|
|
color: #dbeafe;
|
|
padding: 2px 10px;
|
|
border-radius: 999px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.sidebar-users-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.sidebar-users-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
border-radius: 14px;
|
|
background: rgba(15, 23, 42, 0.62);
|
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
|
}
|
|
|
|
.sidebar-users-item:hover,
|
|
.sidebar-users-item:focus-visible {
|
|
border-color: rgba(99, 102, 241, 0.35);
|
|
background: rgba(15, 23, 42, 0.78);
|
|
transform: translateX(2px);
|
|
outline: none;
|
|
}
|
|
|
|
.sidebar-users-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 999px;
|
|
background: linear-gradient(140deg, rgba(99, 102, 241, 0.6), rgba(14, 165, 233, 0.4));
|
|
color: #ecfeff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.sidebar-users-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
font-size: 12px;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
.sidebar-users-info span {
|
|
color: var(--text-muted);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.sidebar-users-footer {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
main {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
padding: 32px 36px;
|
|
border-radius: 28px;
|
|
background: rgba(6, 12, 26, 0.6);
|
|
border: 1px solid rgba(148, 163, 184, 0.14);
|
|
box-shadow: 0 32px 48px rgba(4, 7, 18, 0.55);
|
|
backdrop-filter: blur(26px);
|
|
}
|
|
|
|
main > *:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.card {
|
|
margin-bottom: 28px;
|
|
padding: 24px 26px;
|
|
border-radius: 20px;
|
|
background: var(--card);
|
|
border: 1px solid var(--border-soft);
|
|
box-shadow: 0 22px 46px rgba(6, 10, 24, 0.45);
|
|
}
|
|
|
|
.table-responsive {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.card:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.role-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 6px 12px;
|
|
border-radius: 999px;
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(56, 189, 248, 0.22));
|
|
color: #f1f5f9;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 10px 16px;
|
|
border-radius: 12px;
|
|
border: 1px solid transparent;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
letter-spacing: 0.01em;
|
|
cursor: pointer;
|
|
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, background 0.18s ease;
|
|
background: rgba(15, 23, 42, 0.78);
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn:focus-visible {
|
|
outline: 3px solid rgba(99, 102, 241, 0.35);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, var(--primary), var(--primary-strong));
|
|
border-color: rgba(99, 102, 241, 0.4);
|
|
color: #f8fafc;
|
|
box-shadow: 0 18px 30px rgba(79, 70, 229, 0.32);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: linear-gradient(135deg, var(--danger), var(--danger-accent));
|
|
border-color: rgba(239, 68, 68, 0.28);
|
|
color: #fff7ed;
|
|
box-shadow: 0 18px 30px rgba(249, 115, 22, 0.32);
|
|
}
|
|
|
|
.btn-outline {
|
|
background: transparent;
|
|
border-color: rgba(148, 163, 184, 0.32);
|
|
color: var(--text);
|
|
}
|
|
|
|
.btn-outline:hover {
|
|
background: rgba(15, 23, 42, 0.6);
|
|
border-color: rgba(129, 140, 248, 0.4);
|
|
}
|
|
|
|
.btn-ghost {
|
|
background: rgba(15, 23, 42, 0.3);
|
|
color: var(--text);
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 8px 12px;
|
|
border-radius: 10px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
}
|
|
|
|
th, td {
|
|
text-align: left;
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid rgba(148, 163, 184, 0.16);
|
|
}
|
|
|
|
th {
|
|
color: #cbd5f5;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
font-size: 13px;
|
|
}
|
|
|
|
td {
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
}
|
|
|
|
@media (max-width: 1280px) {
|
|
header {
|
|
padding: 16px 24px;
|
|
}
|
|
|
|
.layout {
|
|
padding: 24px 28px 32px;
|
|
}
|
|
|
|
main {
|
|
padding: 28px 28px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.layout {
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
padding: 20px 18px 28px;
|
|
}
|
|
|
|
.layout.no-sidebar {
|
|
padding: 20px 18px 28px;
|
|
}
|
|
|
|
main {
|
|
padding: 24px 20px;
|
|
}
|
|
|
|
body.sidebar-open .sidebar {
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1025px) {
|
|
.sidebar {
|
|
width: 280px;
|
|
padding: calc(var(--header-height) + 28px) 26px 36px;
|
|
}
|
|
|
|
body.sidebar-open .sidebar-overlay {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
body.sidebar-open {
|
|
overflow: auto;
|
|
}
|
|
|
|
.layout {
|
|
padding-left: 0;
|
|
}
|
|
|
|
body.sidebar-open .layout {
|
|
padding-left: 260px;
|
|
transition: padding-left 0.28s ease;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
header {
|
|
padding: 14px 18px;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
|
|
.header-actions {
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.header-left {
|
|
gap: 14px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
header {
|
|
padding: 14px 16px;
|
|
}
|
|
|
|
header nav {
|
|
gap: 10px;
|
|
}
|
|
|
|
.layout {
|
|
padding: 18px 14px 26px;
|
|
}
|
|
|
|
main {
|
|
padding: 22px 16px;
|
|
}
|
|
|
|
.card {
|
|
padding: 22px 18px;
|
|
}
|
|
|
|
table {
|
|
min-width: 600px;
|
|
}
|
|
|
|
.card,
|
|
.sidebar-users,
|
|
.table-responsive,
|
|
.admin-user-attendance-list,
|
|
main > table,
|
|
.absensi-table-wrapper,
|
|
.report-table-wrapper,
|
|
.leave-table-wrapper {
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
header {
|
|
padding: 12px 14px;
|
|
}
|
|
|
|
.header-left {
|
|
gap: 12px;
|
|
}
|
|
|
|
header .brand {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.btn.btn-sm {
|
|
padding: 8px 10px;
|
|
}
|
|
|
|
.card {
|
|
padding: 20px 16px;
|
|
}
|
|
|
|
main {
|
|
padding: 20px 14px;
|
|
}
|
|
|
|
.header-actions {
|
|
width: 100%;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
}
|
|
|
|
.header-actions form,
|
|
.header-actions .btn {
|
|
width: 100%;
|
|
}
|
|
|
|
.header-actions form button {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
header {
|
|
padding: 12px;
|
|
}
|
|
|
|
.layout,
|
|
.layout.no-sidebar {
|
|
padding: 16px 12px 24px;
|
|
}
|
|
|
|
main {
|
|
padding: 18px 12px;
|
|
}
|
|
|
|
.card {
|
|
padding: 18px 14px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<!-- Leaflet Maps Library -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
|
|
|
@stack('head')
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="header-left">
|
|
<button class="sidebar-toggle" type="button" aria-label="Toggle sidebar" data-sidebar-toggle>
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</button>
|
|
<div class="brand">
|
|
<span style="display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px; border-radius:12px; background:linear-gradient(135deg, rgba(99,102,241,0.65), rgba(37,99,235,0.5)); box-shadow:0 10px 22px rgba(37,99,235,0.28);">
|
|
<span style="font-weight:700; font-size:16px;">A</span>
|
|
</span>
|
|
Absensi
|
|
</div>
|
|
</div>
|
|
<nav class="header-actions">
|
|
@auth
|
|
<span class="role-pill">{{ auth()->user()->role ?? 'User' }}</span>
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<button type="submit" class="btn btn-danger btn-sm">Logout</button>
|
|
</form>
|
|
@endauth
|
|
@guest
|
|
<a href="{{ route('login') }}" class="btn btn-primary btn-sm">Login</a>
|
|
@endguest
|
|
</nav>
|
|
<div class="sidebar-overlay" data-sidebar-overlay></div>
|
|
</header>
|
|
<div class="layout {{ auth()->check() ? '' : 'no-sidebar' }}">
|
|
@auth
|
|
<aside class="sidebar">
|
|
@if(in_array(auth()->user()->role ?? 'pegawai', ['admin','atasan']))
|
|
<ul class="menu">
|
|
<li><a href="{{ route('admin.absensi.index') }}" class="{{ request()->is('admin/absensi*') ? 'active' : '' }}">Data Absensi</a></li>
|
|
<li><a href="{{ route('admin.barang_rusak.index') }}" class="{{ request()->is('admin/barang-rusak*') ? 'active' : '' }}">Laporan Barang Rusak</a></li>
|
|
<li><a href="{{ route('admin.cuti.index') }}" class="{{ request()->is('admin/cuti*') ? 'active' : '' }}">Pengajuan Cuti</a></li>
|
|
<li><a href="{{ route('admin.notifications.index') }}" class="{{ request()->is('admin/notifications*') ? 'active' : '' }}">Pemberitahuan</a></li>
|
|
<li><a href="{{ route('admin.users.index') }}" class="{{ request()->is('admin/users*') ? 'active' : '' }}">Daftar Pengguna</a></li>
|
|
</ul>
|
|
@php
|
|
$sidebarUsers = cache()->remember('sidebar_users_summary', 60, function () {
|
|
return [
|
|
'total' => \App\Models\User::count(),
|
|
'items' => \App\Models\User::select('id', 'name', 'username', 'role', 'attendance_enabled')
|
|
->orderBy('name')
|
|
->limit(6)
|
|
->get(),
|
|
];
|
|
});
|
|
@endphp
|
|
<div class="sidebar-users">
|
|
<div class="sidebar-users-header">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
|
<div>
|
|
<span>Pengguna Terdaftar</span>
|
|
<span class="sidebar-users-total">{{ $sidebarUsers['total'] }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul class="sidebar-users-list">
|
|
@foreach($sidebarUsers['items'] as $user)
|
|
@php
|
|
$initials = collect(explode(' ', trim($user->name)))->filter()->map(fn ($part) => mb_substr($part, 0, 1))->take(2)->implode('');
|
|
$initials = mb_strtoupper($initials ?: 'U');
|
|
@endphp
|
|
<li>
|
|
<div class="sidebar-users-item" style="display:flex;align-items:center;gap:12px;">
|
|
<a href="{{ route('admin.users.show', $user->id) }}" style="display:flex;align-items:center;gap:12px;text-decoration:none;color:inherit;flex:1;">
|
|
<span class="sidebar-users-avatar">{{ $initials }}</span>
|
|
<div class="sidebar-users-info" style="min-width:0;">
|
|
<strong style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:110px;">{{ $user->name }}</strong>
|
|
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:110px;">{{ $user->username ?? 'tanpa username' }} • {{ strtoupper($user->role ?? '-') }}</span>
|
|
</div>
|
|
</a>
|
|
{{-- Tombol ON/OFF dan Hapus dipindahkan ke halaman Daftar Pengguna --}}
|
|
</div>
|
|
</li>
|
|
@endforeach
|
|
</ul>
|
|
<div class="sidebar-users-footer">Menampilkan {{ $sidebarUsers['items']->count() }} dari {{ $sidebarUsers['total'] }} pengguna.</div>
|
|
</div>
|
|
@else
|
|
@php
|
|
$unreadNotifications = auth()->user()->notifications()->unread()->count();
|
|
@endphp
|
|
<ul class="menu">
|
|
<li class="menu-heading">Navigasi Utama</li>
|
|
<li>
|
|
<a href="{{ route('user.absensi') }}" class="{{ request()->routeIs('user.absensi') ? 'active' : '' }}">
|
|
<span class="menu-label">
|
|
Absensi
|
|
<small>Kelola absensi harian Anda</small>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ route('barang-rusak.index') }}" class="{{ request()->routeIs('barang-rusak.*') ? 'active' : '' }}">
|
|
<span class="menu-label">
|
|
Barang Rusak
|
|
<small>Laporkan kerusakan dan pantau status</small>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ route('cuti.form') }}" class="{{ request()->routeIs('cuti.form') ? 'active' : '' }}">
|
|
<span class="menu-label">
|
|
Form Cuti
|
|
<small>Ajukan permohonan cuti</small>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ route('profile.index') }}" class="{{ request()->routeIs('profile.*') ? 'active' : '' }}">
|
|
<span class="menu-label">
|
|
Profil
|
|
<small>Perbarui informasi pribadi</small>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
<li class="menu-heading" style="margin-top: 10px;">Komunikasi</li>
|
|
<li>
|
|
<a href="{{ route('notifications.index') }}" class="notif-link {{ request()->routeIs('notifications.*') ? 'active' : '' }}">
|
|
<span class="menu-label">
|
|
Pemberitahuan
|
|
<small>Kabar terbaru dari admin</small>
|
|
</span>
|
|
@if($unreadNotifications > 0)
|
|
<span class="notif-badge">{{ $unreadNotifications > 9 ? '9+' : $unreadNotifications }}</span>
|
|
@endif
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
@endif
|
|
</aside>
|
|
@endauth
|
|
<main>
|
|
@yield('content')
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const body = document.body;
|
|
const toggle = document.querySelector('[data-sidebar-toggle]');
|
|
const overlay = document.querySelector('[data-sidebar-overlay]');
|
|
const sidebar = document.querySelector('.sidebar');
|
|
if (!toggle || !overlay || !sidebar) {
|
|
return;
|
|
}
|
|
|
|
const DESKTOP_BREAKPOINT = 1024;
|
|
const EDGE_GESTURE_START = 64;
|
|
const SWIPE_TRIGGER_DISTANCE = 60;
|
|
const MAX_VERTICAL_DRIFT = 90;
|
|
|
|
const applyScrollLock = () => {
|
|
if (window.innerWidth <= DESKTOP_BREAKPOINT && body.classList.contains('sidebar-open')) {
|
|
body.style.overflow = 'hidden';
|
|
} else {
|
|
body.style.overflow = '';
|
|
}
|
|
};
|
|
|
|
const openSidebar = () => {
|
|
if (body.classList.contains('sidebar-open')) {
|
|
applyScrollLock();
|
|
return;
|
|
}
|
|
|
|
body.classList.add('sidebar-open');
|
|
applyScrollLock();
|
|
};
|
|
|
|
const closeSidebar = () => {
|
|
if (!body.classList.contains('sidebar-open')) {
|
|
body.style.overflow = '';
|
|
return;
|
|
}
|
|
|
|
body.classList.remove('sidebar-open');
|
|
body.style.overflow = '';
|
|
};
|
|
|
|
const toggleSidebar = () => {
|
|
if (body.classList.contains('sidebar-open')) {
|
|
closeSidebar();
|
|
} else {
|
|
openSidebar();
|
|
}
|
|
};
|
|
|
|
toggle.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
toggleSidebar();
|
|
});
|
|
|
|
overlay.addEventListener('click', closeSidebar);
|
|
|
|
document.addEventListener('keyup', (event) => {
|
|
if (event.key === 'Escape') {
|
|
closeSidebar();
|
|
}
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
applyScrollLock();
|
|
});
|
|
|
|
const sidebarLinks = sidebar.querySelectorAll('.menu a');
|
|
sidebarLinks.forEach((link) => {
|
|
link.addEventListener('click', () => {
|
|
if (window.innerWidth <= DESKTOP_BREAKPOINT) {
|
|
closeSidebar();
|
|
}
|
|
});
|
|
});
|
|
|
|
const shouldStartGesture = (event) => {
|
|
if (window.innerWidth > DESKTOP_BREAKPOINT) {
|
|
return false;
|
|
}
|
|
|
|
const open = body.classList.contains('sidebar-open');
|
|
|
|
if (!open) {
|
|
return event.clientX <= EDGE_GESTURE_START;
|
|
}
|
|
|
|
return sidebar.contains(event.target) || overlay.contains(event.target);
|
|
};
|
|
|
|
let pointerId = null;
|
|
let pointerActive = false;
|
|
let startX = 0;
|
|
let startY = 0;
|
|
|
|
const resetPointer = () => {
|
|
pointerId = null;
|
|
pointerActive = false;
|
|
startX = 0;
|
|
startY = 0;
|
|
};
|
|
|
|
const onPointerDown = (event) => {
|
|
if (!(event.pointerType === 'touch' || event.pointerType === 'pen')) {
|
|
return;
|
|
}
|
|
|
|
if (!shouldStartGesture(event)) {
|
|
resetPointer();
|
|
return;
|
|
}
|
|
|
|
pointerId = event.pointerId;
|
|
pointerActive = true;
|
|
startX = event.clientX;
|
|
startY = event.clientY;
|
|
};
|
|
|
|
const onPointerMove = (event) => {
|
|
if (!pointerActive || event.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
|
|
const deltaX = event.clientX - startX;
|
|
const deltaY = Math.abs(event.clientY - startY);
|
|
const open = body.classList.contains('sidebar-open');
|
|
|
|
if (deltaY > MAX_VERTICAL_DRIFT && Math.abs(deltaX) < deltaY) {
|
|
resetPointer();
|
|
return;
|
|
}
|
|
|
|
if (!open && deltaX > SWIPE_TRIGGER_DISTANCE) {
|
|
openSidebar();
|
|
resetPointer();
|
|
return;
|
|
}
|
|
|
|
if (open && deltaX < -SWIPE_TRIGGER_DISTANCE) {
|
|
closeSidebar();
|
|
resetPointer();
|
|
}
|
|
};
|
|
|
|
const onPointerEnd = (event) => {
|
|
if (event.pointerId === pointerId) {
|
|
resetPointer();
|
|
}
|
|
};
|
|
|
|
if (window.PointerEvent) {
|
|
document.addEventListener('pointerdown', onPointerDown);
|
|
document.addEventListener('pointermove', onPointerMove);
|
|
document.addEventListener('pointerup', onPointerEnd);
|
|
document.addEventListener('pointercancel', onPointerEnd);
|
|
} else {
|
|
document.addEventListener('touchstart', (event) => {
|
|
if (event.touches.length !== 1) {
|
|
return;
|
|
}
|
|
|
|
const touch = event.touches[0];
|
|
if (!shouldStartGesture({ clientX: touch.clientX, target: event.target })) {
|
|
return;
|
|
}
|
|
|
|
pointerActive = true;
|
|
startX = touch.clientX;
|
|
startY = touch.clientY;
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('touchmove', (event) => {
|
|
if (!pointerActive || event.touches.length !== 1) {
|
|
return;
|
|
}
|
|
|
|
const touch = event.touches[0];
|
|
const deltaX = touch.clientX - startX;
|
|
const deltaY = Math.abs(touch.clientY - startY);
|
|
const open = body.classList.contains('sidebar-open');
|
|
|
|
if (deltaY > MAX_VERTICAL_DRIFT && Math.abs(deltaX) < deltaY) {
|
|
resetPointer();
|
|
return;
|
|
}
|
|
|
|
if (!open && deltaX > SWIPE_TRIGGER_DISTANCE) {
|
|
openSidebar();
|
|
resetPointer();
|
|
return;
|
|
}
|
|
|
|
if (open && deltaX < -SWIPE_TRIGGER_DISTANCE) {
|
|
closeSidebar();
|
|
resetPointer();
|
|
}
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('touchend', resetPointer);
|
|
document.addEventListener('touchcancel', resetPointer);
|
|
}
|
|
|
|
applyScrollLock();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|