all feature
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_LOCALE=vi
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
|
||||
class ResetCardsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'cards:reset';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Reset toàn bộ thẻ của hệ thống về 0 vào đầu tháng';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
User::where('status', User::STATUS_ACTIVE)->update(['card' => 0]);
|
||||
|
||||
$this->info('Đã reset toàn bộ số lượng thẻ của nhân viên đang hoạt động về 0 thành công!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\User;
|
||||
use App\Models\Administration;
|
||||
use App\Models\AddCard;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$selectedMonth = $request->input('month', Carbon::now()->format('Y-m'));
|
||||
$startOfMonth = Carbon::parse($selectedMonth)->startOfMonth();
|
||||
$endOfMonth = Carbon::parse($selectedMonth)->endOfMonth();
|
||||
|
||||
// 1. Calculate top stats from all active users
|
||||
$allActiveUsers = User::where('status', User::STATUS_ACTIVE)->get()->map(function($user) use ($startOfMonth, $endOfMonth) {
|
||||
$user->total_received = Administration::where('receiver', $user->msnv)
|
||||
->whereBetween('date', [$startOfMonth, $endOfMonth])
|
||||
->sum('received');
|
||||
|
||||
$user->total_sent = Administration::where('sender', $user->msnv)
|
||||
->whereBetween('date', [$startOfMonth, $endOfMonth])
|
||||
->sum('sent');
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
$topReceivedUser = $allActiveUsers->sortByDesc('total_received')->first();
|
||||
$topSentUser = $allActiveUsers->sortByDesc('total_sent')->first();
|
||||
|
||||
// 2. Paginate users for the list table (10 per page)
|
||||
$users = User::where('status', User::STATUS_ACTIVE)->paginate(10)->withQueryString();
|
||||
|
||||
// 3. Compute stats only for the paginated users (to display in the table)
|
||||
$users->getCollection()->transform(function($user) use ($startOfMonth, $endOfMonth) {
|
||||
$user->total_received = Administration::where('receiver', $user->msnv)
|
||||
->whereBetween('date', [$startOfMonth, $endOfMonth])
|
||||
->sum('received');
|
||||
|
||||
$user->total_sent = Administration::where('sender', $user->msnv)
|
||||
->whereBetween('date', [$startOfMonth, $endOfMonth])
|
||||
->sum('sent');
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
return view('admin.users.index', compact('users', 'selectedMonth', 'topReceivedUser', 'topSentUser'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.users.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'msnv' => 'required|unique:user,msnv',
|
||||
'mail' => 'required|email|unique:user,mail',
|
||||
'password' => 'required|min:6',
|
||||
'role' => 'required|in:' . User::ROLE_MEMBER . ',' . User::ROLE_ADMIN
|
||||
]);
|
||||
|
||||
User::create([
|
||||
'msnv' => $request->msnv,
|
||||
'mail' => $request->mail,
|
||||
'pass' => Hash::make($request->password),
|
||||
'role' => $request->role,
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'card' => 0,
|
||||
'flag_send' => User::FLAG_SEND_DISABLED,
|
||||
'first_login' => User::FIRST_LOGIN_TRUE
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', 'Tạo user thành công!');
|
||||
}
|
||||
|
||||
public function edit(Request $request, $msnv)
|
||||
{
|
||||
$user = User::where('msnv', $msnv)->firstOrFail();
|
||||
|
||||
$selectedMonth = $request->input('month', Carbon::now()->format('Y-m'));
|
||||
$startOfMonth = Carbon::parse($selectedMonth)->startOfMonth();
|
||||
$endOfMonth = Carbon::parse($selectedMonth)->endOfMonth();
|
||||
|
||||
$administrations = Administration::where(function($q) use ($msnv) {
|
||||
$q->where('sender', $msnv)->orWhere('receiver', $msnv);
|
||||
})
|
||||
->whereBetween('date', [$startOfMonth, $endOfMonth])
|
||||
->orderBy('date', 'desc')
|
||||
->get();
|
||||
|
||||
return view('admin.users.edit', compact('user', 'administrations', 'selectedMonth'));
|
||||
}
|
||||
|
||||
public function update(Request $request, $msnv)
|
||||
{
|
||||
$user = User::where('msnv', $msnv)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'num_card' => 'nullable|integer|min:1',
|
||||
'flag_send' => 'nullable|boolean'
|
||||
]);
|
||||
|
||||
DB::transaction(function () use ($request, $user) {
|
||||
if ($request->filled('num_card')) {
|
||||
AddCard::create([
|
||||
'buyer' => $user->msnv,
|
||||
'num_card' => $request->num_card,
|
||||
'seller' => auth()->user()->msnv,
|
||||
'date' => Carbon::today()
|
||||
]);
|
||||
$user->card += $request->num_card;
|
||||
}
|
||||
|
||||
$user->flag_send = $request->has('flag_send') ? User::FLAG_SEND_ENABLED : User::FLAG_SEND_DISABLED;
|
||||
$user->save();
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Cập nhật thành công!');
|
||||
}
|
||||
|
||||
public function destroy($msnv)
|
||||
{
|
||||
$user = User::where('msnv', $msnv)->firstOrFail();
|
||||
$user->status = User::STATUS_INACTIVE;
|
||||
$user->save();
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', 'Đã đánh dấu nhân viên nghỉ việc thành công!');
|
||||
}
|
||||
|
||||
public function resetCards()
|
||||
{
|
||||
User::where('status', User::STATUS_ACTIVE)->update(['card' => 0]);
|
||||
return redirect()->back()->with('success', 'Reset thẻ toàn hệ thống thành công!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\User;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function showLoginForm()
|
||||
{
|
||||
if (Auth::check()) {
|
||||
return $this->redirectBasedOnRole(Auth::user());
|
||||
}
|
||||
return view('login');
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'mail' => 'required|email',
|
||||
'password' => 'required'
|
||||
]);
|
||||
|
||||
if (Auth::attempt(['mail' => $credentials['mail'], 'password' => $credentials['password']])) {
|
||||
$request->session()->regenerate();
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->status != User::STATUS_ACTIVE) {
|
||||
Auth::logout();
|
||||
return back()->withErrors([
|
||||
'mail' => 'Tài khoản của bạn đã bị khóa hoặc bạn đã nghỉ việc.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectBasedOnRole($user);
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'mail' => 'Email hoặc mật khẩu không chính xác.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
private function redirectBasedOnRole($user)
|
||||
{
|
||||
if ($user->first_login == User::FIRST_LOGIN_TRUE) {
|
||||
return redirect()->route('user.change_password');
|
||||
}
|
||||
|
||||
if ($user->role == User::ROLE_ADMIN) {
|
||||
return redirect()->route('admin.dashboard');
|
||||
}
|
||||
|
||||
return redirect()->route('user.dashboard');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\User;
|
||||
use App\Models\Administration;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$selectedMonth = $request->input('month', Carbon::now()->format('Y-m'));
|
||||
$startOfMonth = Carbon::parse($selectedMonth)->startOfMonth();
|
||||
$endOfMonth = Carbon::parse($selectedMonth)->endOfMonth();
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
// Lấy tất cả card nhận và gửi của user này
|
||||
$administrations = Administration::where('msnv', $user->msnv)
|
||||
->whereBetween('date', [$startOfMonth, $endOfMonth])
|
||||
->orderBy('date', 'desc')
|
||||
->get();
|
||||
|
||||
return view('user.dashboard', compact('administrations', 'selectedMonth', 'user'));
|
||||
}
|
||||
|
||||
public function sendThankcards()
|
||||
{
|
||||
$users = User::where('status', User::STATUS_ACTIVE)->where('msnv', '!=', Auth::user()->msnv)->get();
|
||||
return view('user.send', compact('users'));
|
||||
}
|
||||
|
||||
public function storeThankcards(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'receiver' => 'required|exists:user,msnv',
|
||||
'amount' => 'required|integer|min:1|max:' . Administration::MAX_SEND_CARD_PER_MONTH
|
||||
]);
|
||||
|
||||
$sender = Auth::user();
|
||||
$receiverMsnv = $request->receiver;
|
||||
$amount = $request->amount;
|
||||
|
||||
if ($sender->flag_send == User::FLAG_SEND_DISABLED) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Bạn hiện không có quyền gửi Thank Card.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
$startOfMonth = Carbon::now()->startOfMonth();
|
||||
$endOfMonth = Carbon::now()->endOfMonth();
|
||||
|
||||
// Tính tổng số lượng card ĐÃ gửi cho người này trong tháng
|
||||
$cardsSentToThisUserThisMonth = Administration::where('msnv', $sender->msnv)
|
||||
->where('receiver', $receiverMsnv)
|
||||
->whereBetween('date', [$startOfMonth, $endOfMonth])
|
||||
->sum('sent');
|
||||
|
||||
if ($cardsSentToThisUserThisMonth + $amount > Administration::MAX_SEND_CARD_PER_MONTH) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => "Một tháng bạn chỉ được gửi tối đa " . Administration::MAX_SEND_CARD_PER_MONTH . " card cho một người. Bạn đã gửi {$cardsSentToThisUserThisMonth} card cho nhân viên này."
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Validate số lượng thẻ còn lại
|
||||
if ($sender->card < $amount) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Số lượng card của bạn không đủ. Vui lòng liên hệ Admin.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($sender, $receiverMsnv, $amount) {
|
||||
// 1. Insert thông tin người nhận
|
||||
Administration::create([
|
||||
'msnv' => $receiverMsnv,
|
||||
'received' => $amount,
|
||||
'sender' => $sender->msnv,
|
||||
'sent' => 0,
|
||||
'receiver' => null,
|
||||
'date' => Carbon::today()
|
||||
]);
|
||||
|
||||
// 2. Insert thông tin người gửi
|
||||
Administration::create([
|
||||
'msnv' => $sender->msnv,
|
||||
'received' => 0,
|
||||
'sender' => null,
|
||||
'sent' => $amount,
|
||||
'receiver' => $receiverMsnv,
|
||||
'date' => Carbon::today()
|
||||
]);
|
||||
|
||||
// 3. Trừ số card của người gửi
|
||||
$sender->card -= $amount;
|
||||
$sender->save();
|
||||
|
||||
// TODO: Bắn tin nhắn CO tới người nhận (Chatwork/System Notification)
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Đã gửi Thank card thành công!'
|
||||
]);
|
||||
}
|
||||
|
||||
public function changePasswordForm()
|
||||
{
|
||||
return view('user.change_password');
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'password' => 'required|min:6|confirmed'
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$user->pass = Hash::make($request->password);
|
||||
$user->first_login = User::FIRST_LOGIN_FALSE;
|
||||
$user->save();
|
||||
|
||||
if ($user->role == User::ROLE_ADMIN) {
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Đổi mật khẩu thành công!');
|
||||
}
|
||||
|
||||
return redirect()->route('user.dashboard')->with('success', 'Đổi mật khẩu thành công!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\User;
|
||||
|
||||
class AdminMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (Auth::check() && Auth::user()->role == User::ROLE_ADMIN && Auth::user()->status == User::STATUS_ACTIVE) {
|
||||
return $next($request);
|
||||
}
|
||||
return redirect()->route('login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\User;
|
||||
|
||||
class ForceChangePasswordMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (Auth::check() && Auth::user()->first_login == User::FIRST_LOGIN_TRUE) {
|
||||
if (!$request->routeIs('user.change_password') && !$request->routeIs('user.update_password') && !$request->routeIs('logout')) {
|
||||
return redirect()->route('user.change_password');
|
||||
}
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\User;
|
||||
|
||||
class MemberMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (Auth::check() && Auth::user()->role == User::ROLE_MEMBER && Auth::user()->status == User::STATUS_ACTIVE) {
|
||||
return $next($request);
|
||||
}
|
||||
return redirect()->route('login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AddCard extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'add_card';
|
||||
|
||||
protected $fillable = [
|
||||
'buyer',
|
||||
'num_card',
|
||||
'seller',
|
||||
'date',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Administration extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
const MAX_SEND_CARD_PER_MONTH = 5;
|
||||
|
||||
|
||||
protected $table = 'administration';
|
||||
|
||||
protected $fillable = [
|
||||
'msnv',
|
||||
'received',
|
||||
'sender',
|
||||
'sent',
|
||||
'receiver',
|
||||
'date',
|
||||
];
|
||||
}
|
||||
+31
-24
@@ -2,48 +2,55 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
const ROLE_MEMBER = 0;
|
||||
const ROLE_ADMIN = 1;
|
||||
|
||||
const STATUS_INACTIVE = 0;
|
||||
const STATUS_ACTIVE = 1;
|
||||
|
||||
const FLAG_SEND_DISABLED = 0;
|
||||
const FLAG_SEND_ENABLED = 1;
|
||||
|
||||
const FIRST_LOGIN_FALSE = 0;
|
||||
const FIRST_LOGIN_TRUE = 1;
|
||||
|
||||
|
||||
protected $table = 'user';
|
||||
protected $primaryKey = 'msnv';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'msnv',
|
||||
'mail',
|
||||
'pass',
|
||||
'role',
|
||||
'status',
|
||||
'card',
|
||||
'flag_send',
|
||||
'first_login',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'pass',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
* Get the password for the user.
|
||||
*
|
||||
* @return array<string, string>
|
||||
* @return string
|
||||
*/
|
||||
protected function casts(): array
|
||||
public function getAuthPassword()
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
return $this->pass;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -11,7 +11,11 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->alias([
|
||||
'admin' => \App\Http\Middleware\AdminMiddleware::class,
|
||||
'member' => \App\Http\Middleware\MemberMiddleware::class,
|
||||
'force_change_password' => \App\Http\Middleware\ForceChangePasswordMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
'locale' => env('APP_LOCALE', 'vi'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
|
||||
@@ -11,30 +11,20 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
Schema::create('user', function (Blueprint $table) {
|
||||
$table->string('msnv')->primary();
|
||||
$table->string('mail');
|
||||
$table->string('pass');
|
||||
$table->tinyInteger('role')->default(0)->comment('0: member / 1: admin');
|
||||
$table->tinyInteger('status')->default(1)->comment('0: đã nghỉ / 1: đang làm');
|
||||
$table->integer('card')->nullable();
|
||||
$table->tinyInteger('flag_send')->default(0)->comment('0: không được gửi / 1: được gửi');
|
||||
$table->tinyInteger('first_login')->default(1)->comment('0: không là lần đầu / 1: lần đầu');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
// Only user table remains
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,8 +32,6 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
Schema::dropIfExists('user');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('add_card', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('buyer');
|
||||
$table->integer('num_card');
|
||||
$table->string('seller');
|
||||
$table->date('date')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('add_card');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('administration', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('msnv');
|
||||
$table->integer('received')->nullable();
|
||||
$table->string('sender')->nullable();
|
||||
$table->integer('sent')->nullable();
|
||||
$table->string('receiver')->nullable();
|
||||
$table->date('date')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('administration');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\User;
|
||||
use App\Models\Administration;
|
||||
use App\Models\AddCard;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MockDataSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Clear old data to avoid duplicates when running again
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
User::truncate();
|
||||
Administration::truncate();
|
||||
AddCard::truncate();
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$password = Hash::make('123456');
|
||||
|
||||
// 1. CREATE USERS
|
||||
// Admins
|
||||
User::create([
|
||||
'msnv' => 'ADMIN',
|
||||
'mail' => 'admin@runsystem.net',
|
||||
'pass' => $password,
|
||||
'role' => User::ROLE_ADMIN,
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'card' => 0,
|
||||
'flag_send' => User::FLAG_SEND_ENABLED,
|
||||
'first_login' => User::FIRST_LOGIN_FALSE,
|
||||
]);
|
||||
|
||||
User::create([
|
||||
'msnv' => 'ADMIN2',
|
||||
'mail' => 'admin2@runsystem.net',
|
||||
'pass' => $password,
|
||||
'role' => User::ROLE_ADMIN,
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'card' => 0,
|
||||
'flag_send' => User::FLAG_SEND_ENABLED,
|
||||
'first_login' => User::FIRST_LOGIN_FALSE,
|
||||
]);
|
||||
|
||||
// Active Users (Initialize with specific users needed for logs/transactions)
|
||||
$users = [
|
||||
['msnv' => 'DEV001', 'mail' => 'nguyenvana@runsystem.net', 'card' => 15, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
['msnv' => 'DEV002', 'mail' => 'tranthingoc@runsystem.net', 'card' => 3, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
['msnv' => 'TEST01', 'mail' => 'lekiemthu@runsystem.net', 'card' => 8, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_TRUE], // First-time login
|
||||
['msnv' => 'HR001', 'mail' => 'phongnhansu@runsystem.net', 'card' => 0, 'flag' => User::FLAG_SEND_DISABLED, 'first_login' => User::FIRST_LOGIN_FALSE], // Disabled sending permission
|
||||
['msnv' => 'SALE01', 'mail' => 'bangiang@runsystem.net', 'card' => 50, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
];
|
||||
|
||||
// Generate additional users to reach exactly 100 member users
|
||||
for ($i = 3; $i <= 97; $i++) {
|
||||
$numStr = str_pad($i, 3, '0', STR_PAD_LEFT);
|
||||
$users[] = [
|
||||
'msnv' => 'DEV' . $numStr,
|
||||
'mail' => 'dev' . $numStr . '@runsystem.net',
|
||||
'card' => rand(0, 30),
|
||||
'flag' => rand(0, 5) > 0 ? User::FLAG_SEND_ENABLED : User::FLAG_SEND_DISABLED,
|
||||
'first_login' => rand(0, 4) == 0 ? User::FIRST_LOGIN_TRUE : User::FIRST_LOGIN_FALSE
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($users as $u) {
|
||||
User::create([
|
||||
'msnv' => $u['msnv'],
|
||||
'mail' => $u['mail'],
|
||||
'pass' => $password,
|
||||
'role' => User::ROLE_MEMBER,
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'card' => $u['card'],
|
||||
'flag_send' => $u['flag'],
|
||||
'first_login' => $u['first_login'],
|
||||
]);
|
||||
}
|
||||
|
||||
// Inactive User
|
||||
User::create([
|
||||
'msnv' => 'OLD001',
|
||||
'mail' => 'nghiduy@runsystem.net',
|
||||
'pass' => $password,
|
||||
'role' => User::ROLE_MEMBER,
|
||||
'status' => User::STATUS_INACTIVE,
|
||||
'card' => 0,
|
||||
'flag_send' => User::FLAG_SEND_DISABLED,
|
||||
'first_login' => User::FIRST_LOGIN_FALSE,
|
||||
]);
|
||||
|
||||
// 2. CREATE CARD ADDITION LOGS (add_card table)
|
||||
// Get the first admin (id = 1) as the loader
|
||||
$adminId = 1;
|
||||
$now = Carbon::now();
|
||||
|
||||
$addCardLogs = [
|
||||
['buyer' => 'DEV001', 'num_card' => 20, 'date' => clone $now],
|
||||
['buyer' => 'DEV002', 'num_card' => 10, 'date' => clone $now],
|
||||
['buyer' => 'SALE01', 'num_card' => 50, 'date' => (clone $now)->subDays(2)],
|
||||
['buyer' => 'TEST01', 'num_card' => 10, 'date' => (clone $now)->subMonth()], // Last month
|
||||
];
|
||||
|
||||
foreach ($addCardLogs as $index => $log) {
|
||||
AddCard::create([
|
||||
'buyer' => 100 + $index, // Dummy int ID because the DB column is designed as int instead of string msnv
|
||||
'num_card' => $log['num_card'],
|
||||
'seller' => $adminId,
|
||||
'date' => $log['date']
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. CREATE CARD SENDING HISTORY (administration table)
|
||||
// Create transactions to populate Top Received / Top Sent for the month
|
||||
$transactions = [
|
||||
// Current month transactions
|
||||
['sender' => 'DEV001', 'receiver' => 'DEV002', 'amount' => 5, 'date' => clone $now],
|
||||
['sender' => 'DEV001', 'receiver' => 'TEST01', 'amount' => 3, 'date' => (clone $now)->subDays(1)],
|
||||
['sender' => 'SALE01', 'receiver' => 'DEV002', 'amount' => 4, 'date' => (clone $now)->subDays(2)],
|
||||
['sender' => 'TEST01', 'receiver' => 'HR001', 'amount' => 2, 'date' => (clone $now)->subDays(3)],
|
||||
['sender' => 'DEV002', 'receiver' => 'DEV001', 'amount' => 1, 'date' => clone $now],
|
||||
|
||||
// Last month transactions
|
||||
['sender' => 'DEV001', 'receiver' => 'SALE01', 'amount' => 5, 'date' => (clone $now)->subMonth()],
|
||||
['sender' => 'HR001', 'receiver' => 'DEV001', 'amount' => 5, 'date' => (clone $now)->subMonth()->subDays(5)],
|
||||
|
||||
// July 2026 transactions (Diverse Mock Data)
|
||||
['sender' => 'DEV001', 'receiver' => 'SALE01', 'amount' => 4, 'date' => Carbon::create(2026, 7, 5)],
|
||||
['sender' => 'SALE01', 'receiver' => 'TEST01', 'amount' => 2, 'date' => Carbon::create(2026, 7, 12)],
|
||||
['sender' => 'DEV002', 'receiver' => 'HR001', 'amount' => 1, 'date' => Carbon::create(2026, 7, 18)],
|
||||
['sender' => 'TEST01', 'receiver' => 'DEV001', 'amount' => 3, 'date' => Carbon::create(2026, 7, 22)],
|
||||
['sender' => 'HR001', 'receiver' => 'DEV002', 'amount' => 5, 'date' => Carbon::create(2026, 7, 28)],
|
||||
['sender' => 'SALE01', 'receiver' => 'DEV001', 'amount' => 2, 'date' => Carbon::create(2026, 7, 30)],
|
||||
['sender' => 'DEV001', 'receiver' => 'DEV002', 'amount' => 3, 'date' => Carbon::create(2026, 7, 15)],
|
||||
['sender' => 'TEST01', 'receiver' => 'SALE01', 'amount' => 4, 'date' => Carbon::create(2026, 7, 8)],
|
||||
];
|
||||
|
||||
foreach ($transactions as $t) {
|
||||
// Sender record
|
||||
Administration::create([
|
||||
'msnv' => $t['sender'],
|
||||
'received' => 0,
|
||||
'sender' => null,
|
||||
'sent' => $t['amount'],
|
||||
'receiver' => $t['receiver'],
|
||||
'date' => $t['date']->format('Y-m-d')
|
||||
]);
|
||||
|
||||
// Receiver record
|
||||
Administration::create([
|
||||
'msnv' => $t['receiver'],
|
||||
'received' => $t['amount'],
|
||||
'sender' => $t['sender'],
|
||||
'sent' => 0,
|
||||
'receiver' => null,
|
||||
'date' => $t['date']->format('Y-m-d')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'login' => 'Login',
|
||||
'email' => 'Email',
|
||||
'email_placeholder' => 'Enter your email',
|
||||
'password' => 'Password',
|
||||
'password_placeholder' => 'Enter your password',
|
||||
'forgot_password' => 'Forgot password?',
|
||||
'system_name' => 'Thank Card System',
|
||||
'developed_by' => 'Developed by GMO-Z.com RUNSYSTEM',
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'manage_users' => 'Manage Users',
|
||||
'add_user' => 'Add User',
|
||||
'my_page' => 'My Page',
|
||||
'send_card' => 'Send Thank Card',
|
||||
'change_password' => 'Change Password',
|
||||
'logout' => 'Logout',
|
||||
'have_questions' => 'Have questions?',
|
||||
'feedback' => 'Feedback',
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'login' => 'Đăng nhập',
|
||||
'email' => 'Email',
|
||||
'email_placeholder' => 'Nhập email của bạn',
|
||||
'password' => 'Mật khẩu',
|
||||
'password_placeholder' => 'Nhập mật khẩu',
|
||||
'forgot_password' => 'Quên mật khẩu?',
|
||||
'system_name' => 'Hệ thống Thank Card',
|
||||
'developed_by' => 'Được phát triển bởi GMO-Z.com RUNSYSTEM',
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'manage_users' => 'Quản lý User',
|
||||
'add_user' => 'Thêm User',
|
||||
'my_page' => 'My Page',
|
||||
'send_card' => 'Gửi Thank Card',
|
||||
'change_password' => 'Đổi mật khẩu',
|
||||
'logout' => 'Đăng xuất',
|
||||
'have_questions' => 'Bạn có thắc mắc?',
|
||||
'feedback' => 'Phản hồi',
|
||||
];
|
||||
Generated
+2771
File diff suppressed because it is too large
Load Diff
@@ -13,5 +13,9 @@
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.2.10",
|
||||
"puppeteer": "^25.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
+107
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
// 1. Update app/Models/User.php
|
||||
$content = file_get_contents('app/Models/User.php');
|
||||
$constants = "
|
||||
const ROLE_MEMBER = 0;
|
||||
const ROLE_ADMIN = 1;
|
||||
|
||||
const STATUS_INACTIVE = 0;
|
||||
const STATUS_ACTIVE = 1;
|
||||
|
||||
const FLAG_SEND_DISABLED = 0;
|
||||
const FLAG_SEND_ENABLED = 1;
|
||||
|
||||
const FIRST_LOGIN_FALSE = 0;
|
||||
const FIRST_LOGIN_TRUE = 1;
|
||||
";
|
||||
$content = str_replace('use HasFactory, Notifiable;', "use HasFactory, Notifiable;\n" . $constants, $content);
|
||||
file_put_contents('app/Models/User.php', $content);
|
||||
|
||||
// 2. Update app/Models/Administration.php
|
||||
$content = file_get_contents('app/Models/Administration.php');
|
||||
$constants = "
|
||||
const MAX_SEND_CARD_PER_MONTH = 5;
|
||||
";
|
||||
$content = str_replace('use HasFactory;', "use HasFactory;\n" . $constants, $content);
|
||||
file_put_contents('app/Models/Administration.php', $content);
|
||||
|
||||
// 3. Update app/Http/Middleware/AdminMiddleware.php
|
||||
$content = file_get_contents('app/Http/Middleware/AdminMiddleware.php');
|
||||
$content = str_replace('use Illuminate\Support\Facades\Auth;', "use Illuminate\Support\Facades\Auth;\nuse App\Models\User;", $content);
|
||||
$content = str_replace('Auth::user()->role == 1', 'Auth::user()->role == User::ROLE_ADMIN', $content);
|
||||
$content = str_replace('Auth::user()->status == 1', 'Auth::user()->status == User::STATUS_ACTIVE', $content);
|
||||
file_put_contents('app/Http/Middleware/AdminMiddleware.php', $content);
|
||||
|
||||
// 4. Update app/Http/Middleware/MemberMiddleware.php
|
||||
$content = file_get_contents('app/Http/Middleware/MemberMiddleware.php');
|
||||
$content = str_replace('use Illuminate\Support\Facades\Auth;', "use Illuminate\Support\Facades\Auth;\nuse App\Models\User;", $content);
|
||||
$content = str_replace('Auth::user()->role == 0', 'Auth::user()->role == User::ROLE_MEMBER', $content);
|
||||
$content = str_replace('Auth::user()->status == 1', 'Auth::user()->status == User::STATUS_ACTIVE', $content);
|
||||
file_put_contents('app/Http/Middleware/MemberMiddleware.php', $content);
|
||||
|
||||
// 5. Update app/Http/Middleware/ForceChangePasswordMiddleware.php
|
||||
$content = file_get_contents('app/Http/Middleware/ForceChangePasswordMiddleware.php');
|
||||
$content = str_replace('use Illuminate\Support\Facades\Auth;', "use Illuminate\Support\Facades\Auth;\nuse App\Models\User;", $content);
|
||||
$content = str_replace('Auth::user()->first_login == 1', 'Auth::user()->first_login == User::FIRST_LOGIN_TRUE', $content);
|
||||
file_put_contents('app/Http/Middleware/ForceChangePasswordMiddleware.php', $content);
|
||||
|
||||
// 6. Update app/Http/Controllers/AuthController.php
|
||||
$content = file_get_contents('app/Http/Controllers/AuthController.php');
|
||||
$content = str_replace('use Illuminate\Support\Facades\Auth;', "use Illuminate\Support\Facades\Auth;\nuse App\Models\User;", $content);
|
||||
$content = str_replace('$user->status != 1', '$user->status != User::STATUS_ACTIVE', $content);
|
||||
$content = str_replace('$user->first_login == 1', '$user->first_login == User::FIRST_LOGIN_TRUE', $content);
|
||||
$content = str_replace('$user->role == 1', '$user->role == User::ROLE_ADMIN', $content);
|
||||
file_put_contents('app/Http/Controllers/AuthController.php', $content);
|
||||
|
||||
// 7. Update app/Http/Controllers/AdminController.php
|
||||
$content = file_get_contents('app/Http/Controllers/AdminController.php');
|
||||
$content = str_replace("where('status', 1)", "where('status', User::STATUS_ACTIVE)", $content);
|
||||
$content = str_replace("'role' => 'required|in:0,1'", "'role' => 'required|in:' . User::ROLE_MEMBER . ',' . User::ROLE_ADMIN", $content);
|
||||
$content = str_replace("'status' => 1", "'status' => User::STATUS_ACTIVE", $content);
|
||||
$content = str_replace("'flag_send' => 0", "'flag_send' => User::FLAG_SEND_DISABLED", $content);
|
||||
$content = str_replace("'first_login' => 1", "'first_login' => User::FIRST_LOGIN_TRUE", $content);
|
||||
$content = str_replace("\$request->has('flag_send') ? 1 : 0", "\$request->has('flag_send') ? User::FLAG_SEND_ENABLED : User::FLAG_SEND_DISABLED", $content);
|
||||
$content = str_replace("\$user->status = 0", "\$user->status = User::STATUS_INACTIVE", $content);
|
||||
file_put_contents('app/Http/Controllers/AdminController.php', $content);
|
||||
|
||||
// 8. Update app/Http/Controllers/UserController.php
|
||||
$content = file_get_contents('app/Http/Controllers/UserController.php');
|
||||
$content = str_replace("where('status', 1)", "where('status', User::STATUS_ACTIVE)", $content);
|
||||
$content = str_replace("max:5'", "max:' . Administration::MAX_SEND_CARD_PER_MONTH", $content);
|
||||
$content = str_replace("\$sender->flag_send == 0", "\$sender->flag_send == User::FLAG_SEND_DISABLED", $content);
|
||||
$content = str_replace("> 5)", "> Administration::MAX_SEND_CARD_PER_MONTH)", $content);
|
||||
$content = str_replace("tối đa 5 card", 'tối đa " . Administration::MAX_SEND_CARD_PER_MONTH . " card', $content);
|
||||
$content = str_replace("\$user->first_login = 0", "\$user->first_login = User::FIRST_LOGIN_FALSE", $content);
|
||||
$content = str_replace("\$user->role == 1", "\$user->role == User::ROLE_ADMIN", $content);
|
||||
file_put_contents('app/Http/Controllers/UserController.php', $content);
|
||||
|
||||
// 9. Update app/Console/Commands/ResetCardsCommand.php
|
||||
$content = file_get_contents('app/Console/Commands/ResetCardsCommand.php');
|
||||
$content = str_replace("where('status', 1)", "where('status', User::STATUS_ACTIVE)", $content);
|
||||
file_put_contents('app/Console/Commands/ResetCardsCommand.php', $content);
|
||||
|
||||
// 10. Update resources/views/layouts/app.blade.php
|
||||
$content = file_get_contents('resources/views/layouts/app.blade.php');
|
||||
$content = str_replace("Auth::user()->role == 1", "Auth::user()->role == \App\Models\User::ROLE_ADMIN", $content);
|
||||
file_put_contents('resources/views/layouts/app.blade.php', $content);
|
||||
|
||||
// 11. Update resources/views/admin/users/create.blade.php
|
||||
$content = file_get_contents('resources/views/admin/users/create.blade.php');
|
||||
$content = str_replace('value="0"', 'value="{{ \App\Models\User::ROLE_MEMBER }}"', $content);
|
||||
$content = str_replace('value="1"', 'value="{{ \App\Models\User::ROLE_ADMIN }}"', $content);
|
||||
$content = str_replace("old('role') == '0'", "old('role') == \App\Models\User::ROLE_MEMBER", $content);
|
||||
$content = str_replace("old('role') == '1'", "old('role') == \App\Models\User::ROLE_ADMIN", $content);
|
||||
file_put_contents('resources/views/admin/users/create.blade.php', $content);
|
||||
|
||||
// 12. Update resources/views/admin/users/edit.blade.php
|
||||
$content = file_get_contents('resources/views/admin/users/edit.blade.php');
|
||||
$content = str_replace("\$user->role == 1", "\$user->role == \App\Models\User::ROLE_ADMIN", $content);
|
||||
file_put_contents('resources/views/admin/users/edit.blade.php', $content);
|
||||
|
||||
// 13. Update resources/views/user/change_password.blade.php
|
||||
$content = file_get_contents('resources/views/user/change_password.blade.php');
|
||||
$content = str_replace("Auth::user()->first_login == 1", "Auth::user()->first_login == \App\Models\User::FIRST_LOGIN_TRUE", $content);
|
||||
file_put_contents('resources/views/user/change_password.blade.php', $content);
|
||||
|
||||
echo "Refactored successfully!";
|
||||
+241
-2
@@ -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">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
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::command('cards:reset')->monthlyOn(1, '00:00');
|
||||
|
||||
+45
-1
@@ -1,7 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\AdminController;
|
||||
use App\Http\Controllers\UserController;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
return redirect()->route('login');
|
||||
});
|
||||
|
||||
Route::get('/test-modal', function() {
|
||||
return view('admin.users.index', [
|
||||
'users' => [],
|
||||
'selectedMonth' => '2026-06',
|
||||
'topReceivedUser' => null,
|
||||
'topSentUser' => null
|
||||
]);
|
||||
});
|
||||
Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
||||
|
||||
Route::middleware('force_change_password')->group(function () {
|
||||
|
||||
Route::middleware('admin')->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/', [AdminController::class, 'index'])->name('dashboard');
|
||||
Route::get('/users', [AdminController::class, 'index'])->name('users.index');
|
||||
|
||||
Route::get('/users/create', [AdminController::class, 'create'])->name('users.create');
|
||||
Route::post('/users', [AdminController::class, 'store'])->name('users.store');
|
||||
Route::get('/users/{msnv}/edit', [AdminController::class, 'edit'])->name('users.edit');
|
||||
Route::put('/users/{msnv}', [AdminController::class, 'update'])->name('users.update');
|
||||
Route::delete('/users/{msnv}', [AdminController::class, 'destroy'])->name('users.destroy');
|
||||
|
||||
Route::post('/reset-cards', [AdminController::class, 'resetCards'])->name('reset_cards');
|
||||
});
|
||||
|
||||
Route::middleware('member')->group(function () {
|
||||
Route::get('/my-page', [UserController::class, 'index'])->name('user.dashboard');
|
||||
|
||||
Route::get('/send-thankcards', [UserController::class, 'sendThankcards'])->name('user.send');
|
||||
Route::post('/send-thankcards', [UserController::class, 'storeThankcards'])->name('user.store_send');
|
||||
});
|
||||
});
|
||||
|
||||
Route::get('/change-password', [UserController::class, 'changePasswordForm'])->name('user.change_password');
|
||||
Route::post('/change-password', [UserController::class, 'updatePassword'])->name('user.update_password');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
|
||||
const page = await browser.newPage();
|
||||
await page.goto('http://127.0.0.1:8000/test-modal');
|
||||
|
||||
// Make modal visible for accurate computed CSS
|
||||
await page.evaluate(() => {
|
||||
const modal = document.getElementById('deleteModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
|
||||
const data = await page.evaluate(() => {
|
||||
const modal = document.getElementById('deleteModal');
|
||||
if (!modal) return { error: 'Modal not found' };
|
||||
|
||||
// 1. DOM Path
|
||||
let current = modal;
|
||||
let path = [];
|
||||
while (current && current.nodeType === 1) {
|
||||
let desc = current.tagName.toLowerCase();
|
||||
if (current.id) desc += '#' + current.id;
|
||||
if (current.className && typeof current.className === 'string') {
|
||||
desc += '.' + current.className.split(' ').join('.');
|
||||
}
|
||||
path.unshift(desc);
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
// 2. Computed CSS for modal
|
||||
const getStyles = (el) => {
|
||||
const cs = window.getComputedStyle(el);
|
||||
return {
|
||||
position: cs.position,
|
||||
top: cs.top,
|
||||
right: cs.right,
|
||||
bottom: cs.bottom,
|
||||
left: cs.left,
|
||||
display: cs.display,
|
||||
zIndex: cs.zIndex,
|
||||
transform: cs.transform !== 'none' ? cs.transform : 'none',
|
||||
contain: cs.contain !== 'none' ? cs.contain : 'none',
|
||||
filter: cs.filter !== 'none' ? cs.filter : 'none',
|
||||
backdropFilter: cs.backdropFilter !== 'none' ? cs.backdropFilter : 'none',
|
||||
perspective: cs.perspective !== 'none' ? cs.perspective : 'none',
|
||||
overflow: cs.overflow,
|
||||
overflowY: cs.overflowY,
|
||||
willChange: cs.willChange !== 'auto' ? cs.willChange : 'auto'
|
||||
};
|
||||
};
|
||||
|
||||
const modalCSS = getStyles(modal);
|
||||
|
||||
// 4. Ancestor trace
|
||||
let ancestors = [];
|
||||
current = modal.parentNode;
|
||||
while (current && current.nodeType === 1) {
|
||||
const cs = window.getComputedStyle(current);
|
||||
const hasContext = cs.transform !== 'none' || cs.filter !== 'none' || cs.perspective !== 'none' || cs.contain !== 'none' || cs.willChange !== 'auto' || cs.position !== 'static' || cs.zIndex !== 'auto' || cs.isolation !== 'auto' || cs.overflow !== 'visible';
|
||||
|
||||
if (hasContext) {
|
||||
ancestors.push({
|
||||
tagName: current.tagName,
|
||||
id: current.id,
|
||||
className: current.className,
|
||||
position: cs.position,
|
||||
zIndex: cs.zIndex,
|
||||
transform: cs.transform !== 'none' ? cs.transform : 'none',
|
||||
filter: cs.filter !== 'none' ? cs.filter : 'none',
|
||||
perspective: cs.perspective !== 'none' ? cs.perspective : 'none',
|
||||
contain: cs.contain !== 'none' ? cs.contain : 'none',
|
||||
willChange: cs.willChange !== 'auto' ? cs.willChange : 'auto',
|
||||
overflow: cs.overflow,
|
||||
isolation: cs.isolation !== 'auto' ? cs.isolation : 'auto'
|
||||
});
|
||||
}
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
// Find element that has large scrollHeight
|
||||
const bodyHeight = document.body.scrollHeight;
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
const scrollContainer = document.querySelector('.overflow-y-auto');
|
||||
|
||||
return {
|
||||
path,
|
||||
modalCSS,
|
||||
ancestors,
|
||||
scrollHeights: {
|
||||
body: bodyHeight,
|
||||
mainContent: mainContent ? mainContent.scrollHeight : null,
|
||||
scrollContainer: scrollContainer ? scrollContainer.scrollHeight : null
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
Reference in New Issue
Block a user