Improve tasks UI, dark theme, performance fixes, and admin pages polish
This commit is contained in:
@@ -3,21 +3,17 @@
|
||||
use Morilog\Jalali\Jalalian;
|
||||
use Carbon\Carbon;
|
||||
|
||||
// پیدا کردن نام ماه قبل به تاریخ شمسی (مثلا اگر الان اسفند است، خروجی "بهمن" خواهد بود)
|
||||
$previousMonthName = Jalalian::now()->subMonths(1)->format('%B');
|
||||
|
||||
// محاسبه مجموع کارکرد برای هر کارمند به تفکیک ماه
|
||||
$employeeTotals = [];
|
||||
|
||||
foreach ($data as $employee) {
|
||||
// تبدیل آبجکت به آرایه در صورت نیاز برای حلقه
|
||||
$recordsGrouped = is_array($employee->records) ? $employee->records : (array)$employee->records;
|
||||
$monthlyTotals = [];
|
||||
|
||||
foreach ($recordsGrouped as $month => $attendances) {
|
||||
$tsec = 0;
|
||||
foreach ($attendances as $attendance) {
|
||||
// اگر duration موجود باشه استفاده میکنیم
|
||||
if (!empty($attendance->duration)) {
|
||||
[$h, $m, $s] = explode(':', $attendance->duration);
|
||||
$tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s;
|
||||
@@ -26,29 +22,239 @@ foreach ($data as $employee) {
|
||||
|
||||
$hours = floor($tsec / 3600);
|
||||
$minutes = floor(($tsec % 3600) / 60);
|
||||
|
||||
// ذخیره زمان برای این ماه
|
||||
$monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes);
|
||||
}
|
||||
// ذخیره کل ماهها برای این کارمند
|
||||
$employeeTotals[$employee->id] = $monthlyTotals;
|
||||
}
|
||||
?>
|
||||
|
||||
@extends('app')
|
||||
@section('content')
|
||||
|
||||
<div class="container flex flex-col items-center justify-center mx-auto pb-10">
|
||||
@section('style')
|
||||
<style>
|
||||
.admin-show-page,
|
||||
.ui-page {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
padding: 0 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.admin-show-empty,
|
||||
.ui-empty {
|
||||
margin-top: 5rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.employee-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
margin: 0.75rem 0 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--surface-glass);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-soft);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.emp-btn {
|
||||
padding: 0.5rem 0.85rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--border-input);
|
||||
background: var(--surface-input);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.emp-btn:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-tab-hover);
|
||||
border-color: color-mix(in srgb, var(--primary-color) 35%, var(--border-input));
|
||||
}
|
||||
|
||||
.emp-btn-active {
|
||||
color: #fff;
|
||||
background: var(--primary-color);
|
||||
border-color: color-mix(in srgb, var(--primary-color) 80%, white);
|
||||
box-shadow: 0 6px 18px color-mix(in srgb, var(--primary-color) 35%, transparent);
|
||||
}
|
||||
|
||||
.employee-panel {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.employee-panel.is-visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.employee-header {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.employee-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.employee-stat {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
padding: 0.55rem 0.85rem;
|
||||
border-radius: var(--radius-lg);
|
||||
background: color-mix(in srgb, var(--primary-color) 12%, var(--surface-card));
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color) 28%, var(--border-subtle));
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.employee-stat strong {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 800;
|
||||
color: color-mix(in srgb, var(--primary-color) 70%, var(--text-primary));
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.month-tabs {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
overflow-x: auto;
|
||||
padding: 0.45rem;
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
background: var(--surface-tabs);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-bottom: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.month-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.month-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem 0.85rem;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.month-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-tab-hover);
|
||||
}
|
||||
|
||||
.month-tab-active {
|
||||
color: color-mix(in srgb, var(--primary-color) 78%, var(--text-primary));
|
||||
background: var(--surface-tab-active);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.attendance-card {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
background: var(--surface-glass);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
|
||||
box-shadow: var(--shadow-soft);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.attendance-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.attendance-table thead th {
|
||||
padding: 0.65rem 0.35rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
background: var(--surface-tabs);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.attendance-table thead th:not(:last-child) {
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.attendance-table tbody td {
|
||||
padding: 0.5rem 0.25rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.attendance-table tbody td:not(:last-child) {
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.attendance-table tbody tr:hover td {
|
||||
background: var(--surface-tab-hover);
|
||||
}
|
||||
|
||||
.attendance-table tfoot td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
background: color-mix(in srgb, var(--primary-color) 10%, var(--surface-card));
|
||||
border-top: 2px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.panel-empty {
|
||||
margin: 2.5rem 0;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="admin-show-page">
|
||||
|
||||
@if(empty($data) || count($data) == 0)
|
||||
<div class="mt-80 text-gray-500 text-xl font-bold italic">
|
||||
<div class="admin-show-empty">
|
||||
<h1>اطلاعاتی موجود نمیباشد</h1>
|
||||
</div>
|
||||
@else
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-2 mt-10 mb-6 max-w-3xl">
|
||||
<div class="employee-picker">
|
||||
@foreach($data as $employee)
|
||||
<button class="emp-btn px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-cyan-500 hover:text-white transition-colors duration-300 font-bold shadow-sm"
|
||||
<button type="button"
|
||||
class="emp-btn"
|
||||
data-emp-id="{{ $employee->id }}"
|
||||
onclick="openEmployee(event, 'emp_{{ $employee->id }}', '{{ $employee->id }}')">
|
||||
{{ $employee->name }}
|
||||
</button>
|
||||
@@ -56,15 +262,13 @@ foreach ($data as $employee) {
|
||||
</div>
|
||||
|
||||
@foreach($data as $employee)
|
||||
<div id="emp_{{ $employee->id }}" class="employee-content hidden flex-col items-center justify-center w-full">
|
||||
|
||||
<div class="flex justify-between items-center mb-4 w-[350px]">
|
||||
<h2 class="text-xl font-bold text-cyan-700">{{ $employee->name }}</h2>
|
||||
|
||||
<span class="text-xs bg-cyan-50 text-cyan-800 px-3 py-1.5 rounded-full border border-cyan-200 shadow-sm flex items-center gap-1">
|
||||
<div id="emp_{{ $employee->id }}" class="employee-panel">
|
||||
<div class="employee-header">
|
||||
<h2>{{ $employee->name }}</h2>
|
||||
<div class="employee-stat">
|
||||
<span>کارکرد ماه قبل ({{ $previousMonthName }}):</span>
|
||||
<strong class="text-sm font-black">{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong>
|
||||
</span>
|
||||
<strong>{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
@@ -72,13 +276,14 @@ foreach ($data as $employee) {
|
||||
@endphp
|
||||
|
||||
@if(empty($recordsGrouped))
|
||||
<div class="my-10 text-gray-500 text-lg font-bold italic">
|
||||
<div class="panel-empty">
|
||||
رکورد ترددی برای این شخص ثبت نشده است.
|
||||
</div>
|
||||
@else
|
||||
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex overflow-x-auto whitespace-nowrap hide-scrollbar" style="max-width: 350px">
|
||||
<div class="month-tabs">
|
||||
@foreach(array_keys($recordsGrouped) as $month)
|
||||
<button class="tablinks-{{ $employee->id }} active rounded-t-2xl cursor-pointer px-4 py-2 bg-gray-100 border-b-2 border-transparent hover:bg-gray-200 flex-shrink-0"
|
||||
<button type="button"
|
||||
class="month-tab tablinks-{{ $employee->id }}"
|
||||
onclick="openMonth(event, 'month_{{ $employee->id }}_{{ $month }}', '{{ $employee->id }}')">
|
||||
{{ $month }}
|
||||
</button>
|
||||
@@ -86,43 +291,37 @@ foreach ($data as $employee) {
|
||||
</div>
|
||||
|
||||
@foreach($recordsGrouped as $month => $records)
|
||||
<div id="month_{{ $employee->id }}_{{ $month }}" class="tabcontent-{{ $employee->id }} hidden w-[350px] bg-white rounded-b-xl rounded-tl-2xl cursor-default leading-7 shadow-lg border border-gray-100">
|
||||
<table style="width:100%; border-collapse: collapse; text-align: center">
|
||||
<div id="month_{{ $employee->id }}_{{ $month }}"
|
||||
class="attendance-card tabcontent-{{ $employee->id }}"
|
||||
style="display: none;">
|
||||
<table class="attendance-table">
|
||||
<thead>
|
||||
<tr style="border-bottom: 1px solid #ddd; background-color: #f8f9fa;">
|
||||
<th style="border-right: 1px solid #ddd; padding: 8px 0;">تاریخ</th>
|
||||
<th style="border-right: 1px solid #ddd;">روز</th>
|
||||
<th style="border-right: 1px solid #ddd;">ورود</th>
|
||||
<th style="border-right: 1px solid #ddd;">خروج</th>
|
||||
<th style="border-right: 1px solid #ddd;">مدت</th>
|
||||
<tr>
|
||||
<th>تاریخ</th>
|
||||
<th>روز</th>
|
||||
<th>ورود</th>
|
||||
<th>خروج</th>
|
||||
<th>مدت</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($records as $row)
|
||||
<tr class="hover:bg-cyan-100 transition-colors" style="border-bottom: 1px solid #ddd;">
|
||||
<td style="border-right: 1px solid #ddd; padding: 4px 0;">
|
||||
{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}
|
||||
</td>
|
||||
<td style="border-right: 1px solid #ddd;">
|
||||
{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('%A') ?? '-' }}
|
||||
</td>
|
||||
<td style="border-right: 1px solid #ddd;">
|
||||
{{ \Carbon\Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}
|
||||
</td>
|
||||
<td style="border-right: 1px solid #ddd;">
|
||||
{{ $row->exited_at ? \Carbon\Carbon::parse($row->exited_at)->format('H:i') : '-' }}
|
||||
</td>
|
||||
<td style="border-right: 1px solid #ddd;">
|
||||
{{ $row->duration ? \Carbon\Carbon::parse($row->duration)->format('H:i') : '-' }}
|
||||
</td>
|
||||
<tr>
|
||||
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}</td>
|
||||
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('%A') ?? '-' }}</td>
|
||||
<td>{{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
|
||||
<td>{{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
|
||||
<td>{{ $row->duration ? Carbon::parse($row->duration)->format('H:i') : '-' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="5" class="bg-cyan-50 font-bold text-gray-700 rounded-b-xl" style="border-top: 2px solid #ddd; text-align: center; padding: 10px 0;">
|
||||
<td colspan="5">
|
||||
مجموع کارکرد این ماه: {{ $employeeTotals[$employee->id][$month] ?? '00:00' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
@endforeach
|
||||
@@ -132,58 +331,46 @@ foreach ($data as $employee) {
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* مخفی کردن اسکرولبار برای تبهای ماهها اگر تعدادشان زیاد شد */
|
||||
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function openEmployee(evt, empContainerId, empId) {
|
||||
let employeeContents = document.getElementsByClassName("employee-content");
|
||||
for (let i = 0; i < employeeContents.length; i++) {
|
||||
employeeContents[i].style.display = "none";
|
||||
document.querySelectorAll('.employee-panel').forEach(el => el.classList.remove('is-visible'));
|
||||
document.querySelectorAll('.emp-btn').forEach(btn => btn.classList.remove('emp-btn-active'));
|
||||
|
||||
const panel = document.getElementById(empContainerId);
|
||||
if (panel) panel.classList.add('is-visible');
|
||||
|
||||
if (evt.currentTarget) {
|
||||
evt.currentTarget.classList.add('emp-btn-active');
|
||||
}
|
||||
|
||||
let empBtns = document.getElementsByClassName("emp-btn");
|
||||
for (let i = 0; i < empBtns.length; i++) {
|
||||
empBtns[i].classList.remove("bg-cyan-500", "text-white", "shadow-md");
|
||||
empBtns[i].classList.add("bg-gray-200", "text-gray-700");
|
||||
}
|
||||
|
||||
document.getElementById(empContainerId).style.display = "flex";
|
||||
evt.currentTarget.classList.remove("bg-gray-200", "text-gray-700");
|
||||
evt.currentTarget.classList.add("bg-cyan-500", "text-white", "shadow-md");
|
||||
|
||||
let firstMonthTab = document.querySelector("#" + empContainerId + " .tablinks-" + empId);
|
||||
const firstMonthTab = document.querySelector('#' + empContainerId + ' .tablinks-' + empId);
|
||||
if (firstMonthTab) {
|
||||
firstMonthTab.click();
|
||||
}
|
||||
}
|
||||
|
||||
function openMonth(evt, monthId, empId) {
|
||||
let tabcontents = document.getElementsByClassName("tabcontent-" + empId);
|
||||
for (let i = 0; i < tabcontents.length; i++) {
|
||||
tabcontents[i].style.display = "none";
|
||||
}
|
||||
document.querySelectorAll('.tabcontent-' + empId).forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
|
||||
let tablinks = document.getElementsByClassName("tablinks-" + empId);
|
||||
for (let i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].classList.remove("bg-white", "border-cyan-500");
|
||||
tablinks[i].classList.add("bg-gray-100", "border-transparent");
|
||||
}
|
||||
document.querySelectorAll('.tablinks-' + empId).forEach(btn => {
|
||||
btn.classList.remove('month-tab-active');
|
||||
});
|
||||
|
||||
document.getElementById(monthId).style.display = "block";
|
||||
evt.currentTarget.classList.remove("bg-gray-100", "border-transparent");
|
||||
evt.currentTarget.classList.add("bg-white", "border-cyan-500");
|
||||
const monthPanel = document.getElementById(monthId);
|
||||
if (monthPanel) monthPanel.style.display = 'block';
|
||||
|
||||
if (evt.currentTarget) {
|
||||
evt.currentTarget.classList.add('month-tab-active');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let firstEmployeeBtn = document.querySelector(".emp-btn");
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const firstEmployeeBtn = document.querySelector('.emp-btn');
|
||||
if (firstEmployeeBtn) {
|
||||
firstEmployeeBtn.click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
Reference in New Issue
Block a user