reuse form

This commit is contained in:
antv
2026-07-01 16:57:34 +07:00
parent 4e4d03d62a
commit f1858f8f0f
34 changed files with 1273 additions and 681 deletions
@@ -0,0 +1,17 @@
@props(['mode', 'user' => null])
<div class="form-footer flex space-x-2">
@if($mode === 'create')
<a href="{{ route('admin.users.index') }}" class="btn-secondary min-w-[120px] h-[45px]">Hủy bỏ</a>
<button type="submit" class="btn-primary min-w-[120px] h-[45px]">Tạo Nhân Viên</button>
@elseif($mode === 'edit')
<a href="{{ route('admin.users.index') }}" class="btn-secondary min-w-[120px] h-[45px]">Quay lại</a>
<button type="submit" class="btn-primary min-w-[120px] h-[45px]">Lưu Thay Đổi</button>
@elseif($mode === 'delete')
<a href="{{ route('admin.users.index') }}" class="btn-secondary min-w-[120px] h-[45px]">Quay lại</a>
<button type="submit" class="btn-primary bg-red-600 hover:bg-red-700 min-w-[120px] h-[45px]">Nghỉ việc</button>
@elseif($mode === 'self')
<a href="{{ route('user.dashboard') }}" class="btn-secondary min-w-[120px] h-[45px]">Quay lại</a>
@endif
</div>
{{-- duplicated block removed --}}
@@ -0,0 +1,26 @@
@props(['id', 'title' => 'Xác nhận', 'message' => '', 'confirmText' => 'Xác nhận', 'cancelText' => 'Hủy bỏ', 'confirmBg' => 'bg-red-600 hover:bg-red-700'])
<x-modal :id="$id" :title="$title" 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">{{ $title }}</h3>
<p class="text-sm text-gray-600 mt-2 leading-relaxed" id="{{ $id }}-message">
{{ $message }}
</p>
</div>
</div>
<x-slot name="footer">
<form id="{{ $id }}-form" method="POST" class="w-full sm:w-auto">
@csrf
@method('DELETE')
<button type="submit" id="{{ $id }}-confirm-btn" class="w-full btn-primary !px-6 shadow-sm {{ $confirmBg }}">{{ $confirmText }}</button>
</form>
<button type="button" onclick="closeModal('{{ $id }}')" class="w-full sm:w-auto btn-secondary !px-6 shadow-sm">{{ $cancelText }}</button>
</x-slot>
</x-modal>
@@ -0,0 +1,7 @@
@props(['name', 'class' => ''])
@php
$svgPath = public_path('assets/icons/' . $name . '.svg');
@endphp
<span {{ $attributes->merge(['class' => $class]) }} role="img" aria-hidden="true">
{!! file_get_contents($svgPath) !!}
</span>
@@ -0,0 +1,12 @@
@props(['mode', 'user' => null, 'selectedMonth'])
@php
$actionUrl = $mode === 'self'
? route('user.dashboard')
: route('admin.users.edit', $user?->msnv);
@endphp
<form action="{{ $actionUrl }}" method="GET" class="flex items-center gap-2 flex-shrink-0">
<input type="month" name="month" value="{{ $selectedMonth }}" onchange="this.form.submit()" class="form-input !min-h-[45px] !py-0 !text-sm w-auto border-border-medium rounded bg-white">
<button type="submit" class="btn-primary !h-[45px] !px-4 text-sm rounded shadow-sm hover:translate-y-[-1px] sm:hidden">Lọc</button>
</form>
@@ -0,0 +1,14 @@
@props(['title', 'user', 'value', 'bgClass' => 'bg-[#0f4c81]', 'valueLabel' => 'Tổng thẻ đã nhận'])
<div class="card-stat-active {{ $bgClass }}">
<div class="space-y-2">
<div class="flex justify-between items-center">
<span class="text-white/90 text-xs font-bold uppercase tracking-wider">{{ $title }}</span>
<span class="text-2xl font-black">{{ $user && $value > 0 ? $user : '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">{{ $valueLabel }}</span>
<span class="text-lg font-bold text-yellow-300">{{ $value }} thẻ</span>
</div>
</div>
</div>
@@ -0,0 +1,17 @@
@props(['rows' => 10, 'cols' => 7])
@for ($i = 0; $i < $rows; $i++)
<tr class="animate-pulse h-[69px]">
@for ($j = 0; $j < $cols; $j++)
@if ($j === 1)
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-48"></div></td>
@elseif ($j === 5)
<td class="px-6 py-4"><div class="h-6 bg-gray-200 rounded-full w-20"></div></td>
@elseif ($j === 6)
<td class="px-6 py-4"><div class="h-8 bg-gray-200 rounded w-20 ml-auto"></div></td>
@else
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-16"></div></td>
@endif
@endfor
</tr>
@endfor
@@ -0,0 +1,78 @@
@props(['mode', 'user', 'administrations', 'selectedMonth'])
<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">
@if($mode === 'self')
Lịch sử nhận / gửi thẻ (Tháng {{ Carbon\Carbon::parse($selectedMonth)->format('m/Y') }})
@else
Lịch sử nhận / gửi
@endif
</h3>
<x-month-filter :mode="$mode" :user="$user" :selectedMonth="$selectedMonth" />
</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">
{{ $mode === 'self' ? 'Loại giao dịch' : 'Hành động' }}
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">
{{ $mode === 'self' ? 'Đối tác' : 'Đố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">
{{ $mode === 'self' ? 'Bạn đã nhận thẻ' : '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">
{{ $mode === 'self' ? 'Bạn gửi tặng thẻ' : 'Gửi thẻ' }}
</span>
@endif
</td>
<td class="px-6 py-3 whitespace-nowrap font-bold text-text-dark">
@if($isReceiver)
{{ $mode === 'self' ? 'Nhận từ: ' : 'Từ: ' }}<span class="text-primary">{{ $admin_record->sender }}</span>
@else
{{ $mode === 'self' ? 'Gửi cho: ' : '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">
{{ $mode === 'self' ? 'Bạn chưa có giao dịch gửi/nhận thẻ nào trong tháng này.' : 'Không có giao dịch nào trong tháng này.' }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($administrations->hasPages())
<div class="px-6 py-4 border-t border-border-light bg-gray-50/30">
{{ $administrations->links() }}
</div>
@endif
</div>
@@ -0,0 +1,28 @@
@props(['action', 'search' => '', 'flagSend' => '', 'selectedMonth' => ''])
<form id="filterForm" action="{{ $action }}" method="GET" class="flex flex-col md:flex-row items-stretch md:items-center gap-3 w-full mb-6 bg-gray-50/50 p-4 rounded-xl border border-border-light">
<div class="relative flex items-center flex-1 min-w-[200px]">
<input type="text" id="searchInput" name="search" value="{{ $search }}" placeholder="Tìm MSNV hoặc Email..." class="form-input !py-1.5 !text-sm !pl-10 !pr-10 w-full border-border-medium rounded bg-white">
<svg class="w-4 h-4 text-text-light absolute left-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="pointer-events: none;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<button type="button" id="btnClearSearch" class="{{ empty($search) ? 'hidden' : '' }} absolute right-3 text-text-light hover:text-text-dark focus:outline-none">
<svg class="w-4 h-4" 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<select name="flag_send" id="flagSendFilter" class="form-input !py-1.5 !text-sm w-full md:w-auto border-border-medium rounded bg-white min-w-[150px]">
<option value="" {{ is_null($flagSend) || $flagSend === '' ? 'selected' : '' }}>Quyền gửi: Tất cả</option>
<option value="1" {{ $flagSend === '1' ? 'selected' : '' }}>Được gửi</option>
<option value="0" {{ $flagSend === '0' ? 'selected' : '' }}>Không được gửi</option>
</select>
<input type="month" name="month" id="monthFilter" value="{{ $selectedMonth }}" class="form-input !py-1.5 !text-sm w-full md:w-auto border-border-medium rounded bg-white">
<div class="flex items-center gap-2 w-full md:w-auto">
<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] flex-1 md:flex-none">Tìm</button>
<button type="button" id="btnClearFilters" class="btn-secondary !py-1.5 !px-4 text-sm rounded border border-border-medium hover:bg-gray-100 flex items-center justify-center min-w-[100px] flex-1 md:flex-none">Xóa bộ lọc</button>
</div>
</form>
@@ -0,0 +1,141 @@
@props(['mode', 'user' => null])
@php
$isCreate = $mode === 'create';
$isEdit = $mode === 'edit';
$isDelete = $mode === 'delete';
$isSelf = $mode === 'self';
$actionUrl = match($mode) {
'create' => route('admin.users.store'),
'edit' => route('admin.users.update', $user?->msnv),
'delete' => route('admin.users.destroy', $user?->msnv),
default => null,
};
$method = match($mode) {
'edit' => 'PUT',
'delete' => 'DELETE',
default => 'POST',
};
@endphp
<div class="form-container flex flex-col h-full justify-between">
<div class="form-header">
<h3 class="form-header-title">
{{ match($mode) {
'create' => 'Thêm User Mới',
'edit' => 'Quản Lý Thẻ: ' . ($user?->msnv),
'delete' => 'Xác nhận nghỉ việc: ' . ($user?->msnv),
'self' => 'Thông tin cá nhân: ' . ($user?->msnv),
} }}
</h3>
</div>
@if(!$isCreate && $user)
<div class="form-body pb-0">
<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>
@endif
@if($actionUrl)
<form action="{{ $actionUrl }}" method="POST" class="flex flex-col flex-1 justify-between">
@csrf
@if($method !== 'POST')
@method($method)
@endif
@endif
<div class="form-body {{ !$isCreate ? 'pt-0' : '' }}">
@if($isCreate)
<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 bg-white" 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 bg-white" 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 bg-white" 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 bg-white" 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>
@endif
@if($isEdit && $user)
<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 bg-white" 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 bg-white">
<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-[14px] font-[600] text-text-dark">Bật quyền cho phép User gửi thẻ</span>
</label>
</div>
@endif
@if($isDelete && $user)
<p class="text-sm text-red-600 leading-relaxed font-semibold">
Bạn chắc chắn muốn cho nhân viên {{ $user->msnv }} nghỉ việc không? Tài khoản này sẽ không thể đăng nhập hoặc thực hiện các giao dịch thẻ sau khi xác nhận.
</p>
@endif
</div>
<x-action-panel :mode="$mode" :user="$user" />
@if($actionUrl)
</form>
@endif
</div>
@@ -0,0 +1,39 @@
@props(['mode', 'user' => null, 'administrations' => null, 'selectedMonth' => null])
@if($mode === 'self' && $user)
<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>
</div>
@endif
<div class="grid grid-cols-1 {{ in_array($mode, ['edit', 'delete']) ? 'lg:grid-cols-3' : '' }} gap-6 items-start">
<!-- LEFT PANEL: User Profile & Actions / Form -->
<div class="{{ in_array($mode, ['edit', 'delete']) ? 'col-span-1' : 'col-span-full' }}">
@if($mode === 'create')
<div class="max-w-2xl mx-auto w-full">
<x-user-form :mode="$mode" :user="$user" />
</div>
@else
<x-user-form :mode="$mode" :user="$user" />
@endif
</div>
<!-- RIGHT PANEL: Transaction History -->
@if(in_array($mode, ['edit', 'delete', 'self']) && $user && $administrations)
<div class="col-span-1 lg:col-span-2">
<x-transaction-history :mode="$mode" :user="$user" :administrations="$administrations" :selectedMonth="$selectedMonth" />
</div>
@endif
</div>