all feature

This commit is contained in:
antv
2026-06-29 11:55:33 +07:00
parent 08d13336cd
commit 8a77324f89
59 changed files with 5587 additions and 145 deletions
+241 -2
View File
@@ -1,3 +1,56 @@
@font-face {
font-family: 'SVN-Poppins-Regular';
src: url('/fonts/FZ Poppins-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'SVN-Poppins-Medium';
src: url('/fonts/FZ Poppins-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'SVN-Poppins-SemiBold';
src: url('/fonts/FZ Poppins-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'SVN-Poppins';
src: url('/fonts/FZ Poppins-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'SVN-Poppins';
src: url('/fonts/FZ Poppins-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'SVN-Poppins';
src: url('/fonts/FZ Poppins-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'SVN-Poppins';
src: url('/fonts/FZ Poppins-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
}
@import "@fontsource/roboto/300.css";
@import "@fontsource/roboto/400.css";
@import "@fontsource/roboto/500.css";
@import "@fontsource/roboto/700.css";
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@@ -6,6 +59,192 @@
@source '../**/*.js';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-sans: 'SVN-Poppins', 'SVN-Poppins-SemiBold', 'SVN-Poppins-Regular', 'SVN-Poppins-Medium', 'Roboto', 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--color-primary: #0f4c81;
--color-primary-hover: #0a3356;
--color-primary-light: #e8f0fe;
--color-secondary: #ef222e;
--color-success: #2e7d32;
--color-warning: #ed6c02;
--color-info: #0288d1;
--color-bg-main: #f0f4f9;
--color-bg-sidebar: #ffffff;
--color-bg-card: #ffffff;
--color-bg-card-soft: #eef2f6;
--color-border-light: #e2e8f0;
--color-border-medium: #cbd5e1;
--color-text-dark: #1e293b;
--color-text-medium: #475569;
--color-text-light: #64748b;
--color-text-muted: #94a3b8;
--color-calendar-header: #4b729f;
--shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
--shadow-sidebar: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
--shadow-header: 0 2px 4px 0 rgba(0, 0, 0, 0.02);
}
@layer base {
body {
@apply bg-bg-main text-text-dark antialiased;
}
}
.app-layout {
@apply min-h-screen flex bg-bg-main;
}
.sidebar {
@apply w-60 bg-bg-sidebar border-r border-border-light flex flex-col justify-between shrink-0 shadow-sidebar transition-all duration-300;
}
.main-content {
@apply flex-1 flex flex-col min-w-0;
}
.top-header {
@apply h-16 bg-bg-sidebar border-b border-border-light flex items-center justify-between px-6 shadow-header;
}
.sidebar-link {
@apply flex items-center gap-3 px-4 py-3 text-text-medium font-medium rounded-lg hover:bg-gray-50 hover:text-primary transition-all duration-200;
}
.sidebar-link-active {
@apply text-primary bg-primary-light font-bold;
}
.card-stat {
@apply bg-bg-card-soft text-primary p-5 rounded-lg border border-transparent shadow-card relative overflow-hidden transition-all duration-200 hover:shadow-md;
}
.card-stat-active {
@apply bg-primary text-white p-5 rounded-lg border border-transparent shadow-card relative overflow-hidden transition-all duration-200 hover:shadow-md;
}
.card-stat::after, .card-stat-active::after {
content: '';
position: absolute;
right: -20px;
bottom: -20px;
width: 100px;
height: 100px;
border-radius: 50%;
background: radial-gradient(circle, rgba(15, 76, 129, 0.05) 0%, transparent 70%);
pointer-events: none;
}
.card-stat-active::after {
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
}
.calendar-card {
@apply bg-bg-card rounded-lg shadow-card border border-border-light overflow-hidden;
}
.calendar-header {
@apply grid grid-cols-7 bg-calendar-header text-white font-bold text-center py-3;
}
.calendar-grid {
@apply grid grid-cols-7 border-t border-l border-border-light;
}
.calendar-day {
@apply bg-bg-card border-r border-b border-border-light min-h-[100px] p-2 flex flex-col justify-between transition-colors duration-150 hover:bg-gray-50;
}
.calendar-day-weekend {
@apply bg-gray-50;
}
/* --- FORM DESIGN SYSTEM --- */
.form-container {
@apply w-full max-w-[1100px] mx-auto bg-white rounded-xl shadow-sm border border-[#d9dfe7] overflow-hidden;
}
.form-header {
@apply px-6 py-6 sm:px-8 sm:py-8 border-b border-[#d9dfe7] flex items-center justify-between;
}
.form-header-title {
@apply text-[24px] sm:text-[32px] font-[600] text-text-dark leading-tight;
}
.form-body {
@apply p-6 sm:p-8; /* 32px top/bottom card padding */
}
.form-group {
@apply mb-6 relative; /* 24px gap */
}
.form-label {
@apply block text-[16px] font-[600] text-text-dark mb-2; /* 8px label gap */
}
/* Standard Input & Select */
.form-input {
@apply w-full h-[48px] px-4 bg-white border border-[#d9dfe7] rounded-lg outline-none text-text-dark text-base placeholder-text-muted focus:border-primary focus:ring-2 focus:ring-primary/20 hover:border-gray-400 transition-all duration-200 shadow-sm;
}
select.form-input {
@apply appearance-none pr-10 cursor-pointer bg-no-repeat;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 1rem center;
background-size: 1.25em 1.25em;
}
.form-input[type="checkbox"] {
@apply w-5 h-5 min-h-0 text-primary border-[#d9dfe7] rounded focus:ring-primary focus:ring-2 focus:ring-offset-1 cursor-pointer transition-colors hover:border-gray-400;
}
.form-input-with-icon {
@apply pl-11;
}
.form-input-icon {
@apply absolute left-4 text-text-light pointer-events-none w-5 h-5 flex items-center justify-center;
}
.form-input-group {
@apply relative flex items-center w-full;
}
.form-actions {
@apply mt-2 flex flex-col sm:flex-row justify-end gap-4;
}
.form-footer {
@apply px-6 py-6 sm:px-8 sm:py-6 border-t border-[#d9dfe7] bg-gray-50/30 flex flex-col sm:flex-row justify-end gap-3;
}
.helper-text {
@apply text-[14px] text-text-muted mt-2 leading-snug block;
}
.error-text {
@apply text-red-500 text-[14px] font-semibold mt-2 block;
}
/* Action Buttons */
.btn-primary {
@apply bg-primary hover:bg-primary-hover text-white font-[600] h-[45px] max-h-[45px] px-8 border border-transparent rounded-lg shadow-sm transition-all duration-200 text-center cursor-pointer inline-flex items-center justify-center focus:ring-2 focus:ring-offset-1 focus:ring-primary/50 box-border;
}
.btn-secondary {
@apply bg-white hover:bg-gray-50 text-text-medium border border-[#d9dfe7] font-[600] h-[45px] max-h-[45px] px-8 rounded-lg shadow-sm transition-all duration-200 text-center cursor-pointer inline-flex items-center justify-center focus:ring-2 focus:ring-offset-1 focus:ring-gray-200 box-border;
}
.nav-badge {
@apply bg-secondary text-white text-[10px] font-bold px-1.5 py-0.5 rounded-md uppercase ml-2 animate-pulse;
}
.notification-badge {
@apply absolute -top-1 -right-1 bg-secondary text-white text-[10px] font-bold w-4.5 h-4.5 rounded-full flex items-center justify-center;
}
@@ -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"> 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
+116
View File
@@ -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 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ố 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 giao dịch nào trong tháng này.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
+258
View File
@@ -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ố 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 </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 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 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
+419
View File
@@ -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 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]">&copy; 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>
+182
View File
@@ -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]">&copy; 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>
+131
View File
@@ -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">&copy; {{ __('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ầ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
+77
View File
@@ -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 : <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 giao dịch gửi/nhận thẻ nào trong tháng này.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection
+146
View File
@@ -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