all feature
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Thêm User Mới')
|
||||
|
||||
@section('content')
|
||||
<div class="form-container">
|
||||
<div class="form-header">
|
||||
<h3 class="form-header-title">Thêm User Mới</h3>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.users.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="form-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="msnv">Mã nhân viên (MSNV) <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
<span class="form-input-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5zm6-10.125a1.875 1.875 0 11-3.75 0 1.875 1.875 0 013.75 0zm1.294 6.336a6.721 6.721 0 01-3.17.789 6.721 6.721 0 01-3.168-.789 3.376 3.376 0 016.338 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="msnv" id="msnv" class="form-input form-input-with-icon" required value="{{ old('msnv') }}" placeholder="VD: RUN-0123">
|
||||
</div>
|
||||
@error('msnv')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="mail">Email <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
<span class="form-input-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="email" name="mail" id="mail" class="form-input form-input-with-icon" required value="{{ old('mail') }}" placeholder="VD: nguyenva@runsystem.net">
|
||||
</div>
|
||||
@error('mail')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Mật khẩu khởi tạo <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
<span class="form-input-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0V10.5m-2.812 10.5h14.625c.621 0 1.125-.504 1.125-1.125V11.25c0-.621-.504-1.125-1.125-1.125H3.75c-.621 0-1.125.504-1.125 1.125v7.875c0 .621.504 1.125 1.125 1.125z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="password" id="password" class="form-input form-input-with-icon" required placeholder="Nhập mật khẩu cho user...">
|
||||
</div>
|
||||
<span class="helper-text">Lưu ý: User sẽ bị buộc phải đổi mật khẩu này ở lần đăng nhập đầu tiên.</span>
|
||||
@error('password')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label" for="role">Quyền hạn <span class="text-red-500">*</span></label>
|
||||
<select name="role" id="role" class="form-input" required>
|
||||
<option value="{{ \App\Models\User::ROLE_MEMBER }}" {{ old('role') == \App\Models\User::ROLE_MEMBER ? 'selected' : '' }}>Nhân viên (Member)</option>
|
||||
<option value="{{ \App\Models\User::ROLE_ADMIN }}" {{ old('role') == \App\Models\User::ROLE_ADMIN ? 'selected' : '' }}>Quản trị viên (Admin)</option>
|
||||
</select>
|
||||
@error('role')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-footer">
|
||||
<a href="{{ route('admin.users.index') }}" class="btn-secondary">Hủy bỏ</a>
|
||||
<button type="submit" class="btn-primary">Tạo Nhân Viên</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,116 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Chi tiết User')
|
||||
|
||||
@section('content')
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
||||
<!-- Cột trái: Form thao tác -->
|
||||
<div class="col-span-1">
|
||||
<div class="form-container">
|
||||
<div class="form-header">
|
||||
<h3 class="form-header-title">Quản Lý Thẻ: {{ $user->msnv }}</h3>
|
||||
</div>
|
||||
<div class="form-body">
|
||||
<div class="mb-6 pb-6 border-b border-border-light space-y-3">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-text-medium font-bold">Email:</span>
|
||||
<span class="font-semibold text-text-dark">{{ $user->mail }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-text-medium font-bold">Role:</span>
|
||||
<span class="font-semibold text-text-dark">{{ $user->role == \App\Models\User::ROLE_ADMIN ? 'Admin' : 'Member' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-text-medium font-bold">Số dư thẻ:</span>
|
||||
<span class="font-extrabold text-orange-500 text-xl">{{ $user->card }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.users.update', $user->msnv) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="form-body pt-0">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="num_card">Nạp thêm thẻ</label>
|
||||
<input type="number" name="num_card" id="num_card" min="1" class="form-input" placeholder="Nhập số lượng thẻ muốn nạp...">
|
||||
@error('num_card')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="flex items-center gap-3 cursor-pointer p-4 border border-[#d9dfe7] rounded-lg hover:bg-gray-50 hover:border-gray-400 transition-colors shadow-sm">
|
||||
<input type="checkbox" name="flag_send" id="flag_send" value="1" class="form-input !w-5 !h-5 !min-h-0" {{ $user->flag_send ? 'checked' : '' }}>
|
||||
<span class="text-[16px] font-[600] text-text-dark">Bật quyền cho phép User gửi thẻ</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-footer">
|
||||
<a href="{{ route('admin.users.index') }}" class="btn-secondary">Quay lại</a>
|
||||
<button type="submit" class="btn-primary">Lưu Thay Đổi</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cột phải: Lịch sử -->
|
||||
<div class="col-span-1 lg:col-span-2">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-border-light overflow-hidden flex flex-col h-full">
|
||||
<div class="px-6 py-4 sm:py-5 border-b border-border-light bg-gray-50/50 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<h3 class="text-lg font-bold text-text-dark">Lịch sử nhận / gửi</h3>
|
||||
<form action="{{ route('admin.users.edit', $user->msnv) }}" method="GET" class="flex-shrink-0">
|
||||
<input type="month" name="month" value="{{ $selectedMonth }}" onchange="this.form.submit()" class="form-input !min-h-[38px] !py-1.5 !text-sm w-auto border-border-medium rounded">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="p-0 overflow-y-auto max-h-[600px]">
|
||||
<table class="min-w-full divide-y divide-border-light text-sm">
|
||||
<thead class="bg-gray-50/80 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Ngày</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Hành động</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Đối tượng</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Số lượng</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-border-light">
|
||||
@forelse($administrations as $admin_record)
|
||||
@php
|
||||
$isReceiver = $admin_record->msnv == $user->msnv && $admin_record->received > 0;
|
||||
@endphp
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-3 whitespace-nowrap text-text-medium font-medium">{{ Carbon\Carbon::parse($admin_record->date)->format('d/m/Y') }}</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap">
|
||||
@if($isReceiver)
|
||||
<span class="px-2 py-1 inline-flex text-xs leading-5 font-bold rounded-full bg-green-100 text-green-800">Nhận thẻ</span>
|
||||
@else
|
||||
<span class="px-2 py-1 inline-flex text-xs leading-5 font-bold rounded-full bg-blue-100 text-blue-800">Gửi thẻ</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap font-bold text-text-dark">
|
||||
@if($isReceiver)
|
||||
Từ: <span class="text-primary">{{ $admin_record->sender }}</span>
|
||||
@else
|
||||
Tới: <span class="text-primary">{{ $admin_record->receiver }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap font-extrabold text-lg">
|
||||
@if($isReceiver)
|
||||
<span class="text-green-600">+{{ $admin_record->received }}</span>
|
||||
@else
|
||||
<span class="text-blue-600">-{{ $admin_record->sent }}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-12 text-center text-text-light font-medium text-base">Không có giao dịch nào trong tháng này.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,258 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Quản lý Users')
|
||||
|
||||
@section('content')
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<h2 class="text-xl font-bold text-text-dark flex items-center gap-2">
|
||||
Danh sách User (Tháng {{ Carbon\Carbon::parse($selectedMonth)->format('m/Y') }})
|
||||
</h2>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<form id="filterForm" action="{{ route('admin.users.index') }}" method="GET" class="flex items-center gap-2">
|
||||
<input type="month" name="month" value="{{ $selectedMonth }}" class="form-input !py-1.5 !text-sm w-auto border-border-medium rounded">
|
||||
<button type="submit" id="btnFilter" class="btn-primary !py-1.5 !px-4 text-sm rounded shadow-sm hover:translate-y-[-1px] flex items-center justify-center min-w-[70px]">Lọc</button>
|
||||
</form>
|
||||
|
||||
<form action="{{ route('admin.reset_cards') }}" method="POST" onsubmit="return confirm('Bạn có chắc chắn muốn reset toàn bộ số card hiện tại của các user về 0? Hành động này thường chỉ thực hiện vào cuối tháng.');">
|
||||
@csrf
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white font-bold py-1.5 px-4 rounded text-sm shadow-md transition-colors">Reset Card Tháng</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="card-stat-active !bg-[#0f4c81]">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-white/90 text-xs font-bold uppercase tracking-wider">User nhận nhiều nhất</span>
|
||||
<span class="text-2xl font-black">{{ $topReceivedUser && $topReceivedUser->total_received > 0 ? $topReceivedUser->msnv : 'Chưa có' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center pt-2 border-t border-white/20">
|
||||
<span class="text-white/80 text-[10px] font-bold">Tổng thẻ đã nhận</span>
|
||||
<span class="text-lg font-bold text-yellow-300">{{ $topReceivedUser ? $topReceivedUser->total_received : 0 }} thẻ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-stat-active !bg-[#ef222e]">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-white/90 text-xs font-bold uppercase tracking-wider">User gửi nhiều nhất</span>
|
||||
<span class="text-2xl font-black">{{ $topSentUser && $topSentUser->total_sent > 0 ? $topSentUser->msnv : 'Chưa có' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center pt-2 border-t border-white/20">
|
||||
<span class="text-white/80 text-[10px] font-bold">Tổng thẻ đã gửi</span>
|
||||
<span class="text-lg font-bold text-yellow-300">{{ $topSentUser ? $topSentUser->total_sent : 0 }} thẻ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-sm rounded-xl overflow-hidden border border-border-light mt-2">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-border-light text-sm">
|
||||
<thead class="bg-gray-50/80">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">MSNV</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Email</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Đã Nhận</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Đã Gửi</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Số dư thẻ</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Quyền gửi</th>
|
||||
<th class="px-6 py-4 text-right text-xs font-bold text-text-light uppercase tracking-wider">Thao tác</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-border-light">
|
||||
@forelse($users as $u)
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-3 whitespace-nowrap font-bold text-text-dark">{{ $u->msnv }}</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap text-text-medium">{{ $u->mail }}</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap font-extrabold text-green-600">+{{ $u->total_received }}</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap font-extrabold text-blue-600">{{ $u->total_sent }}</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap font-extrabold text-orange-500">{{ $u->card }}</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap">
|
||||
@if($u->flag_send)
|
||||
<span class="px-2 py-1 inline-flex text-[10px] leading-5 font-semibold rounded-full bg-green-100 text-green-800">Được gửi</span>
|
||||
@else
|
||||
<span class="px-2 py-1 inline-flex text-[10px] leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">Không</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap text-right text-sm font-medium space-x-3">
|
||||
<a href="{{ route('admin.users.edit', $u->msnv) }}" class="text-primary hover:text-[#0f4c81] font-semibold underline underline-offset-2">Quản lý</a>
|
||||
<button type="button" onclick="openDeleteModal('{{ $u->msnv }}', '{{ route('admin.users.destroy', $u->msnv) }}')" class="text-red-500 hover:text-red-700 font-semibold underline underline-offset-2">Nghỉ việc</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-10 text-center text-text-light font-medium">Chưa có user nào đang hoạt động.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if($users->hasPages())
|
||||
<div class="px-6 py-4 border-t border-border-light bg-gray-50/30">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Modal Xác nhận Nghỉ việc -->
|
||||
<x-modal id="deleteModal" title="Xác nhận nghỉ việc" maxWidth="md" hideHeader="true">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-12 h-12 bg-red-100 rounded-full">
|
||||
<svg class="w-6 h-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 mt-1">
|
||||
<h3 class="text-lg font-bold text-gray-900" id="modal-title">Xác nhận nghỉ việc</h3>
|
||||
<p class="text-sm text-gray-600 mt-2 leading-relaxed">
|
||||
Bạn có chắc chắn muốn đánh dấu nhân viên <span id="deleteMsnv" class="font-extrabold text-red-600"></span> đã nghỉ việc?<br><br>
|
||||
Thao tác này sẽ ngăn user đăng nhập vào hệ thống, nhưng dữ liệu lịch sử vẫn được giữ lại.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-slot name="footer">
|
||||
<form id="deleteForm" method="POST" class="w-full sm:w-auto">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="w-full btn-primary !bg-red-600 hover:!bg-red-700 focus:!ring-red-500 !px-6 shadow-sm">Xác nhận Nghỉ việc</button>
|
||||
</form>
|
||||
<button type="button" onclick="closeModal('deleteModal')" class="w-full sm:w-auto btn-secondary !px-6 shadow-sm">Hủy bỏ</button>
|
||||
</x-slot>
|
||||
</x-modal>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function openDeleteModal(msnv, url) {
|
||||
document.getElementById('deleteMsnv').innerText = msnv;
|
||||
document.getElementById('deleteForm').action = url;
|
||||
openModal('deleteModal');
|
||||
}
|
||||
|
||||
document.getElementById('filterForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnFilter');
|
||||
|
||||
btn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
btn.disabled = true;
|
||||
|
||||
const tableBody = document.querySelector('.bg-white.shadow-sm tbody');
|
||||
if(tableBody) {
|
||||
let skeletonHtml = '';
|
||||
for(let i=0; i<5; i++) {
|
||||
skeletonHtml += `
|
||||
<tr class="animate-pulse">
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-16"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-48"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-12"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-12"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-16"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-20"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-6 bg-gray-200 rounded-full w-20"></div></td>
|
||||
<td class="px-6 py-4 flex justify-end gap-3"><div class="h-8 bg-gray-200 rounded w-20"></div></td>
|
||||
</tr>`;
|
||||
}
|
||||
tableBody.innerHTML = skeletonHtml;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = this.action + '?' + new URLSearchParams(new FormData(this)).toString();
|
||||
|
||||
const [res] = await Promise.all([
|
||||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }),
|
||||
new Promise(resolve => setTimeout(resolve, 200))
|
||||
]);
|
||||
|
||||
if(!res.ok) throw new Error('Network response was not ok');
|
||||
const html = await res.text();
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const updateDOM = () => {
|
||||
const currentTitle = document.querySelector('h2.text-xl');
|
||||
const newTitle = doc.querySelector('h2.text-xl');
|
||||
if(currentTitle && newTitle) currentTitle.innerHTML = newTitle.innerHTML;
|
||||
|
||||
const currentGrid = document.querySelector('.grid.grid-cols-1');
|
||||
const newGrid = doc.querySelector('.grid.grid-cols-1');
|
||||
if(currentGrid && newGrid) currentGrid.innerHTML = newGrid.innerHTML;
|
||||
|
||||
const currentTable = document.querySelector('.bg-white.shadow-sm');
|
||||
const newTable = doc.querySelector('.bg-white.shadow-sm');
|
||||
if(currentTable && newTable) currentTable.innerHTML = newTable.innerHTML;
|
||||
};
|
||||
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(updateDOM);
|
||||
} else {
|
||||
updateDOM();
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
} catch(err) {
|
||||
console.error('Error fetching data:', err);
|
||||
window.location.reload();
|
||||
} finally {
|
||||
btn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Intercept pagination clicks for AJAX loading
|
||||
document.addEventListener('click', async function(e) {
|
||||
const link = e.target.closest('.bg-white.shadow-sm nav a');
|
||||
if (!link) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const tableBody = document.querySelector('.bg-white.shadow-sm tbody');
|
||||
if(tableBody) {
|
||||
let skeletonHtml = '';
|
||||
for(let i=0; i<5; i++) {
|
||||
skeletonHtml += `
|
||||
<tr class="animate-pulse">
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-16"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-48"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-12"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-12"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-16"></div></td>
|
||||
<td class="px-6 py-4 flex justify-end gap-3"><div class="h-8 bg-gray-200 rounded w-20"></div></td>
|
||||
</tr>`;
|
||||
}
|
||||
tableBody.innerHTML = skeletonHtml;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = link.href;
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
if(!res.ok) throw new Error('Network response was not ok');
|
||||
const html = await res.text();
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const updateDOM = () => {
|
||||
const currentTable = document.querySelector('.bg-white.shadow-sm');
|
||||
const newTable = doc.querySelector('.bg-white.shadow-sm');
|
||||
if(currentTable && newTable) currentTable.innerHTML = newTable.innerHTML;
|
||||
};
|
||||
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(updateDOM);
|
||||
} else {
|
||||
updateDOM();
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', url);
|
||||
} catch(err) {
|
||||
console.error('Error fetching pagination data:', err);
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -0,0 +1,45 @@
|
||||
@props(['id', 'maxWidth' => 'md', 'title' => 'Thông báo', 'hideHeader' => false])
|
||||
|
||||
@php
|
||||
$maxWidthClass = [
|
||||
'sm' => 'sm:max-w-sm',
|
||||
'md' => 'sm:max-w-md',
|
||||
'lg' => 'sm:max-w-lg',
|
||||
'xl' => 'sm:max-w-xl',
|
||||
'2xl' => 'sm:max-w-2xl',
|
||||
][$maxWidth] ?? 'sm:max-w-md';
|
||||
@endphp
|
||||
|
||||
@push('modals')
|
||||
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
|
||||
<!-- Backdrop overlay -->
|
||||
<div class="absolute inset-0 bg-gray-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
|
||||
|
||||
<!-- Modal Panel -->
|
||||
<div class="modal-panel bg-white rounded-xl shadow-2xl w-full {{ $maxWidthClass }} overflow-hidden z-50 relative transform scale-95 translate-y-4 transition-all duration-300 m-4 flex flex-col max-h-[90vh]">
|
||||
|
||||
@if(!$hideHeader)
|
||||
@if(isset($header))
|
||||
{{ $header }}
|
||||
@else
|
||||
<div class="px-6 py-5 border-b border-gray-100 flex items-center justify-between bg-gray-50/50 shrink-0">
|
||||
<h3 id="modal-title-{{ $id }}" class="text-lg font-bold text-gray-900">{{ $title }}</h3>
|
||||
<button type="button" onclick="closeModal('{{ $id }}')" class="text-gray-400 hover:text-gray-600 transition-colors focus:outline-none">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div class="overflow-y-auto px-6 py-6 grow">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
@if(isset($footer))
|
||||
<div class="px-6 py-4 bg-gray-50 flex flex-col sm:flex-row-reverse gap-3 border-t border-gray-100 shrink-0">
|
||||
{{ $footer }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
@@ -0,0 +1,419 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>GMO-Z.com RUNSYSTEM - Dashboard</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-bg-main text-text-dark font-sans antialiased">
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar">
|
||||
<div class="flex flex-col h-full justify-between">
|
||||
<div>
|
||||
<div class="h-16 flex items-center justify-between px-6 border-b border-border-light">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="{{ asset('images/logo.png') }}" alt="GMO-Z.com RUNSYSTEM" class="h-8 w-auto object-contain">
|
||||
</div>
|
||||
<button class="text-text-light hover:text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="p-4 space-y-1">
|
||||
<a href="#" class="sidebar-link sidebar-link-active">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
</svg>
|
||||
<span>DashBoard</span>
|
||||
</a>
|
||||
|
||||
<div class="relative">
|
||||
<a href="#" class="sidebar-link justify-between">
|
||||
<span class="flex items-center gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Chấm công</span>
|
||||
</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="#" class="sidebar-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
|
||||
</svg>
|
||||
<span>Danh sách khóa học</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mx-4 my-2 p-4 rounded-lg bg-primary/5 border border-primary/10 flex flex-col items-center text-center">
|
||||
<img src="https://illustrations.popsy.co/blue/document-delivery.svg" alt="Support" class="w-24 h-24 object-contain mb-2">
|
||||
<h4 class="text-text-dark font-semibold text-xs mb-1">Bạn có thắc mắc về ngày công?</h4>
|
||||
<a href="#" class="btn-primary py-1.5 px-4 text-xs w-full mt-2 inline-block">Phản hồi</a>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-border-light text-center">
|
||||
<span class="text-text-muted text-[10px]">© 2026 GMO-Z.com RUNSYSTEM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="top-header">
|
||||
<div class="flex items-center gap-3 overflow-x-auto py-2">
|
||||
<span class="text-text-light text-xs font-bold whitespace-nowrap">Link liên kết:</span>
|
||||
<a href="#" class="border border-border-medium rounded px-2.5 py-1 text-xs text-text-medium hover:text-primary relative font-medium whitespace-nowrap">
|
||||
RUN AI <span class="nav-badge">NEW!</span>
|
||||
</a>
|
||||
<a href="#" class="border border-border-medium rounded px-2.5 py-1 text-xs text-text-medium hover:text-primary relative font-medium whitespace-nowrap">
|
||||
G-Office <span class="nav-badge">NEW!</span>
|
||||
</a>
|
||||
<a href="#" class="border border-border-medium rounded px-2.5 py-1 text-xs text-text-medium hover:text-primary relative font-medium whitespace-nowrap">
|
||||
RUN Helpline <span class="nav-badge">NEW!</span>
|
||||
</a>
|
||||
<a href="#" class="border border-border-medium rounded px-2.5 py-1 text-xs text-text-medium hover:text-primary font-medium whitespace-nowrap">JIRA8</a>
|
||||
<a href="#" class="border border-border-medium rounded px-2.5 py-1 text-xs text-text-medium hover:text-primary font-medium whitespace-nowrap">WE</a>
|
||||
<a href="#" class="border border-border-medium rounded px-2.5 py-1 text-xs text-text-medium hover:text-primary font-medium whitespace-nowrap">G-Help</a>
|
||||
<a href="#" class="border border-border-medium rounded px-2.5 py-1 text-xs text-text-medium hover:text-primary font-medium whitespace-nowrap">G-Booking</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 shrink-0">
|
||||
<button class="relative text-text-medium hover:text-primary p-1.5 rounded-full hover:bg-gray-100 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
|
||||
</svg>
|
||||
<span class="notification-badge">1</span>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-2 border-l border-border-light pl-4">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-light text-primary flex items-center justify-center font-bold text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-primary">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-text-dark">TRẦN VĂN AN</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<h2 class="text-xl font-bold text-text-dark flex items-center gap-2">
|
||||
Bảng chấm công
|
||||
</h2>
|
||||
<div class="flex flex-wrap items-center gap-4 text-xs font-semibold">
|
||||
<a href="#" class="text-secondary hover:underline flex items-center gap-1.5">
|
||||
👉 Quy định chấm công, nghỉ phép
|
||||
</a>
|
||||
<a href="#" class="text-secondary hover:underline flex items-center gap-1.5">
|
||||
👉 Quy định chấm công, nghỉ phép HCM
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="card-stat-active">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-white/80 text-xs font-bold">Ngày công chuẩn</span>
|
||||
<span class="text-xl font-bold">22</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center pt-2 border-t border-white/10">
|
||||
<span class="text-white/80 text-xs font-bold">Ngày công lễ</span>
|
||||
<span class="text-xl font-bold">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-stat">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-text-medium text-xs font-bold">Ngày công thực tế</span>
|
||||
<span class="text-xl font-bold">20</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center pt-2 border-t border-border-light">
|
||||
<span class="text-text-medium text-xs font-bold">Nghỉ phép</span>
|
||||
<span class="text-xl font-bold">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-stat">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-text-medium text-xs font-bold">Số lần đi muộn, về sớm</span>
|
||||
<span class="text-xl font-bold text-red-500">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center pt-2 border-t border-border-light">
|
||||
<span class="text-text-medium text-xs font-bold">Số giờ đi muộn, về sớm</span>
|
||||
<span class="text-xl font-bold">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-stat">
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-text-medium text-xs font-bold">Số giờ OT 150%</span>
|
||||
<span class="text-sm font-bold">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-text-medium text-xs font-bold">Số giờ OT 200%</span>
|
||||
<span class="text-sm font-bold">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-text-medium text-xs font-bold">Số giờ OT 300%</span>
|
||||
<span class="text-sm font-bold">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4 text-primary font-bold">
|
||||
<button class="p-1 hover:bg-gray-100 rounded transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-lg">Tháng 06/2026</span>
|
||||
<button class="p-1 hover:bg-gray-100 rounded transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center border border-border-medium rounded-lg overflow-hidden bg-white">
|
||||
<button class="p-2 bg-primary-light text-primary hover:bg-primary/10 transition-colors border-r border-border-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="p-2 hover:bg-gray-50 text-text-medium transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12M8.25 17.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 17.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-card">
|
||||
<div class="calendar-header">
|
||||
<div>Thứ 2</div>
|
||||
<div>Thứ 3</div>
|
||||
<div>Thứ 4</div>
|
||||
<div>Thứ 5</div>
|
||||
<div>Thứ 6</div>
|
||||
<div>Thứ 7</div>
|
||||
<div>CN</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">1</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:45 - 17:13</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:28</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">2</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:53 - 17:08</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:15</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">3</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:54 - 17:16</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:22</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">4</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:52 - 17:04</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:12</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">5</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:50 - 17:20</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:30</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day calendar-day-weekend">
|
||||
<div class="flex justify-end">
|
||||
<span class="text-xs font-semibold text-text-light">6</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="calendar-day calendar-day-weekend">
|
||||
<div class="flex justify-end">
|
||||
<span class="text-xs font-semibold text-text-light">7</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">8</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:54 - 17:05</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:11</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">9</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:48 - 17:05</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:17</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">10</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:51 - 17:05</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:14</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">11</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:45 - 17:13</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:28</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">12</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:45 - 17:37</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:52</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day calendar-day-weekend">
|
||||
<div class="flex justify-end">
|
||||
<span class="text-xs font-semibold text-text-light">13</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="calendar-day calendar-day-weekend">
|
||||
<div class="flex justify-end">
|
||||
<span class="text-xs font-semibold text-text-light">14</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">15</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:47 - 17:04</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:17</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">16</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:45 - 17:02</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:17</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">17</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:54 - 17:02</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:08</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">18</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:45 - 17:03</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:18</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="text-lg font-extrabold text-primary">X</span>
|
||||
<span class="text-xs font-semibold text-text-light">19</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs font-bold text-text-dark">07:45 - 17:04</div>
|
||||
<div class="text-[10px] font-bold text-primary">+ 0:19</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-day calendar-day-weekend">
|
||||
<div class="flex justify-end">
|
||||
<span class="text-xs font-semibold text-text-light">20</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="calendar-day calendar-day-weekend">
|
||||
<div class="flex justify-end">
|
||||
<span class="text-xs font-semibold text-text-light">21</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,182 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>GMO-Z.com RUNSYSTEM - @yield('title', 'Dashboard')</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body class="bg-bg-main text-text-dark font-sans antialiased">
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar">
|
||||
<div class="flex flex-col h-full justify-between">
|
||||
<div>
|
||||
<div class="h-16 flex items-center justify-between px-6 border-b border-border-light">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="{{ asset('images/logo.png') }}" alt="GMO-Z.com RUNSYSTEM" class="h-8 w-auto object-contain">
|
||||
</div>
|
||||
<button class="text-text-light hover:text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="p-4 space-y-1">
|
||||
@if(Auth::check() && Auth::user()->role == \App\Models\User::ROLE_ADMIN)
|
||||
<!-- ADMIN SIDEBAR -->
|
||||
<a href="{{ route('admin.users.index') }}" class="sidebar-link {{ request()->routeIs('admin.users.index') ? 'sidebar-link-active' : '' }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
||||
</svg>
|
||||
<span>{{ __('layout.manage_users') }}</span>
|
||||
</a>
|
||||
<a href="{{ route('admin.users.create') }}" class="sidebar-link {{ request()->routeIs('admin.users.create') ? 'sidebar-link-active' : '' }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>{{ __('layout.add_user') }}</span>
|
||||
</a>
|
||||
@else
|
||||
<!-- USER SIDEBAR -->
|
||||
<a href="{{ route('user.dashboard') }}" class="sidebar-link {{ request()->routeIs('user.dashboard') ? 'sidebar-link-active' : '' }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
</svg>
|
||||
<span>{{ __('layout.my_page') }}</span>
|
||||
</a>
|
||||
<a href="{{ route('user.send') }}" class="sidebar-link {{ request()->routeIs('user.send') ? 'sidebar-link-active' : '' }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
||||
</svg>
|
||||
<span>{{ __('layout.send_card') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<a href="{{ route('user.change_password') }}" class="sidebar-link {{ request()->routeIs('user.change_password') ? 'sidebar-link-active' : '' }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0V10.5m-2.812 10.5h14.625c.621 0 1.125-.504 1.125-1.125V11.25c0-.621-.504-1.125-1.125-1.125H3.75c-.621 0-1.125.504-1.125 1.125v7.875c0 .621.504 1.125 1.125 1.125z" />
|
||||
</svg>
|
||||
<span>{{ __('layout.change_password') }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mx-4 my-2 p-4 rounded-lg bg-primary/5 border border-primary/10 flex flex-col items-center text-center">
|
||||
<img src="https://illustrations.popsy.co/blue/document-delivery.svg" alt="Support" class="w-24 h-24 object-contain mb-2">
|
||||
<h4 class="text-text-dark font-semibold text-xs mb-1">{{ __('layout.have_questions') }}</h4>
|
||||
<a href="#" class="btn-primary py-1.5 px-4 text-xs w-full mt-2 inline-block">{{ __('layout.feedback') }}</a>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-border-light text-center">
|
||||
<span class="text-text-muted text-[10px]">© 2026 GMO-Z.com RUNSYSTEM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="top-header">
|
||||
<div class="flex items-center gap-3 overflow-x-auto py-2">
|
||||
<span class="text-text-light text-xs font-bold whitespace-nowrap">Thank Card System</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 shrink-0">
|
||||
<div class="flex items-center gap-2 border-l border-border-light pl-4">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-light text-primary flex items-center justify-center font-bold text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-primary">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-text-dark">{{ Auth::check() ? Auth::user()->mail : 'GUEST' }}</span>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="ml-2">
|
||||
@csrf
|
||||
<button type="submit" class="text-xs text-red-500 font-bold hover:underline">{{ __('layout.logout') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
@if(session('success'))
|
||||
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded relative">
|
||||
<span class="block sm:inline">{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded relative">
|
||||
<span class="block sm:inline">{{ session('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@yield('modals')
|
||||
@stack('modals')
|
||||
<script>
|
||||
window.openModal = function(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (!modal) return;
|
||||
|
||||
// 1. Prevent body scroll
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 2. Setup initial display state
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
|
||||
// 3. Trigger animations
|
||||
requestAnimationFrame(() => {
|
||||
modal.classList.remove('opacity-0');
|
||||
modal.classList.add('opacity-100');
|
||||
const panel = modal.querySelector('.modal-panel');
|
||||
if (panel) {
|
||||
panel.classList.remove('scale-95', 'translate-y-4');
|
||||
panel.classList.add('scale-100', 'translate-y-0');
|
||||
}
|
||||
});
|
||||
|
||||
// 4. ESC to close and Trap focus
|
||||
modal.setAttribute('tabindex', '-1');
|
||||
modal.focus();
|
||||
|
||||
const escHandler = function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal(id);
|
||||
modal.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
};
|
||||
modal.addEventListener('keydown', escHandler);
|
||||
};
|
||||
|
||||
window.closeModal = function(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (!modal) return;
|
||||
|
||||
// 1. Trigger exit animations
|
||||
modal.classList.remove('opacity-100');
|
||||
modal.classList.add('opacity-0');
|
||||
const panel = modal.querySelector('.modal-panel');
|
||||
if (panel) {
|
||||
panel.classList.remove('scale-100', 'translate-y-0');
|
||||
panel.classList.add('scale-95', 'translate-y-4');
|
||||
}
|
||||
|
||||
// 2. Wait for transition then hide
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
document.body.style.overflow = '';
|
||||
}, 300); // 300ms matches Tailwind duration-300
|
||||
};
|
||||
</script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>GMO-Z.com RUNSYSTEM - {{ __('auth.login') }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-[#f0f4f9] min-h-screen flex items-center justify-center font-sans antialiased overflow-x-hidden">
|
||||
<div class="relative w-full max-w-6xl mx-auto px-4 flex flex-col lg:flex-row items-center justify-between gap-12 py-10">
|
||||
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="relative w-full max-w-[680px] aspect-[16/10.5] bg-[#1e293b] rounded-t-2xl p-3 shadow-2xl border border-slate-700">
|
||||
<div class="w-full h-full bg-[#f0f4f9] rounded-lg overflow-hidden flex flex-col justify-between p-6 relative">
|
||||
<div class="absolute inset-0 pointer-events-none opacity-20">
|
||||
<svg class="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<path d="M0,40 C30,45 70,30 100,40 L100,100 L0,100 Z" fill="#0f4c81"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start items-center z-10">
|
||||
<img src="{{ asset('images/logo.png') }}" alt="GMO-Z.com RUNSYSTEM" class="h-10 w-auto object-contain">
|
||||
</div>
|
||||
|
||||
<div class="w-full max-w-[380px] mx-auto z-10 space-y-6 my-auto">
|
||||
<h3 class="text-2xl font-bold text-center text-[#1e293b]">{{ __('auth.login') }}</h3>
|
||||
|
||||
<form action="{{ route('login') }}" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 text-red-600 text-sm font-semibold p-3 rounded-lg border border-red-200 shadow-sm flex items-start gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<span>{{ $errors->first() }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group !mb-5">
|
||||
<label class="form-label !mb-1.5 uppercase tracking-wider text-xs">{{ __('auth.email') }} <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
<span class="form-input-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4.5 h-4.5 text-text-light">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="email" name="mail" placeholder="{{ __('auth.email_placeholder') }}" class="form-input form-input-with-icon !bg-white" required value="{{ old('mail') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group !mb-6">
|
||||
<label class="form-label !mb-1.5 uppercase tracking-wider text-xs">{{ __('auth.password') }} <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
<span class="form-input-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4.5 h-4.5 text-text-light">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0V10.5m-2.812 10.5h14.625c.621 0 1.125-.504 1.125-1.125V11.25c0-.621-.504-1.125-1.125-1.125H3.75c-.621 0-1.125.504-1.125 1.125v7.875c0 .621.504 1.125 1.125 1.125z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="password" name="password" placeholder="{{ __('auth.password_placeholder') }}" class="form-input form-input-with-icon !bg-white" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full shadow-md text-sm uppercase tracking-wide">
|
||||
{{ __('auth.login') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="text-center z-10 mt-auto">
|
||||
<span class="text-xs text-[#475569]/80 font-semibold">© {{ __('auth.developed_by') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full max-w-[760px] h-3 bg-[#e2e8f0] rounded-b-2xl border-t border-white/20 shadow-lg">
|
||||
<div class="absolute left-1/2 -translate-x-1/2 top-0 w-28 h-1.5 bg-[#cbd5e1] rounded-b-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex justify-center items-center relative min-h-[360px] lg:min-h-[480px]">
|
||||
<div class="relative z-10 w-full max-w-[420px]">
|
||||
<svg viewBox="0 0 500 500" class="w-full h-auto">
|
||||
<circle cx="250" cy="400" r="80" fill="#e2e8f0" />
|
||||
<rect x="235" y="320" width="30" height="90" fill="#78350f" />
|
||||
<polygon points="210,410 290,410 270,440 230,440" fill="#451a03" />
|
||||
<g transform="translate(0, -20)">
|
||||
<path d="M 280,310 C 280,210 320,180 340,160 C 350,150 370,160 380,180 C 390,200 400,240 370,300 C 340,360 300,320 280,310 Z" fill="#334155" />
|
||||
<path d="M 310,210 C 320,200 335,200 345,210 C 350,215 350,225 340,235 C 330,245 315,245 305,235 C 300,230 300,215 310,210 Z" fill="#fbcfe8" />
|
||||
<circle cx="335" cy="205" r="16" fill="#fbcfe8" />
|
||||
<path d="M 320,195 C 322,185 330,180 340,183 C 345,185 348,190 345,198 Z" fill="#1e293b" />
|
||||
<ellipse cx="338" cy="203" rx="1" ry="2" fill="#1e293b" />
|
||||
<path d="M 334,212 Q 338,216 342,212" stroke="#1e293b" stroke-width="1.5" fill="none" />
|
||||
<path d="M 345,200 Q 365,190 350,175 Q 335,160 320,178 Z" fill="#1e293b" />
|
||||
<path d="M 340,160 L 350,170 L 335,175 Z" fill="#fbbf24" />
|
||||
</g>
|
||||
<g transform="translate(0, -20)">
|
||||
<path d="M 320,220 C 340,230 370,250 380,280 L 330,340 L 290,280 Z" fill="#ef222e" />
|
||||
<rect x="330" y="270" width="22" height="18" fill="#ffffff" rx="2" />
|
||||
<text x="332" y="282" font-size="8" font-family="sans-serif" font-weight="bold" fill="#ef222e">GMO-Z</text>
|
||||
</g>
|
||||
<g transform="translate(0, -20)">
|
||||
<path d="M 285,280 L 245,360 C 235,380 250,400 270,400 L 325,400 L 340,335 Z" fill="#334155" />
|
||||
<path d="M 315,335 L 340,400 C 345,410 360,400 355,390 L 330,335 Z" fill="#334155" />
|
||||
<path d="M 245,360 L 220,375 C 210,380 220,400 235,395 L 255,385 Z" fill="#fde047" />
|
||||
<path d="M 340,400 L 315,420 C 305,425 315,440 330,435 L 355,415 Z" fill="#fde047" />
|
||||
</g>
|
||||
<g transform="translate(-20, -10)">
|
||||
<rect x="250" y="295" width="65" height="45" rx="4" fill="#cbd5e1" transform="rotate(-10, 250, 295)" />
|
||||
<rect x="255" y="300" width="55" height="35" rx="2" fill="#94a3b8" transform="rotate(-10, 250, 295)" />
|
||||
<circle cx="282" cy="318" r="4" fill="#ffffff" />
|
||||
<rect x="278" y="338" width="40" height="4" rx="2" fill="#e2e8f0" transform="rotate(-10, 278, 338)" />
|
||||
</g>
|
||||
<g transform="translate(380, 110)">
|
||||
<circle cx="50" cy="50" r="30" fill="#ffffff" stroke="#e2e8f0" stroke-width="2" />
|
||||
<path d="M 47,40 L 53,40 L 53,46 L 47,46 Z" fill="#fbbf24" />
|
||||
<path d="M 43,46 L 57,46 L 57,60 L 43,60 Z" fill="#fbbf24" rx="2" />
|
||||
<circle cx="50" cy="53" r="2" fill="#1e293b" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-[#fbbf24] flex items-center justify-center shadow-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6 text-white">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Đổi Mật Khẩu')
|
||||
|
||||
@section('content')
|
||||
<div class="form-container mt-10">
|
||||
<div class="form-header">
|
||||
<h3 class="form-header-title">Đổi Mật Khẩu</h3>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('user.update_password') }}" method="POST">
|
||||
@csrf
|
||||
<div class="form-body">
|
||||
@if(Auth::user()->first_login == \App\Models\User::FIRST_LOGIN_TRUE)
|
||||
<div class="mb-6 p-4 bg-orange-50 border border-orange-200 text-orange-800 rounded-lg text-sm font-medium leading-relaxed flex items-start gap-2 shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 shrink-0 text-orange-500 mt-0.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>Đây là lần đầu tiên bạn đăng nhập vào hệ thống. Để đảm bảo an toàn, hệ thống yêu cầu bạn đổi mật khẩu trước khi sử dụng các chức năng.</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Mật khẩu mới <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
<span class="form-input-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0V10.5m-2.812 10.5h14.625c.621 0 1.125-.504 1.125-1.125V11.25c0-.621-.504-1.125-1.125-1.125H3.75c-.621 0-1.125.504-1.125 1.125v7.875c0 .621.504 1.125 1.125 1.125z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="password" name="password" id="password" class="form-input form-input-with-icon" required placeholder="Nhập mật khẩu mới">
|
||||
</div>
|
||||
@error('password')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password_confirmation">Xác nhận lại mật khẩu mới <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
<span class="form-input-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="password" name="password_confirmation" id="password_confirmation" class="form-input form-input-with-icon" required placeholder="Nhập lại mật khẩu mới">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-footer">
|
||||
@if(Auth::user()->first_login != \App\Models\User::FIRST_LOGIN_TRUE)
|
||||
<a href="{{ route('user.dashboard') }}" class="btn-secondary">Hủy bỏ</a>
|
||||
@endif
|
||||
<button type="submit" class="btn-primary">Xác Nhận Đổi Mật Khẩu</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,77 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'My Page')
|
||||
|
||||
@section('content')
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="text-xl font-bold text-text-dark flex items-center gap-2">
|
||||
My Page
|
||||
</h2>
|
||||
<div class="px-4 py-1.5 bg-orange-100 border border-orange-200 rounded-full text-orange-800 font-bold text-sm flex items-center gap-2 shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-orange-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
|
||||
</svg>
|
||||
Thẻ hiện có: <span class="text-lg">{{ $user->card }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('user.dashboard') }}" method="GET" class="flex items-center gap-2">
|
||||
<input type="month" name="month" value="{{ $selectedMonth }}" class="form-input !py-1.5 !text-sm w-auto border-border-medium rounded">
|
||||
<button type="submit" class="btn-primary !py-1.5 !px-4 text-sm rounded shadow-sm hover:translate-y-[-1px]">Lọc</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-sm rounded-xl overflow-hidden border border-border-light">
|
||||
<div class="px-6 py-4 border-b border-border-light bg-gray-50/50">
|
||||
<h3 class="text-lg font-bold text-text-dark">Lịch sử nhận / gửi thẻ (Tháng {{ Carbon\Carbon::parse($selectedMonth)->format('m/Y') }})</h3>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-border-light text-sm">
|
||||
<thead class="bg-gray-50/80">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Ngày</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Loại giao dịch</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Đối tác</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Số lượng</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-border-light">
|
||||
@forelse($administrations as $admin_record)
|
||||
@php
|
||||
$isReceiver = $admin_record->msnv == $user->msnv && $admin_record->received > 0;
|
||||
@endphp
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-3 whitespace-nowrap text-text-medium font-medium">{{ Carbon\Carbon::parse($admin_record->date)->format('d/m/Y') }}</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap">
|
||||
@if($isReceiver)
|
||||
<span class="px-3 py-1 inline-flex text-xs leading-5 font-bold rounded-full bg-green-100 text-green-800">Bạn đã nhận thẻ</span>
|
||||
@else
|
||||
<span class="px-3 py-1 inline-flex text-xs leading-5 font-bold rounded-full bg-blue-100 text-blue-800">Bạn gửi tặng thẻ</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap font-bold text-text-dark">
|
||||
@if($isReceiver)
|
||||
Nhận từ: <span class="text-primary">{{ $admin_record->sender }}</span>
|
||||
@else
|
||||
Gửi cho: <span class="text-primary">{{ $admin_record->receiver }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap font-extrabold text-lg">
|
||||
@if($isReceiver)
|
||||
<span class="text-green-600">+{{ $admin_record->received }}</span>
|
||||
@else
|
||||
<span class="text-blue-600">-{{ $admin_record->sent }}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-12 text-center text-text-light font-medium text-base">Bạn chưa có giao dịch gửi/nhận thẻ nào trong tháng này.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,146 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Gửi Thank Card')
|
||||
|
||||
@section('content')
|
||||
<div class="form-container">
|
||||
<div class="form-header">
|
||||
<h3 class="form-header-title">Gửi Thank Card Tới Đồng Nghiệp</h3>
|
||||
</div>
|
||||
|
||||
<form id="sendCardForm">
|
||||
@csrf
|
||||
|
||||
<div class="form-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="receiver">Chọn người nhận <span class="text-red-500">*</span></label>
|
||||
<select name="receiver" id="receiver" class="form-input" required>
|
||||
<option value="" disabled selected>-- Chọn đồng nghiệp --</option>
|
||||
@foreach($users as $u)
|
||||
<option value="{{ $u->msnv }}">{{ $u->msnv }} ({{ $u->mail }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="amount">Số lượng thẻ <span class="text-red-500">*</span></label>
|
||||
<select name="amount" id="amount" class="form-input" required>
|
||||
<option value="1">1 Thẻ</option>
|
||||
<option value="2">2 Thẻ</option>
|
||||
<option value="3">3 Thẻ</option>
|
||||
<option value="4">4 Thẻ</option>
|
||||
<option value="{{ \App\Models\Administration::MAX_SEND_CARD_PER_MONTH }}">{{ \App\Models\Administration::MAX_SEND_CARD_PER_MONTH }} Thẻ (Tối đa)</option>
|
||||
</select>
|
||||
<p class="helper-text">Lưu ý: Chỉ được gửi tối đa {{ \App\Models\Administration::MAX_SEND_CARD_PER_MONTH }} thẻ cho cùng 1 người trong 1 tháng.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-footer">
|
||||
<a href="{{ route('user.dashboard') }}" class="btn-secondary">Hủy bỏ</a>
|
||||
<button type="button" id="btnSubmitSend" class="btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
||||
</svg>
|
||||
Gửi Card
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Custom Popup Modal -->
|
||||
<x-modal id="customModal" title="Thông báo" maxWidth="md">
|
||||
<p id="modalMessage" class="text-sm text-gray-600 leading-relaxed"></p>
|
||||
|
||||
<x-slot name="footer">
|
||||
<div id="modalActions" class="w-full flex flex-col sm:flex-row-reverse gap-3">
|
||||
<!-- Buttons injected by JS -->
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-modal>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const btnSubmit = document.getElementById('btnSubmitSend');
|
||||
const form = document.getElementById('sendCardForm');
|
||||
|
||||
const modalTitle = document.getElementById('modal-title-customModal');
|
||||
const modalMessage = document.getElementById('modalMessage');
|
||||
const modalActions = document.getElementById('modalActions');
|
||||
|
||||
const showModal = (title, message, isConfirm, confirmCallback) => {
|
||||
modalTitle.textContent = title;
|
||||
modalMessage.innerHTML = message;
|
||||
|
||||
if(isConfirm) {
|
||||
modalTitle.className = "text-lg font-bold text-primary";
|
||||
modalActions.innerHTML = `
|
||||
<button id="btnConfirmOk" class="w-full sm:w-auto btn-primary !px-6 shadow-sm">Xác nhận gửi</button>
|
||||
<button id="btnCancel" class="w-full sm:w-auto btn-secondary !px-6 shadow-sm">Hủy</button>
|
||||
`;
|
||||
document.getElementById('btnCancel').addEventListener('click', () => window.closeModal('customModal'));
|
||||
document.getElementById('btnConfirmOk').addEventListener('click', function() {
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<span class="flex items-center gap-2"><svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Đang xử lý...</span>';
|
||||
confirmCallback();
|
||||
});
|
||||
} else {
|
||||
modalTitle.className = "text-lg font-bold text-red-600";
|
||||
modalActions.innerHTML = `
|
||||
<button id="btnOkOnly" class="w-full sm:w-auto btn-secondary !px-6 shadow-sm">Đóng</button>
|
||||
`;
|
||||
document.getElementById('btnOkOnly').addEventListener('click', () => window.closeModal('customModal'));
|
||||
}
|
||||
|
||||
window.openModal('customModal');
|
||||
};
|
||||
|
||||
btnSubmit.addEventListener('click', function() {
|
||||
if(!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
const receiver = document.getElementById('receiver').value;
|
||||
const amount = document.getElementById('amount').value;
|
||||
|
||||
showModal(
|
||||
'Xác nhận gửi Thank Card',
|
||||
`Bạn có chắc chắn muốn gửi <b>${amount} thẻ</b> cho nhân viên <span class="font-bold text-primary">${receiver}</span> không? <br><br><span class="text-xs text-text-light">Hành động này không thể hoàn tác, tin nhắn CO sẽ được gửi đi!</span>`,
|
||||
true,
|
||||
() => {
|
||||
fetch('{{ route('user.store_send') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({
|
||||
receiver: receiver,
|
||||
amount: amount
|
||||
})
|
||||
})
|
||||
.then(response => response.json().then(data => ({status: response.status, body: data})))
|
||||
.then(res => {
|
||||
if (res.status === 200 && res.body.success) {
|
||||
alert(res.body.message);
|
||||
window.location.href = '{{ route('user.dashboard') }}';
|
||||
} else {
|
||||
window.closeModal('customModal');
|
||||
setTimeout(() => {
|
||||
showModal('Lỗi Giao Dịch', res.body.message || 'Có lỗi xảy ra, vui lòng thử lại.', false, null);
|
||||
}, 350);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
window.closeModal('customModal');
|
||||
setTimeout(() => {
|
||||
showModal('Lỗi Hệ Thống', 'Không thể kết nối đến máy chủ.', false, null);
|
||||
}, 350);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
Reference in New Issue
Block a user