Compare commits

..

No commits in common. "master" and "Sprint-4/MS-37-FE-Evaluation" have entirely different histories.

273 changed files with 2979 additions and 43205 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -3,7 +3,6 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Modules\Admin\app\Models\Category;
@ -15,7 +14,7 @@ class CategoryController extends Controller
* @param Request $request The HTTP request object.
* @return \Illuminate\Http\JsonResponse The JSON response containing the list of master data.
*/
public static function getListMaster(Request $request)
public function getListMaster(Request $request)
{
$data = Category::where('c_type', '=', $request->type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
return AbstractController::ResultSuccess($data);
@ -25,50 +24,4 @@ class CategoryController extends Controller
$data = Category::where('c_type', '=', $type)->where('c_code', '=', $code)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->first();
return $data;
}
public static function getListMasterByType($type)
{
$data = Category::where('c_type', '=', $type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
return $data;
}
public function workDays()
{
$saturday_work_schedules = Category::where('c_type', 'SATURDAY_WORK_SCHEDULE')
->get();
return response()->json([
'data' => $saturday_work_schedules,
'status' => true
]);
}
public function updateWorkDays(Request $request)
{
$request->validate([
'c_code' => 'required|date_format:d-m-Y',
]);
$schedule = Category::where('c_type', 'SATURDAY_WORK_SCHEDULE')->first();
if (!$schedule) {
$schedule = Category::create([
'c_type' => 'SATURDAY_WORK_SCHEDULE',
'c_name' => "Ngày bắt đầu làm việc thứ 7 trong năm",
'c_code' => $request->c_code,
'c_value' => Carbon::now()->year,
'c_active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
} else {
$schedule->update([
'c_code' => $request->c_code,
]);
}
return response()->json([
'status' => true,
'message' => 'Saturday work schedule updated successfully'
]);
}
}

View File

@ -1,161 +0,0 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Models\Document;
class DocumentController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
public function all(Request $request)
{
$documents = new Document;
// Order by
$this->orderByRequest($documents, $request);
$documents->orderBy('title', 'asc');
// Filter
$this->filterRequest(
builder: $documents,
request: $request,
filterKeys: [
'title' => self::F_TEXT,
]
);
$this->searchRequest(
builder: $documents,
value: $request->get('search'),
fields: [
'title',
]
);
$responseData = $documents->get();
return AbstractController::ResultSuccess($responseData);
}
public function create(Request $request)
{
$request->validate([
'type' => 'required|in:file,link',
'files' => 'nullable|array',
'files.*.title' => 'required|string|max:255',
'files.*.file' => 'required|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
'links' => 'nullable|array',
'links.*.title' => 'required|string|max:255',
'links.*.uri' => 'required|string|url',
'is_active' => 'required|boolean',
]);
$documents = [];
if ($request->type === "file") {
foreach ($request->file('files') as $index => $file) {
$path = $file['file']->store('uploads', 'public');
$documents[] = [
'title' => $request->input("files.$index.title"),
'type' => 'file',
'uri' => "storage/{$path}",
'is_active' => $request->is_active,
'created_at' => now(),
'updated_at' => now(),
];
}
} else {
foreach ($request->links as $link) {
$documents[] = [
'title' => $link['title'],
'type' => 'link',
'uri' => $link['uri'],
'is_active' => $request->is_active,
'created_at' => now(),
'updated_at' => now(),
];
}
}
if (!empty($documents)) {
Document::insert($documents);
}
return AbstractController::ResultSuccess($documents, "Documents created successfully!");
}
public function update(Request $request)
{
$request->validate([
'id' => 'required|exists:documents,id',
'title' => 'required|string|max:255',
'type' => 'required|in:file,link',
'uri' => 'nullable|url',
'file' => 'nullable|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
'existing_file' => 'nullable|string',
'is_active' => 'required|boolean',
]);
$document = Document::find($request->input('id'));
if (!$document) {
return AbstractController::ResultError("Document not found.");
}
if ($request->type === "file") {
$uri = $request->existing_file;
if ($request->hasFile('file')) {
$filePath = str_replace('storage/', 'public/', $request->existing_file);
if (Storage::exists($filePath)) {
Storage::delete($filePath);
}
$path = $request->file('file')->store('uploads', 'public');
$uri = "storage/{$path}";
}
$document->update([
'title' => $request->title,
'type' => $request->type,
'uri' => $uri,
'is_active' => $request->is_active,
]);
return AbstractController::ResultSuccess($document, "Document updated successfully!");
}
$document->update([
'title' => $request->title,
'type' => $request->type,
'uri' => $request->uri,
'is_active' => $request->is_active,
]);
return AbstractController::ResultSuccess($document, "Document updated successfully!");
}
public function delete(Request $request)
{
$id = $request->input('id');
$document = Document::find($id);
if (!$document) {
return AbstractController::ResultError("Document not found");
}
if ($document->type === "file") {
$filePath = str_replace('storage/', 'public/', $document->uri);
if (Storage::exists($filePath)) {
Storage::delete($filePath);
}
}
$document->delete();
return AbstractController::ResultSuccess("Document deleted successfully!");
}
}

View File

@ -1,359 +0,0 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Modules\Admin\app\Models\ElectricityBill;
use Illuminate\Support\Facades\Log;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use Illuminate\Support\Facades\Storage;
class ElectricityBillController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
/**
* Get all electricity bills with pagination
*/
public function index(Request $request)
{
try {
$bills = new ElectricityBill;
// Order by
$this->orderByRequest($bills, $request);
// Filter
$this->filterRequest(
builder: $bills,
request: $request,
filterKeys: [
'billing_date' => self::F_TEXT,
]
);
// Search
$this->searchRequest(
builder: $bills,
value: $request->get('search'),
fields: ['billing_date', 'notes']
);
$responseData = $bills
->leftJoin('users as creator', 'electricity_bills.created_by', '=', 'creator.id')
->leftJoin('users as updater', 'electricity_bills.updated_by', '=', 'updater.id')
->orderBy('electricity_bills.billing_date', 'desc')
->select(
'electricity_bills.*',
'creator.name as creator_name',
'updater.name as updater_name'
)
->paginate($request->get('per_page', 15));
return AbstractController::ResultSuccess($responseData);
} catch (\Exception $e) {
Log::error('Error fetching electricity bills: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Create new electricity bill
*/
public function create(Request $request)
{
try {
$validated = $request->validate([
'billing_date' => 'required|string',
'previous_reading' => 'required|numeric|min:0',
'current_reading' => 'required|numeric|min:0',
'unit_price' => 'required|numeric|min:0',
'notes' => 'nullable|string',
]);
// Check if billing_date already exists
$existingBill = ElectricityBill::where('billing_date', $validated['billing_date'])->first();
if ($existingBill) {
return AbstractController::ResultError('Bill for this month already exists', 422);
}
// Calculate total amount
$consumption = $validated['current_reading'] - $validated['previous_reading'];
$totalAmount = $consumption * $validated['unit_price'];
$bill = ElectricityBill::create([
'billing_date' => $validated['billing_date'],
'previous_reading' => $validated['previous_reading'],
'current_reading' => $validated['current_reading'],
'unit_price' => $validated['unit_price'],
'total_amount' => $totalAmount,
'notes' => $validated['notes'] ?? null,
'created_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($bill, 'Electricity bill created successfully');
} catch (\Exception $e) {
Log::error('Error creating electricity bill: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Update electricity bill
*/
public function update(Request $request, $id)
{
try {
$validated = $request->validate([
'billing_date' => 'sometimes|string',
'previous_reading' => 'sometimes|numeric|min:0',
'current_reading' => 'sometimes|numeric|min:0',
'unit_price' => 'sometimes|numeric|min:0',
'notes' => 'nullable|string',
]);
$bill = ElectricityBill::findOrFail($id);
// Check if billing_date already exists (excluding current record)
if (isset($validated['billing_date'])) {
$existingBill = ElectricityBill::where('billing_date', $validated['billing_date'])
->where('id', '!=', $id)
->first();
if ($existingBill) {
return AbstractController::ResultError('Bill for this month already exists', 422);
}
}
// Recalculate total if any reading or price changed
$previousReading = $validated['previous_reading'] ?? $bill->previous_reading;
$currentReading = $validated['current_reading'] ?? $bill->current_reading;
$unitPrice = $validated['unit_price'] ?? $bill->unit_price;
$consumption = $currentReading - $previousReading;
$totalAmount = $consumption * $unitPrice;
$bill->update(array_merge($validated, [
'total_amount' => $totalAmount,
'updated_by' => auth('admins')->user()->id ?? null,
]));
return AbstractController::ResultSuccess($bill, 'Electricity bill updated successfully');
} catch (\Exception $e) {
Log::error('Error updating electricity bill: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Delete electricity bill
*/
public function delete(Request $request, $id)
{
try {
$bill = ElectricityBill::findOrFail($id);
$bill->delete();
return AbstractController::ResultSuccess(null, 'Electricity bill deleted successfully');
} catch (\Exception $e) {
Log::error('Error deleting electricity bill: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Export electricity bill to PDF
*/
public function exportPdf(Request $request, $id)
{
try {
$bill = ElectricityBill::findOrFail($id);
// Get month name from billing_date
$consumption = $bill->current_reading - $bill->previous_reading;
$totalText = $this->numberToVietnamese($bill->total_amount);
$date = Carbon::parse($bill->billing_date);
$dateNow = 'Ngày ' . $date->day .
' tháng ' . $date->month .
' năm ' . $date->year;
// Generate PDF
$pdf = Pdf::loadView('admin::admin.electricity_bills.pdf', [
'bill' => $bill,
'consumption' => $consumption,
'dateNow' => $dateNow,
'totalText' => $totalText
]);
$fileName = 'electricity_bill_' . $bill->billing_date . '.pdf';
$filePath = 'electricity_bills/' . $fileName;
// đảm bảo folder tồn tại
if (!Storage::disk('public')->exists('electricity_bills')) {
Storage::disk('public')->makeDirectory('electricity_bills');
}
// 👇 render 1 lần
$pdfContent = $pdf->output();
// 👇 lưu file
Storage::disk('public')->put($filePath, $pdfContent);
// update DB
$bill->update(['file_path' => $filePath]);
// 👇 trả về đúng file đã tạo
return response($pdfContent)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="' . $fileName . '"');
} catch (\Exception $e) {
Log::error('Error exporting electricity bill to PDF: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Get electricity bill by ID
*/
public function show($id)
{
try {
$bill = ElectricityBill::with(['creator', 'updater'])->findOrFail($id);
return AbstractController::ResultSuccess($bill);
} catch (\Exception $e) {
Log::error('Error fetching electricity bill: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Upload an attachment (PDF or image) for an electricity bill
*/
public function uploadAttachment(Request $request, $id)
{
try {
$request->validate([
'file' => 'required|file|mimes:pdf,jpg,jpeg,png,gif,webp|max:10240',
]);
$bill = ElectricityBill::findOrFail($id);
$file = $request->file('file');
$extension = $file->getClientOriginalExtension();
$filename = 'bill_' . $bill->id . '_' . time() . '.' . $extension;
$directory = 'electricity_bills/attachments';
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory);
}
// Delete old attachment if exists
if ($bill->attachment_path && Storage::disk('public')->exists($bill->attachment_path)) {
Storage::disk('public')->delete($bill->attachment_path);
}
$path = $file->storeAs($directory, $filename, 'public');
$bill->update([
'attachment_path' => $path,
'updated_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($bill, 'Attachment uploaded successfully');
} catch (\Exception $e) {
Log::error('Error uploading electricity bill attachment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Delete the attachment of an electricity bill
*/
public function deleteAttachment($id)
{
try {
$bill = ElectricityBill::findOrFail($id);
if ($bill->attachment_path && Storage::disk('public')->exists($bill->attachment_path)) {
Storage::disk('public')->delete($bill->attachment_path);
}
$bill->update([
'attachment_path' => null,
'updated_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($bill, 'Attachment deleted successfully');
} catch (\Exception $e) {
Log::error('Error deleting electricity bill attachment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
function numberToVietnamese($number)
{
$units = ["", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín"];
$levels = ["", "nghìn", "triệu", "tỷ"];
if ($number == 0) return "không đồng";
$number = (int)$number;
$result = "";
$level = 0;
while ($number > 0) {
$threeDigits = $number % 1000;
if ($threeDigits != 0) {
$result = $this->readThreeDigits($threeDigits, $units) . " " . $levels[$level] . " " . $result;
}
$number = floor($number / 1000);
$level++;
}
return ucfirst(trim(preg_replace('/\s+/', ' ', $result))) . " đồng";
}
function readThreeDigits($number, $units)
{
$hundreds = floor($number / 100);
$tens = floor(($number % 100) / 10);
$ones = $number % 10;
$result = "";
if ($hundreds > 0) {
$result .= $units[$hundreds] . " trăm";
if ($tens == 0 && $ones > 0) {
$result .= " lẻ";
}
}
if ($tens > 1) {
$result .= " " . $units[$tens] . " mươi";
if ($ones == 1) {
$result .= " mốt";
} elseif ($ones == 5) {
$result .= " lăm";
} elseif ($ones > 0) {
$result .= " " . $units[$ones];
}
} elseif ($tens == 1) {
$result .= " mười";
if ($ones == 5) {
$result .= " lăm";
} elseif ($ones > 0) {
$result .= " " . $units[$ones];
}
} elseif ($ones > 0) {
$result .= " " . $units[$ones];
}
return trim($result);
}
}

View File

@ -1,517 +0,0 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Equipment;
use Modules\Admin\app\Models\EquipmentAssignment;
class EquipmentController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
/**
* Get all equipments with pagination + filter + search.
*/
public function index(Request $request)
{
try {
$equipments = new Equipment;
$this->orderByRequest($equipments, $request);
$this->filterRequest(
builder: $equipments,
request: $request,
filterKeys: [
'code' => self::F_TEXT,
'name' => self::F_TEXT,
'note' => self::F_TEXT,
]
);
$this->searchRequest(
builder: $equipments,
value: $request->get('search'),
fields: ['equipments.code', 'equipments.name', 'equipments.note']
);
// Optional filter: assigned (in use) vs warehouse
$assignedFilter = $request->get('assigned');
if ($assignedFilter === 'in_use') {
$equipments->whereNotNull('equipments.current_user_id');
} elseif ($assignedFilter === 'warehouse') {
$equipments->whereNull('equipments.current_user_id');
}
$responseData = $equipments
->leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
->orderBy('equipments.updated_at', 'desc')
->select(
'equipments.*',
'holder.name as current_user_name'
)
->paginate($request->get('per_page', 15));
return AbstractController::ResultSuccess($responseData);
} catch (\Exception $e) {
Log::error('Error fetching equipments: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Get a single equipment by code (for barcode scan).
*/
public function showByCode(Request $request)
{
try {
$code = trim((string) $request->get('code'));
if ($code === '') {
return AbstractController::ResultError('Code is required', 422);
}
$equipment = Equipment::leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
->where('equipments.code', $code)
->select('equipments.*', 'holder.name as current_user_name')
->first();
if (!$equipment) {
return AbstractController::ResultSuccess(null, 'Equipment not found');
}
$history = EquipmentAssignment::leftJoin('users', 'equipment_assignments.user_id', '=', 'users.id')
->where('equipment_assignments.equipment_id', $equipment->id)
->orderBy('equipment_assignments.assigned_at', 'desc')
->select(
'equipment_assignments.*',
'users.name as user_name'
)
->get();
$equipment->history = $history;
return AbstractController::ResultSuccess($equipment);
} catch (\Exception $e) {
Log::error('Error fetching equipment by code: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Get assignment history for an equipment.
*/
public function history($id)
{
try {
$history = EquipmentAssignment::leftJoin('users', 'equipment_assignments.user_id', '=', 'users.id')
->where('equipment_assignments.equipment_id', $id)
->orderBy('equipment_assignments.assigned_at', 'desc')
->select(
'equipment_assignments.*',
'users.name as user_name'
)
->get();
return AbstractController::ResultSuccess($history);
} catch (\Exception $e) {
Log::error('Error fetching equipment history: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Get all users for assignment selectbox.
*/
public function userOptions()
{
try {
$users = Admin::orderBy('name', 'asc')
->select('id', 'name', 'email')
->get();
return AbstractController::ResultSuccess($users);
} catch (\Exception $e) {
Log::error('Error fetching user options: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Create new equipment. Accepts multipart with optional image file.
*/
public function create(Request $request)
{
try {
$validated = $request->validate([
'code' => 'required|string|max:255|unique:equipments,code',
'name' => 'required|string|max:255',
'note' => 'nullable|string',
'current_user_id' => 'nullable|integer|exists:users,id',
'image' => 'nullable|file|mimes:jpg,jpeg,png,gif,webp|max:10240',
]);
$authId = auth('admins')->user()->id ?? null;
$imagePath = null;
if ($request->hasFile('image')) {
$directory = 'equipments/images';
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory);
}
$imagePath = $request->file('image')->store($directory, 'public');
}
$equipment = DB::transaction(function () use ($validated, $authId, $imagePath) {
$now = Carbon::now();
$currentUserId = $validated['current_user_id'] ?? null;
$equipment = Equipment::create([
'code' => $validated['code'],
'name' => $validated['name'],
'note' => $validated['note'] ?? null,
'image_path' => $imagePath,
'current_user_id' => $currentUserId,
'assigned_at' => $now,
'created_by' => $authId,
]);
// Always create an initial assignment record so the history
// shows the very first state (warehouse or user).
EquipmentAssignment::create([
'equipment_id' => $equipment->id,
'user_id' => $currentUserId, // null = at warehouse
'assigned_at' => $now,
'note' => $currentUserId ? 'Initial assignment' : 'Initial — at warehouse',
'created_by' => $authId,
]);
return $equipment;
});
return AbstractController::ResultSuccess($equipment, 'Equipment created successfully');
} catch (\Illuminate\Validation\ValidationException $e) {
return AbstractController::ResultError($e->getMessage(), 422);
} catch (\Exception $e) {
Log::error('Error creating equipment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Upload / replace the equipment image (optional, post-create or edit).
*/
public function uploadImage(Request $request, $id)
{
try {
$request->validate([
'image' => 'required|file|mimes:jpg,jpeg,png,gif,webp|max:10240',
]);
$equipment = Equipment::findOrFail($id);
// Delete old image if present
if ($equipment->image_path && Storage::disk('public')->exists($equipment->image_path)) {
Storage::disk('public')->delete($equipment->image_path);
}
$directory = 'equipments/images';
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory);
}
$path = $request->file('image')->store($directory, 'public');
$equipment->update([
'image_path' => $path,
'updated_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($equipment, 'Image uploaded');
} catch (\Exception $e) {
Log::error('Error uploading equipment image: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Delete the equipment image.
*/
public function deleteImage($id)
{
try {
$equipment = Equipment::findOrFail($id);
if ($equipment->image_path && Storage::disk('public')->exists($equipment->image_path)) {
Storage::disk('public')->delete($equipment->image_path);
}
$equipment->update([
'image_path' => null,
'updated_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($equipment, 'Image deleted');
} catch (\Exception $e) {
Log::error('Error deleting equipment image: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Update equipment basic info (code, name, note).
*/
public function update(Request $request, $id)
{
try {
$equipment = Equipment::findOrFail($id);
$validated = $request->validate([
'code' => 'sometimes|string|max:255|unique:equipments,code,' . $id,
'name' => 'sometimes|string|max:255',
'note' => 'nullable|string',
]);
$equipment->update(array_merge($validated, [
'updated_by' => auth('admins')->user()->id ?? null,
]));
return AbstractController::ResultSuccess($equipment, 'Equipment updated successfully');
} catch (\Exception $e) {
Log::error('Error updating equipment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Assign equipment to a user, or return to warehouse (user_id = null).
* Every state change is recorded: closes the previous open record AND opens
* a new one for the new holder (user or warehouse).
*/
public function assign(Request $request, $id)
{
try {
$validated = $request->validate([
'user_id' => 'nullable|integer|exists:users,id',
'note' => 'nullable|string',
]);
$authId = auth('admins')->user()->id ?? null;
$equipment = DB::transaction(function () use ($id, $validated, $authId) {
$equipment = Equipment::lockForUpdate()->findOrFail($id);
$now = Carbon::now();
$newUserId = $validated['user_id'] ?? null;
// No-op guard: if assigning to the same holder, skip to avoid duplicate history rows
if ($equipment->current_user_id == $newUserId) {
return $equipment;
}
// Close any currently open assignment (records who/Kho was holding it until now)
EquipmentAssignment::where('equipment_id', $equipment->id)
->whereNull('returned_at')
->update([
'returned_at' => $now,
'updated_at' => $now,
]);
// Always open a new assignment row — for a user OR for the warehouse.
// user_id = null means "currently at warehouse".
EquipmentAssignment::create([
'equipment_id' => $equipment->id,
'user_id' => $newUserId,
'assigned_at' => $now,
'note' => $validated['note'] ?? ($newUserId === null ? 'Returned to warehouse' : null),
'created_by' => $authId,
]);
$equipment->update([
'current_user_id' => $newUserId,
'assigned_at' => $now,
'updated_by' => $authId,
]);
return $equipment->fresh();
});
return AbstractController::ResultSuccess($equipment, 'Equipment assignment updated');
} catch (\Exception $e) {
Log::error('Error assigning equipment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Statistics: list users grouped with the equipments they are currently holding.
*/
public function userStatistics(Request $request)
{
try {
$search = trim((string) $request->get('search', ''));
$usersQuery = DB::table('equipments')
->join('users', 'equipments.current_user_id', '=', 'users.id')
->whereNotNull('equipments.current_user_id');
if ($search !== '') {
$usersQuery->where(function ($q) use ($search) {
$q->where('users.name', 'like', "%{$search}%")
->orWhere('users.email', 'like', "%{$search}%")
->orWhere('equipments.code', 'like', "%{$search}%")
->orWhere('equipments.name', 'like', "%{$search}%");
});
}
$userTotals = (clone $usersQuery)
->select(
'users.id as user_id',
'users.name as user_name',
'users.email as user_email',
DB::raw('COUNT(equipments.id) as equipment_count')
)
->groupBy('users.id', 'users.name', 'users.email')
->orderByDesc('equipment_count')
->orderBy('users.name')
->get();
$userIds = $userTotals->pluck('user_id')->all();
$devices = Equipment::leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
->whereIn('equipments.current_user_id', $userIds)
->select(
'equipments.id',
'equipments.code',
'equipments.name',
'equipments.note',
'equipments.image_path',
'equipments.current_user_id',
'equipments.assigned_at'
)
->orderBy('equipments.assigned_at', 'desc')
->get()
->groupBy('current_user_id');
$result = $userTotals->map(function ($u) use ($devices) {
return [
'user_id' => $u->user_id,
'user_name' => $u->user_name,
'user_email' => $u->user_email,
'equipment_count' => (int) $u->equipment_count,
'equipments' => ($devices->get($u->user_id) ?? collect())->values(),
];
});
// Also include warehouse summary
$warehouseCount = Equipment::whereNull('current_user_id')->count();
return AbstractController::ResultSuccess([
'users' => $result,
'warehouse_count' => $warehouseCount,
'total_equipment' => Equipment::count(),
]);
} catch (\Exception $e) {
Log::error('Error fetching equipment user statistics: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Delete equipment (and its history via cascade).
*/
public function delete($id)
{
try {
$equipment = Equipment::findOrFail($id);
$equipment->delete();
return AbstractController::ResultSuccess(null, 'Equipment deleted successfully');
} catch (\Exception $e) {
Log::error('Error deleting equipment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Export the (filtered) equipment list as CSV.
*/
public function export(Request $request)
{
try {
$equipments = new Equipment;
$this->filterRequest(
builder: $equipments,
request: $request,
filterKeys: [
'code' => self::F_TEXT,
'name' => self::F_TEXT,
'note' => self::F_TEXT,
]
);
$this->searchRequest(
builder: $equipments,
value: $request->get('search'),
fields: ['equipments.code', 'equipments.name', 'equipments.note']
);
$assignedFilter = $request->get('assigned');
if ($assignedFilter === 'in_use') {
$equipments->whereNotNull('equipments.current_user_id');
} elseif ($assignedFilter === 'warehouse') {
$equipments->whereNull('equipments.current_user_id');
}
$rows = $equipments
->leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
->orderBy('equipments.updated_at', 'desc')
->select(
'equipments.id',
'equipments.code',
'equipments.name',
'equipments.note',
'equipments.assigned_at',
'equipments.created_at',
'equipments.updated_at',
'holder.name as current_user_name'
)
->get();
$filename = 'equipments_' . date('Y-m-d_His') . '.csv';
$callback = function () use ($rows) {
$out = fopen('php://output', 'w');
// BOM for Excel UTF-8
fwrite($out, "\xEF\xBB\xBF");
fputcsv($out, ['ID', 'Code', 'Name', 'Note', 'Current Holder', 'Assigned At', 'Created At', 'Updated At']);
foreach ($rows as $r) {
fputcsv($out, [
$r->id,
$r->code,
$r->name,
$r->note,
$r->current_user_name ?: 'Kho',
$r->assigned_at,
$r->created_at,
$r->updated_at,
]);
}
fclose($out);
};
return response()->stream($callback, 200, [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
} catch (\Exception $e) {
Log::error('Error exporting equipments: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
}

View File

@ -8,11 +8,9 @@ use App\Services\JiraService;
use Carbon\Carbon;
use Modules\Admin\app\Models\TechnicalUser;
use Illuminate\Http\Request;
use Modules\Admin\app\Models\ProjectReview;
use Modules\Admin\app\Models\UserCriteria;
use PhpOffice\PhpWord\IOFactory;
use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\SimpleType\Jc;
class EvaluationController extends Controller
{
@ -224,50 +222,6 @@ class EvaluationController extends Controller
'spaceAfter' => 600,
]);
}
// **ProjectReview Section**
// Fetch Project Reviews
$projectReviews = ProjectReview::where('user_id', $user->id);
if ($startDate && $endDate) {
$projectReviews->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
} elseif ($startDate) {
$projectReviews->where('updated_at', '>=', $startDate);
} elseif ($endDate) {
$projectReviews->where('updated_at', '<=', $endDate . ' 23:59:59');
}
if ($projectReviews->get()->count() > 0) {
$section->addText("Project Reviews", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
$table = $section->addTable([
'borderColor' => '000000',
'borderSize' => 6,
'cellMargin' => 80,
]);
// Table Header
$table->addRow();
$table->addCell(3500)->addText('Project Name', ['bold' => true]);
$table->addCell(2500)->addText('Role', ['bold' => true]);
$table->addCell(5000)->addText('Note', ['bold' => true]);
$table->addCell(2500)->addText('Created At', ['bold' => true]);
$table->addCell(2500)->addText('Updated At', ['bold' => true]);
foreach ($projectReviews->get() as $review) {
$table->addRow();
$table->addCell(3500)->addText($review->name);
$table->addCell(2500)->addText($review->role);
$table->addCell(5000)->addText($review->note);
$table->addCell(2500)->addText(Carbon::parse($review->created_at)->format('d/m/Y H:i:s'));
$table->addCell(2500)->addText(Carbon::parse($review->updated_at)->format('d/m/Y H:i:s'));
}
$section->addText(' ', [], [
'spaceAfter' => 600,
]);
}
if ($technicalData)
$section->addPageBreak();
//Technical
@ -306,156 +260,4 @@ class EvaluationController extends Controller
return response()->download($tempFile, "$user->name.docx")->deleteFileAfterSend(true);
}
public function reportAllUsers(Request $request)
{
$request->validate([
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$users = User::all();
$phpWord = new PhpWord();
$phpWord->setDefaultFontName('Times New Roman');
$phpWord->setDefaultFontSize(12);
$section = $phpWord->addSection();
foreach ($users as $index => $user) {
$userEmail = $user->email;
// Add user heading
$section->addText("Staff Evaluation", ['bold' => true, 'size' => 20, 'color' => '000000'], ['align' => 'center']);
if ($startDate) {
$fromDate = Carbon::parse($startDate)->format('d-m-Y');
$section->addText("From: " . $fromDate, ['size' => 12], ['align' => 'end']);
}
if ($endDate) {
$toDate = Carbon::parse($endDate)->format('d-m-Y');
$section->addText("To: " . $toDate, ['size' => 12], ['align' => 'end']);
}
$section->addText("{$user->name}", ['bold' => true, 'size' => 14, 'color' => '000000'], ['spaceAfter' => 400]);
// **Projects Data**
$projectsData = self::getProjectReviewByParams($startDate, $endDate, $userEmail);
if (!empty($projectsData)) {
foreach ($projectsData as $project) {
$section->addText("Project: {$project['name']}", ['bold' => true, 'size' => 14, 'color' => '000080']);
foreach ($project['sprints'] as $sprint) {
$section->addText("Sprint: {$sprint['name']}", ['bold' => true, 'italic' => true, 'size' => 12]);
$table = $section->addTable(['borderSize' => 6, 'cellMargin' => 80]);
$table->addRow();
$table->addCell(3000)->addText('Criteria', ['bold' => true]);
$table->addCell(3000)->addText('Note', ['bold' => true]);
$table->addCell(2500)->addText('Created By', ['bold' => true]);
$table->addCell(1500)->addText('Point', ['bold' => true]);
foreach ($sprint['criterias'] as $criteria) {
$table->addRow();
$table->addCell(3000)->addText($criteria['criteria']);
$table->addCell(3000)->addText($criteria['note']);
$table->addCell(2500)->addText($criteria['createdBy']);
$table->addCell(1500)->addText($criteria['point'] > 0 ? $criteria['point'] : '');
}
}
$section->addText(' ', [], [
'spaceAfter' => 600,
]);
}
}
// **ProjectReview Section**
// Fetch Project Reviews
$projectReviews = ProjectReview::where('user_id', $user->id);
if ($startDate && $endDate) {
$projectReviews->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
} elseif ($startDate) {
$projectReviews->where('updated_at', '>=', $startDate);
} elseif ($endDate) {
$projectReviews->where('updated_at', '<=', $endDate . ' 23:59:59');
}
if ($projectReviews->get()->count() > 0) {
$section->addText("Project Reviews", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
$table = $section->addTable([
'borderColor' => '000000',
'borderSize' => 6,
'cellMargin' => 80,
]);
// Table Header
$table->addRow();
$table->addCell(3500)->addText('Project Name', ['bold' => true]);
$table->addCell(2500)->addText('Role', ['bold' => true]);
$table->addCell(5000)->addText('Note', ['bold' => true]);
$table->addCell(2500)->addText('Created At', ['bold' => true]);
$table->addCell(2500)->addText('Updated At', ['bold' => true]);
foreach ($projectReviews->get() as $review) {
$table->addRow();
$table->addCell(3500)->addText($review->name);
$table->addCell(2500)->addText($review->role);
$table->addCell(5000)->addText($review->note);
$table->addCell(2500)->addText(Carbon::parse($review->created_at)->format('d/m/Y H:i:s'));
$table->addCell(2500)->addText(Carbon::parse($review->updated_at)->format('d/m/Y H:i:s'));
}
$section->addText(' ', [], [
'spaceAfter' => 600,
]);
}
// **Technical Section**
$section->addText("Technicals", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
$table = $section->addTable([
'borderColor' => '000000',
'borderSize' => 6,
'cellMargin' => 80,
]);
$table->addRow();
$table->addCell(1500)->addText('Level', ['bold' => true]);
$table->addCell(3500)->addText('Name', ['bold' => true]);
$table->addCell(2500)->addText('Point', ['bold' => true]);
$table->addCell(2500)->addText('Last Update', ['bold' => true]);
// Fetch Technical Data
$technicalData = TechnicalController::getDataTechnicalsByUserId($user->id);
foreach ($technicalData as $technical) {
$updated_at = $technical['updated_at'] ? Carbon::parse($technical['updated_at'])->format('d/m/Y H:i:s') : null;
$table->addRow();
$table->addCell(1500)->addText($technical['level']);
$table->addCell(3500)->addText($technical['name']);
$table->addCell(2500)->addText($technical['point']);
$table->addCell(2500)->addText($updated_at);
}
// Add page break between users (except last one)
if ($index < count($users) - 1) {
$section->addPageBreak();
}
}
// Save & Download Word File
$tempFile = tempnam(sys_get_temp_dir(), 'word');
$objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
$objWriter->save($tempFile);
return response()->download($tempFile, "All_Users_Report.docx")->deleteFileAfterSend(true);
}
}

View File

@ -3,9 +3,7 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Mail\WarningLongTask;
use App\Mail\WorklogReport;
use App\Models\User;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
@ -178,27 +176,6 @@ class JiraController extends Controller
}
}
public function getWeeklyReport()
{
try {
$startOfWeek = Carbon::now()->startOfWeek()->format('Y-m-d'); // Mặc định là Thứ Hai
$endOfWeek = Carbon::now()->endOfWeek()->format('Y-m-d'); // Mặc định là Chủ Nhật
// dd($startOfWeek);
$results = [];
$workLogs = $this->jiraService->getAllUserWorkLogs($startOfWeek, $endOfWeek);
foreach($workLogs as $data){
$results[$data['username']] = $data['information']['issues'];
}
return response()->json([
$results
], 200);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
public function sendReport()
{
$dateFormatted = Carbon::yesterday()->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
@ -206,8 +183,8 @@ class JiraController extends Controller
$workLogs = $this->jiraService->getAllUserWorkLogs($dateFormatted, $dateFormatted);
$tasksByUser = $this->formatWorkLogsByUser($workLogs);
// Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
Mail::to(['joseph@apactech.io', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
// Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
// return "Email sent successfully!";
return response()->json([
@ -271,6 +248,7 @@ class JiraController extends Controller
return $tasksByUser;
}
public function getAllUserDoing(Request $request)
{
try {
@ -325,7 +303,6 @@ class JiraController extends Controller
'status' => true
], 200);
}
public function getAllIssueByIdSprint(Request $request)
{
$id = $request->input('id');
@ -336,42 +313,4 @@ class JiraController extends Controller
'status' => true
], 200);
}
public function sendWarningMailByAllowcation()
{
$data = $this->jiraService->getAllUserDoing();
$user_info = [];
foreach ($data['projects'] as $project) {
foreach ($project['users'] as $user) {
foreach ($user['issues'] as $issue) {
$targetDate = Carbon::parse($issue['changelog']['histories'][0]['created']); // Target date
$daysRemaining = Carbon::now()->setTimezone(env('TIME_ZONE'))->diffInDays($targetDate);
if ($daysRemaining > 10) {
$issue['daysRemaining'] = $daysRemaining;
$user_info[$user['user']['emailAddress']][] = $issue;
}
}
}
}
foreach ($user_info as $email => $user) {
Mail::to([$email])->cc(['admin@apactech.io', 'joseph@apactech.io'])->send(new WarningLongTask($user_info[$email]));
}
}
public function getUserProjectParticipating(Request $request)
{
$userID = $request->input('userID');
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$user = User::find($userID);
$userJira = $this->jiraService->getUserByEmail($user->email);
$projects = $this->jiraService->getUserWorkLogs($userJira[0]['accountId'], $startDate, $endDate);
return response()->json([
'data' => $projects,
"accountId" => $userJira[0]['accountId'],
'status' => true
], 200);
}
}

View File

@ -2,23 +2,16 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Exports\LeaveManagementExport;
use App\Http\Controllers\Controller;
use App\Jobs\InitializeLeaveDays;
use App\Models\LeaveDays;
use App\Models\Notes;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Admin\app\Models\Ticket;
use App\Traits\AnalyzeData;
class LeaveManagementController extends Controller
{
use AnalyzeData;
public function get(Request $request)
{
$yearNow = $request->query('year', now()->year);
@ -40,34 +33,31 @@ class LeaveManagementController extends Controller
})
->leftJoin("categories as reason", function ($join) {
$join->on('n_reason', '=', 'reason.c_code');
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
})
->select(
DB::raw('notes.id as id'),
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_time_type as time_type'),
DB::raw('notes.n_year as year'),
DB::raw('notes.n_month as month'),
DB::raw('categories.c_value as leave_days'),
DB::raw('notes.n_day as day'),
DB::raw('notes.n_reason as reason_code'),
'reason.c_name as reason_name',
'categories.c_name as time_type_name',
// DB::raw('SUM(categories.c_value) as leave_days')
)
// ->where('notes.n_user_id', "1")
->where('notes.n_year', $year)
->whereIn('notes.n_reason', ['ONLEAVE', 'LEAVE_WITHOUT_PAY', 'TEMPORARY_ONLEAVE'])
->where('notes.n_reason', 'ONLEAVE')
// ->groupBy("notes.n_user_id")
->orderBy('notes.n_month')
->orderBy('notes.n_day')
->get()
->map(function ($item) {
return [
"id" => $item->id,
"day" => $item->day,
"n_user_id" => $item->n_user_id,
"reason_code" => $item->reason_code,
// "time_type" => $item->time_type,
"reason_name" => $item->reason_name,
"time_type_name" => $item->time_type_name,
"month" => $item->month,
@ -83,7 +73,6 @@ class LeaveManagementController extends Controller
'users.name as user_name',
'users.email',
'users.avatar',
'users.is_separated',
'users.created_at as user_created_at',
'users.permission',
'users.updated_at as user_updated_at',
@ -105,7 +94,6 @@ class LeaveManagementController extends Controller
'name' => $item->user_name,
'email' => $item->email,
'avatar' => $item->avatar,
'is_separated' => $item->is_separated,
'created_at' => $item->user_created_at,
'permission' => $item->permission,
'updated_at' => $item->user_updated_at,
@ -115,10 +103,9 @@ class LeaveManagementController extends Controller
'leaveDay' => [
'id' => $item->id,
'ld_user_id' => $item->ld_user_id,
'ld_day_total' => $item->ld_day_total,
'ld_day' => $item->ld_day,
'ld_year' => $item->ld_year,
'ld_additional_day' => $item->ld_additional_day,
'ld_special_leave_day' => $item->ld_special_leave_day,
'ld_date_additional' => $item->ld_date_additional,
'ld_note' => $item->ld_note,
'created_at' => $item->created_at,
'updated_at' => $item->updated_at,
@ -144,66 +131,12 @@ class LeaveManagementController extends Controller
$validatedData = $request->all();
$leaveDays = LeaveDays::find($validatedData['id']);
$leaveDays->ld_day_total = $validatedData['totalLeave'];
$leaveDays->ld_additional_day = $validatedData['dayAdditional'];
$leaveDays->ld_special_leave_day = $validatedData['specialLeave'];
$leaveDays->ld_day = $validatedData['totalLeave'];
$leaveDays->ld_date_additional = $validatedData['dayAdditional']; // Assuming you have this field to store additional days
$leaveDays->ld_note = $validatedData['note'];
$leaveDays->save();
return response()->json(['status' => true, 'message' => 'Updated successfully']);
}
public function updateNoteStatus(Request $request)
{
$rules = [
'id' => 'required',
'n_reason' => 'required|in:ONLEAVE,LEAVE_WITHOUT_PAY'
];
// Validate the request
$request->validate($rules);
$id = $request->input('id');
$reason = $request->input('n_reason');
$note = Notes::find($id);
if (!$note) {
return response()->json(['message' => 'Note not found', 'status' => false]);
}
$note->n_reason = $reason;
$note->save();
// Clear Timekeeping cache
$ticket = Ticket::find($note->ticket_id);
if ($ticket) {
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->start_date)->month, Carbon::parse($ticket->start_date)->year);
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->end_date)->month, Carbon::parse($ticket->end_date)->year);
} else {
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($note->created_at)->month, Carbon::parse($note->created_at)->year);
}
return response()->json(data: ['message' => 'Update success', 'status' => true]);
}
public function export(Request $request)
{
$year = $request->query('year', now()->year);
$leaveDays = $this->getDataByYear($year);
if ($leaveDays->isEmpty()) {
return response()->json(['status' => false, 'message' => 'No data found']);
}
// Lọc chỉ lấy user có permission bao gồm staff
$staffData = $leaveDays->filter(function ($user) {
return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
});
$currentDate = date('d_His');
return Excel::download(
new LeaveManagementExport($staffData),
"LeaveManagement_{$year}_{$currentDate}.xlsx"
);
}
}

View File

@ -10,14 +10,9 @@ use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Sprint;
use Modules\Admin\app\Models\UserCriteria;
use App\Models\Files;
use App\DataTransferObjects\FileData;
class ProfileController extends Controller
{
@ -33,7 +28,6 @@ class ProfileController extends Controller
$this->jiraService = $jiraService;
}
public function getProfilesData(Request $request)
{
$user = auth('admins')->user();
@ -121,353 +115,4 @@ class ProfileController extends Controller
$user->save();
return AbstractController::ResultSuccess($path);
}
public function listFiles(Request $request)
{
// Get the root folder from the input URL
$rootFolder = $request->input('root_folder');
// Ensure the root folder is correctly formatted
$rootFolder = rtrim($rootFolder, '/') . '/';
// Get all files and directories in the specified root folder
$fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder : 'image' . $rootFolder);
return response()->json(['data' => $fileList, 'status' => true]);
}
private function getDirectoryTree($dir, $urlRoot)
{
$results = [];
// Scan the directory for files and folders
$files = scandir($dir);
foreach ($files as $file) {
if ($file !== '.' && $file !== '..') {
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
$fileUrl = url($urlRoot . $file);
if (is_dir($filePath)) {
// If it's a directory, recurse into it
$results[] = [
'label' => $file,
'type' => 'directory',
'value' => $fileUrl,
'children' => $this->getDirectoryTree($filePath, $urlRoot . $file . '/')
];
} else {
// If it's a file, add it to the list
$results[] = [
'label' => $file,
'type' => 'file',
'value' => $fileUrl
];
}
}
}
return $results;
}
public function updateProfile(Request $request)
{
$name = $request->input('name') ?? auth('admins')->user()->name;
// Validate the incoming files
$request->validate([
'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx,xlsx,xls,csv|max:5120', // Adjust file types and size limit as needed
]);
$uploadedFiles = [];
$baseDirectory = 'profiles/' . $name;
$othersDirectory = $baseDirectory . '/others';
// Check if the base directory exists, if not create it
if (!Storage::disk('public')->exists($baseDirectory)) {
Storage::disk('public')->makeDirectory($baseDirectory);
}
// Check if the "others" directory exists, if not create it
if (!Storage::disk('public')->exists($othersDirectory)) {
Storage::disk('public')->makeDirectory($othersDirectory);
}
$adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
$currentUser = auth('admins')->user();
if ($request->hasFile('files')) {
foreach ($request->file('files') as $file) {
// Store the file and get its path
$originalFilename = $file->getClientOriginalName();
if (strpos($originalFilename, '__') === 0) {
// Store the file in the "others" directory
$path = $file->storeAs($othersDirectory, $originalFilename, 'public');
} else {
// Store the file in the base directory
$path = $file->storeAs($baseDirectory, $originalFilename, 'public');
}
$uploadedFiles[] = $path;
// Tạo URL đầy đủ cho file
$fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
? env('APP_URL') . '/image/' . str_replace('/storage/', '', Storage::url($path))
: env('APP_URL') . str_replace('/storage/', '', Storage::url($path));
// // Gửi email thông báo cho admin
// foreach ($adminEmails as $adminEmail) {
// $admin = Admin::where('email', $adminEmail)->first();
// if ($admin) {
// $this->sendFileUploadNotification(
// $admin,
// "File {$originalFilename} đã được tải lên bởi {$currentUser->name}",
// $fileUrl,
// "[APAC Tech] {$currentUser->name} - Đã tải lên file mới"
// );
// }
// }
// // Gửi email xác nhận cho người tải lên
// $this->sendFileUploadNotification(
// $currentUser,
// "Bạn đã tải lên file {$originalFilename} thành công",
// $fileUrl,
// "[APAC Tech] {$currentUser->name} - Tải file thành công"
// );
}
}
return response()->json([
'status' => true,
'message' => 'Files uploaded successfully',
'files' => $uploadedFiles,
]);
}
public function removeFile(Request $request)
{
// Validate that the file URL is provided in the request
$request->validate([
'file_url' => 'required|string',
]);
// Get the full file URL from the request
$fileUrl = $request->input('file_url');
// Parse the file path from the URL (remove the base URL part)
$storagePath = parse_url($fileUrl, PHP_URL_PATH); // Extract the path part of the URL
$filePath = str_replace(env('APP_ENV') === 'local' ? '/storage/' : '/image/storage/', '', $storagePath); // Remove "/storage/" to get the actual file path
// Check if the file exists before attempting to delete it
if (Storage::disk('public')->exists($filePath)) {
// Delete the file
Storage::disk('public')->delete($filePath);
return response()->json([
'status' => true,
'message' => 'File deleted successfully',
]);
}
return response()->json([
'status' => false,
'message' => 'File not found',
], 404);
}
public function sendFileUploadNotification($user, $description, $url, $subject, $note)
{
try {
// Gửi email bất đồng bộ không cần job
dispatch(function() use ($user, $description, $url, $subject, $note) {
Mail::send('emails.file_upload_notification', [
'user' => $user,
'description' => $description,
'url' => $url,
'note' => $note
], function ($message) use ($user, $subject) {
$message->to($user->email)
->subject($subject);
});
})->afterResponse();
return true;
} catch (\Exception $e) {
Log::error('Error dispatching file upload notification email: ' . $e->getMessage());
return false;
}
}
public function uploadFiles(Request $request)
{
try {
$request->validate([
'file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx,csv|max:5120',
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'user_name' => 'required|string|max:255'
]);
$file = $request->file('file');
$user = auth('admins')->user();
// Tạo thư mục cho user nếu chưa tồn tại
$userFolder = 'files/' . $request->user_name;
if (!Storage::disk('public')->exists($userFolder)) {
Storage::disk('public')->makeDirectory($userFolder);
}
$path = $file->store($userFolder, 'public');
$fileRecord = Files::create([
'name' => $request->name,
'url' => $path,
'type' => $this->getFileType($file->getClientOriginalName()),
'description' => $request->description,
'user_id' => Admin::where('name', $request->user_name)->first()->id
]);
$currentUser = Admin::where('name', $request->user_name)->first();
// Gửi email thông báo cho người upload
$fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
? env('APP_URL') . '/image' . Storage::url($path)
: env('APP_URL') . Storage::url($path);
$this->sendFileUploadNotification(
$user,
'Bạn đã tải lên file "' . $request->name . '" thành công',
$fileUrl,
"[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
$request->description ?? 'No description'
);
// Gửi email thông báo cho tất cả admin khác
$otherAdmins = Admin::where('permission', 'like', '%admin%')->get();
foreach ($otherAdmins as $admin) {
$this->sendFileUploadNotification(
$admin,
'File "' . $request->name . '" đã được tải lên bởi ' . $user->name,
$fileUrl,
"[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
$request->description ?? 'No description'
);
}
return response()->json([
'status' => true,
'message' => 'File uploaded successfully',
'data' => [
'id' => $fileRecord->id,
'name' => $fileRecord->name,
'url' => Storage::url($path),
'type' => $fileRecord->type,
'description' => $fileRecord->description
]
]);
} catch (\Exception $e) {
return response()->json([
'status' => false,
'message' => $e->getMessage()
], 500);
}
}
public function getFiles()
{
try {
// Lấy tất cả users
$users = Admin::all();
// Lấy files và map theo cấu trúc
$files = Files::with('user')->get()
->map(function($file) {
return [
'id' => $file->id,
'name' => $file->name,
'url' => Storage::url($file->url),
'type' => $file->type,
'description' => $file->description,
'created_at' => $file->created_at,
'user_id' => $file->user_id,
'user_name' => $file->user->name
];
});
// Tạo mảng kết quả với tất cả users, không có file thì mảng rỗng
$result = $users->pluck('name')->mapWithKeys(function($userName) use ($files) {
$userFiles = $files->where('user_name', $userName)
->map(function($file) {
return (object)[
'id' => $file['id'],
'name' => $file['name'],
'url' => $file['url'],
'type' => $file['type'],
'description' => $file['description'],
'created_at' => $file['created_at'],
'user_id' => $file['user_id']
];
})->values();
return [$userName => $userFiles];
});
return response()->json([
'status' => true,
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'status' => false,
'message' => $e->getMessage()
], 500);
}
}
public function deleteFile($id)
{
try {
$file = Files::findOrFail($id);
$user = auth('admins')->user();
if ($file->user_id !== $user->id) {
return response()->json([
'status' => false,
'message' => 'Unauthorized'
], 403);
}
Storage::disk('public')->delete($file->url);
$file->delete();
return response()->json([
'status' => true,
'message' => 'File deleted successfully'
]);
} catch (\Exception $e) {
return response()->json([
'status' => false,
'message' => $e->getMessage()
], 500);
}
}
private function getFileType($filename)
{
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$typeMap = [
'pdf' => 'document',
'doc' => 'document',
'docx' => 'document',
'jpg' => 'image',
'jpeg' => 'image',
'png' => 'image',
'xls' => 'spreadsheet',
'xlsx' => 'spreadsheet',
'csv' => 'spreadsheet'
];
return $typeMap[$extension] ?? 'other';
}
}

View File

@ -1,91 +0,0 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Modules\Admin\app\Http\Controllers\AbstractController;
use Modules\Admin\app\Models\ProjectReview;
use Carbon\Carbon;
use Illuminate\Http\Request;
use DateTime;
class ProjectReviewController extends Controller
{
/**
* Display a listing of the resource.
*/
public function getListReviews(Request $request)
{
$request->validate([
'userID' => 'required|exists:users,id',
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
$userID = $request->input('userID');
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$projectsData = ProjectReview::where('user_id', $userID);
if ($startDate && $endDate) {
$projectsData->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
} elseif ($startDate) {
$projectsData->where('updated_at', '>=', $startDate);
} elseif ($endDate) {
$projectsData->where('updated_at', '<=', $endDate . ' 23:59:59');
}
return AbstractController::ResultSuccess($projectsData->get());
}
/**
* Store a newly created resource in storage.
*/
public function create(Request $request)
{
$request->validate([
'name' => 'required|string',
'role' => 'required|string',
'note' => 'required|string',
'user_id' => 'required|exists:users,id',
]);
$review = ProjectReview::create($request->all());
return response()->json($review, 201);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request)
{
$request->validate([
'name' => 'sometimes|required|string',
'role' => 'sometimes|required|string',
'note' => 'sometimes|required|string',
'user_id' => 'sometimes|required|exists:users,id',
]);
$id = $request->get('id');
$projectReview = ProjectReview::find($id);
$payload = $request->all();
// if ($request->has('created_at')) {
// $created_at = Carbon::create($request->get('created_at'))->setTimezone(env('TIME_ZONE'));
// $payload['created_at'] = $created_at;
// }
if ($projectReview) {
$projectReview->update($payload);
}
return response()->json([
'data' => $projectReview,
'status' => true
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Request $request)
{
$id = $request->get('id');
ProjectReview::destroy($id);
return response()->json(['message' => 'Deleted successfully', 'status' => true]);
}
}

View File

@ -1,8 +0,0 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Traits\AnalyzeData;
class DataAnalyzer
{
use AnalyzeData;
}

File diff suppressed because it is too large Load Diff

View File

@ -11,14 +11,10 @@ use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\TicketMail;
use Illuminate\Support\Facades\DB;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\MonthlyTimekeeping;
use Modules\Admin\app\Models\Tracking;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\TimekeepingExport;
use Modules\Admin\app\Models\Ticket;
class TimekeepingController extends Controller
{
@ -78,14 +74,14 @@ class TimekeepingController extends Controller
'user_id' => $user->id,
'status' => 'check in',
'time_string' => $start->format('Y-m-d H:i:s'),
'created_at' => $start->setTimezone(env('TIME_ZONE'))
'created_at' => $start->setTimezone('UTC')
],
[
'name' => $user->name,
'user_id' => $user->id,
'status' => 'check out',
'time_string' => $end->format('Y-m-d H:i:s'),
'created_at' => $end->setTimezone(env('TIME_ZONE'))
'created_at' => $end->setTimezone('UTC')
]
]);
}
@ -154,6 +150,8 @@ class TimekeepingController extends Controller
return response()->json(['status' => true, 'message' => 'Add successfully']);
}
public function updateCacheMonth(Request $request)
{
$month = $request->month;
@ -172,97 +170,19 @@ class TimekeepingController extends Controller
// Validate the request
$request->validate($rules);
$id = $request->input('id');
$month = $request->month;
$year = $request->year;
$note = Notes::find($id);
if (!$note) {
return response()->json(['message' => 'Note not found', 'status' => false]);
}
if ($note->ticket_id != null) {
$ticket = Ticket::find($note->ticket_id);
if (!$ticket) {
return response()->json(['message' => 'Ticket not found, can not delete note', 'status' => false]);
}
$admin = auth('admins')->user();
// Handle send mail
$dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->start_period);
$dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->end_period);
$dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $ticket->type);
$formattedStartDate = Carbon::createFromFormat('Y-m-d', $ticket->start_date)->format('d/m/Y');
$formattedEndDate = Carbon::createFromFormat('Y-m-d', $ticket->end_date)->format('d/m/Y');
$user = Admin::find($ticket->user_id);
$data = array(
"email_template" => "email.notification_tickets_user",
"user_name" => $user->name,
"email" => $user->email,
"name" => $admin->name, //name admin duyệt
"date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
"type" => $dataMasterType->c_name,
"note" => $ticket->reason,
"admin_note" => $ticket->admin_note,
"link" => "/tickets", //link đến page admin
"status" => "refused",
"subject" => "[Ticket response] Ticket From " . $admin->name
);
Mail::to($user->email)->send(new TicketMail($data));
// Update
$ticket->updated_by = $admin->name;
$ticket->status = "REFUSED";
$ticket->save();
Notes::where('ticket_id', $ticket->id)->delete();
// Clear Timekeeping cache
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->start_date)->month, Carbon::parse($ticket->start_date)->year);
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->end_date)->month, Carbon::parse($ticket->end_date)->year);
return response()->json(['message' => 'Delete success', 'status' => true]);
}
if ($note) {
$note->delete();
$this->createOrUpdateRecordForCurrentMonth($month, $year);
return response()->json(['message' => 'Delete success', 'status' => true]);
}
public function export(Request $request)
{
// Validate request
$request->validate([
'month' => 'required|numeric|between:1,12',
'year' => 'required|numeric|min:2000',
'working_days' => 'required|numeric|between:1,31'
]);
// Reuse get() function to fetch data
$response = $this->get($request);
$responseData = json_decode($response->getContent(), true);
if (!$responseData['status']) {
return response()->json(['status' => false, 'message' => 'No data found']);
}
// Lọc chỉ lấy user có permission bao gồm staff
$staffData = array_filter($responseData['data'], function ($user) {
return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
});
$currentDate = date('d_His');
return Excel::download(
new TimekeepingExport(
array_values($staffData), // Convert to indexed array after filtering
$request->month,
$request->year,
$request->working_days
),
"Timekeeping_{$request->month}_{$request->year}_{$currentDate}.xlsx"
);
return response()->json(['message' => 'Delete fail', 'status' => false]);
}
}

View File

@ -2,9 +2,8 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Events\WarningChekinLate;
use App\Helper\Cache\CurrentMonthTimekeeping;
use App\Http\Controllers\Controller;
use App\Mail\CheckinLateMail;
use App\Traits\AnalyzeData;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
@ -12,12 +11,10 @@ use App\Traits\HasSearchRequest;
use Carbon\Carbon;
use DateTime;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\MonthlyTimekeeping;
use Modules\Admin\app\Models\Tracking;
use Illuminate\Support\Facades\DB;
class TrackingController extends Controller
{
@ -166,9 +163,14 @@ class TrackingController extends Controller
$id = $request->get('id');
$tracking = Tracking::find($id);
// $id = $request->get('id');
$user = Admin::find($tracking->user_id);
$this->sendCheckinLateMail($user, $tracking->created_at, $tracking->status);
// $tracking = Tracking::find($id);
// $payload = $request->all();
// if ($tracking) {
// $tracking->update($payload);
// }
if ($request->hasFile('image')) {
$file = $request->file('image');
$filename = $request->file_name;
@ -201,163 +203,4 @@ class TrackingController extends Controller
'status' => true
]);
}
// public function sendCheckinLateMail()
private function sendCheckinLateMail($user, $time, $status)
{
// $status = $this->CHECK_IN;
// $lastCheck =Tracking::find(1)->created_at;
// $user = Admin::where('name', 'LE TAN LUAN')->first();
if ($status === $this->CHECK_IN) {
$morning_time = Carbon::createFromTimeString('07:30AM')->setTimezone(env('TIME_ZONE'));
$morning_condition_time = Carbon::createFromTimeString('07:40AM')->setTimezone(env('TIME_ZONE'));
$afternoon_time = Carbon::createFromTimeString('11:30AM')->setTimezone(env('TIME_ZONE'));
$afternoon_condition_time = Carbon::createFromTimeString('01:10PM')->setTimezone(env('TIME_ZONE'));
$admin_mails = Admin::where('permission', 'like', '%admin%')->pluck('email');
if ($time->greaterThan($morning_condition_time) && $time->lessThan($afternoon_time)) {
$period = 'morning';
$minutes_late = $morning_time->diffInMinutes($time);
$data = array(
"email_template" => "email.checkin_late_notification",
"email" => $user->email,
"name" => $user->name,
"admin_mails" => $admin_mails,
"message1" => "Your " . $period . " starts " . $minutes_late . " minutes late",
"message2" => "You checked in at [" . $time . "]",
"url" => env('ADMIN_URL') . "/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=" . $user->name . "&time_string=" . $time->format("Y-m-d H:i") . "&status=check+in",
"subject" => "[Management System] Late warning - " . $user->name
);
Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
}
// if ($time->greaterThan($afternoon_condition_time))
// {
// $period = 'afternoon';
// $minutes_late = $afternoon_time->diffInMinutes($time);
// $data = array(
// "email_template" => "email.checkin_late_notification",
// "email" => $user->email,
// "name" => $user->name,
// "message1" => "Your ". $period ." starts <b>". $minutes_late ."</b> minutes late",
// "message2" => "You checked in at <b>[" . $time ."]</b>",
// "url" => env('ADMIN_URL')."/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=".$user->name."&time_string=".$time->format("Y-m-d H:i")."&status=check+in",
// "subject" => "[Management System] Late warning"
// );
// Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
// }
}
}
public function getSummaryTracking(Request $request)
{
$request->validate([
'userID' => 'required|exists:users,id',
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
$userID = $request->input('userID');
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$trackingData = Tracking::where('user_id', $userID);
if ($startDate && $endDate) {
$trackingData->whereBetween(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
[$startDate, $endDate . ' 23:59:59']
);
} elseif ($startDate) {
$trackingData->where(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
'>=',
$startDate
);
} elseif ($endDate) {
$trackingData->where(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
'<=',
$endDate . ' 23:59:59'
);
}
$onTimeMorning = 0;
$lateMorning = 0;
$onTimeAfternoon = 0;
$lateAfternoon = 0;
$returnTracking = $trackingData->get();
$listLate = [];
$returnTracking->groupBy(function ($record) {
return Carbon::parse($record->time_string)->toDateString();
})->each(function ($records, $date) use ($userID, &$listLate, &$onTimeMorning, &$lateMorning, &$onTimeAfternoon, &$lateAfternoon, &$datesChecked) {
$morningCheck = $records->filter(function ($record) {
return Carbon::parse($record->time_string)->hour < 12;
})->sortBy('time_string')->first();
$afternoonCheck = $records->filter(function ($record) {
$time = Carbon::parse($record->time_string)->hour;
return $time >= 12 && $time <= 14;
})->sortBy('time_string')->first();
$morningTime = Carbon::parse($date)->setTime(7, 40, 0);
$afternoonTime = Carbon::parse($date)->setTime(13, 10, 0);
$checkOutAfternoonTime = Carbon::parse($date)->setTime(15, 00, 0);
if ($morningCheck) {
$checkInTime = Carbon::parse($morningCheck->time_string);
if ($checkInTime->lessThanOrEqualTo($morningTime)) {
$onTimeMorning++;
} else {
array_push($listLate, $morningCheck->id);
$lateMorning++;
}
}
if ($afternoonCheck) {
$checkInTime = Carbon::parse($afternoonCheck->time_string);
if ($checkInTime->lessThanOrEqualTo($afternoonTime)) {
$onTimeAfternoon++;
} else {
array_push($listLate, $afternoonCheck->id);
$lateAfternoon++;
}
} else {
// print ($date . "\n");
$checkOutAfternoon = Tracking::where('user_id', $userID)->whereBetween(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
[$date, $date . ' 23:59:59'],
)->where("status", "check out")->first();
// check if not check-in afternoon but has check out
if ($checkOutAfternoon) {
$timeCheck = Carbon::parse($checkOutAfternoon->time_string);
// print ($timeCheck . "\n");
if ($checkOutAfternoon && $timeCheck->greaterThanOrEqualTo($checkOutAfternoonTime)) {
$onTimeAfternoon++;
}
}
}
});
// ** Add `isLate` flag to each record **
foreach ($returnTracking as $record) {
if (in_array($record->id, $listLate)) {
$record->isLate = true;
}
}
return AbstractController::ResultSuccess([
'on_time_morning' => $onTimeMorning,
'late_morning' => $lateMorning,
'on_time_afternoon' => $onTimeAfternoon,
'late_afternoon' => $lateAfternoon,
'value' => $returnTracking
]);
}
}

View File

@ -8,12 +8,4 @@ use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $fillable = [
'c_type',
'c_name',
'c_code',
'c_value',
'c_active',
];
}

View File

@ -1,13 +0,0 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Document extends Model
{
use HasFactory;
protected $fillable = ['title', 'uri', 'type', 'is_active'];
}

View File

@ -1,52 +0,0 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasCacheModel;
class ElectricityBill extends Model
{
use HasFactory;
use HasCacheModel;
public function __construct()
{
$this->table = 'electricity_bills';
$this->guarded = [];
}
/**
* Calculate total amount based on reading difference and unit price
*/
public function calculateTotal(): float
{
$consumption = $this->current_reading - $this->previous_reading;
return round($consumption * $this->unit_price, 2);
}
/**
* Get consumption in kWh
*/
public function getConsumption(): float
{
return $this->current_reading - $this->previous_reading;
}
/**
* Get user who created this record
*/
public function creator()
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
/**
* Get user who updated this record
*/
public function updater()
{
return $this->belongsTo(\App\Models\User::class, 'updated_by');
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Equipment extends Model
{
use HasFactory;
public function __construct()
{
$this->table = 'equipments';
$this->guarded = [];
}
public function currentUser()
{
return $this->belongsTo(\App\Models\User::class, 'current_user_id');
}
public function assignments()
{
return $this->hasMany(EquipmentAssignment::class, 'equipment_id')->orderBy('assigned_at', 'desc');
}
public function creator()
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(\App\Models\User::class, 'updated_by');
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class EquipmentAssignment extends Model
{
use HasFactory;
public function __construct()
{
$this->table = 'equipment_assignments';
$this->guarded = [];
}
public function equipment()
{
return $this->belongsTo(Equipment::class, 'equipment_id');
}
public function user()
{
return $this->belongsTo(\App\Models\User::class, 'user_id');
}
public function creator()
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectReview extends Model
{
use HasFactory;
protected $fillable = ['name', 'role', 'note', 'user_id'];
// Relationship: A review belongs to a user
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -1,98 +0,0 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Bảng thanh toán tiền điện</title>
<style>
body {
font-family: DejaVu Sans, sans-serif;
font-size: 14px;
}
.text-center {
text-align: center;
}
.mt-10 {
margin-top: 10px;
}
.mt-20 {
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
table, th, td {
border: 1px solid #000;
}
th, td {
padding: 8px;
text-align: center;
}
.no-border {
border: none;
}
.signature {
width: 100%;
margin-top: 50px;
}
.signature td {
border: none;
text-align: center;
}
</style>
</head>
<body>
<h3 class="text-center">BẢNG THANH TOÁN TIỀN ĐIỆN</h3>
<p class="text-center">({{ $dateNow ?? '' }})</p>
<div class="mt-20">
<p>- Tên doanh nghiệp: Công ty TNHH Kỹ Thuật Công Nghệ APAC</p>
<p>- số thuế: 0110038408</p>
<p>- Địa chỉ: Số 219/26/3 đường Lĩnh Nam, Phường Vĩnh Hưng, thành phố Nội, Việt Nam</p>
<p>- Tên chủ sở hữu cho thuê địa điểm sản xuất kinh doanh: Lâm Văn Mười</p>
<p>- Địa chỉ thuê: 50B31 tại Khu dân 91B giai đoạn 3, phường Tân An, thành phố Cần Thơ</p>
</div>
<table>
<thead>
<tr>
<th>Số điện đầu kỳ</th>
<th>Số điện cuối kỳ</th>
<th>Số điện tiêu thụ</th>
<th>Đơn giá</th>
<th>Thành tiền</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ number_format($bill->previous_reading) ?? 0 }}</td>
<td>{{ number_format($bill->current_reading) ?? 0 }}</td>
<td>{{ $consumption ?? 0 }}</td>
<td>{{ number_format($bill->unit_price) ?? '0' }}</td>
<td>{{ number_format($bill->total_amount) ?? '0' }}</td>
</tr>
</tbody>
</table>
<p class="mt-20">
- Tổng tiền thanh toán: <strong>{{ number_format($bill->total_amount) ?? '0' }} VND</strong>
({{ $totalText ?? '' }})
</p>
<table class="signature no-border">
<tr>
<td>
Người lập bảng <br>
(, ghi họ tên)
</td>
<td>
Đại diện doanh nghiệp<br>
(, ghi họ tên)
</td>
</tr>
</table>
</body>
</html>

View File

@ -2,7 +2,6 @@
use App\Http\Middleware\CheckPermission;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Http\Controllers\AdminController;
use Modules\Admin\app\Http\Controllers\BannerController;
use Modules\Admin\app\Http\Controllers\CategoryController;
@ -10,7 +9,6 @@ use Modules\Admin\app\Http\Controllers\ClientController;
use Modules\Admin\app\Http\Controllers\CountryController;
use Modules\Admin\app\Http\Controllers\CustomThemeController;
use Modules\Admin\app\Http\Controllers\DashboardController;
use Modules\Admin\app\Http\Controllers\DocumentController;
use Modules\Admin\app\Http\Controllers\JiraController;
use Modules\Admin\app\Http\Controllers\LeaveManagementController;
use Modules\Admin\app\Http\Controllers\SettingController;
@ -19,12 +17,9 @@ use Modules\Admin\app\Http\Controllers\TimekeepingController;
use Modules\Admin\app\Http\Controllers\TrackingController;
use Modules\Admin\app\Http\Controllers\CriteriasController;
use Modules\Admin\app\Http\Controllers\EvaluationController;
use Modules\Admin\app\Http\Controllers\ProjectReviewController;
use Modules\Admin\app\Http\Controllers\ProfileController;
use Modules\Admin\app\Http\Controllers\TechnicalController;
use Modules\Admin\app\Http\Controllers\TestCaseForSprintController;
use Modules\Admin\app\Http\Controllers\ElectricityBillController;
use Modules\Admin\app\Http\Controllers\EquipmentController;
use Modules\Admin\app\Http\Middleware\AdminMiddleware;
/*
@ -46,7 +41,6 @@ Route::middleware('api')
Route::post('login', [AdminController::class, 'login']);
Route::post('reset-password', [AdminController::class, 'resetPassword']);
Route::get('forgot-password', [AdminController::class, 'forgotPassword']);
Route::get('/email-handle-ticket', [TicketController::class, 'handleTicketEmail'])->name('email.ticket.handle');
});
// NOTE after login
@ -117,25 +111,21 @@ Route::middleware('api')
Route::get('/get-all-sprint-by-id-board', [JiraController::class, 'getAllSprintByIdBoard'])->middleware('check.permission:admin.tester');
Route::get('/get-all-issue-by-id-sprint', [JiraController::class, 'getAllIssueByIdSprint']);
Route::get('/export-weekly-report', [JiraController::class, 'getWeeklyReport']);
Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff');
Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff');
Route::get('/issue/detail', [JiraController::class, 'getDetailIssueById'])->middleware('check.permission:admin.staff');
Route::get('/project-participating', [JiraController::class, 'getUserProjectParticipating'])->middleware('check.permission:admin');
});
Route::group([
'prefix' => 'timekeeping',
], function () {
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
Route::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr');
Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->middleware('check.permission:admin.hr');
Route::get('/delete', [TimekeepingController::class, 'deleteNote'])->middleware('check.permission:admin.hr');
Route::post('/update-cache-month', [TimekeepingController::class, 'updateCacheMonth'])->middleware('check.permission:admin');
Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr');
Route::get('/export', [TimekeepingController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
});
Route::group([
@ -144,24 +134,19 @@ Route::middleware('api')
Route::post('/create', [TrackingController::class, 'create'])->middleware('check.permission:admin.hr');
Route::post('/update', [TrackingController::class, 'update'])->middleware('check.permission:admin.hr');
Route::get('/delete', [TrackingController::class, 'delete'])->middleware('check.permission:admin.hr');
Route::get('/summary', [TrackingController::class, 'getSummaryTracking'])->middleware('check.permission:admin');
});
Route::group([
'prefix' => 'category',
], function () {
Route::get('/get-list-master', [CategoryController::class, 'getListMaster']);
Route::get('/work-days', [CategoryController::class, 'workDays'])->middleware('check.permission:admin.hr');
Route::put('/update-work-days', [CategoryController::class, 'updateWorkDays'])->middleware('check.permission:admin.hr');
});
Route::group([
'prefix' => 'leave-management',
], function () {
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/export', [LeaveManagementController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff');
Route::post('/saveNoteLeave', [LeaveManagementController::class, 'saveNoteLeave'])->middleware('check.permission:admin.hr');
Route::post('/updateNoteStatus', [LeaveManagementController::class, 'updateNoteStatus'])->middleware('check.permission:admin.hr');
});
Route::group([
@ -169,55 +154,11 @@ Route::middleware('api')
], function () {
Route::get('/all', [TicketController::class, 'getAll'])->middleware('check.permission:admin.hr');
Route::get('/getByUserId', [TicketController::class, 'getByUserId'])->middleware('check.permission:admin.hr.staff');
Route::post('/update', [TicketController::class, 'updateTicket'])->middleware('check.permission:admin.hr');
Route::post('/create', [TicketController::class, 'createTicket'])->middleware('check.permission:admin.hr.staff');
Route::get('/delete', [TicketController::class, 'deleteTicket'])->middleware('check.permission:admin.hr.staff');
Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');
});
// Electricity Bills
Route::group([
'prefix' => 'electricity-bill',
], function () {
Route::get('/', [ElectricityBillController::class, 'index'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/{id}', [ElectricityBillController::class, 'show'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/create', [ElectricityBillController::class, 'create'])->middleware('check.permission:admin.hr.staff.accountant');
Route::put('/{id}', [ElectricityBillController::class, 'update'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/delete/{id}', [ElectricityBillController::class, 'delete'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/export-pdf/{id}', [ElectricityBillController::class, 'exportPdf'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/upload-attachment/{id}', [ElectricityBillController::class, 'uploadAttachment'])->middleware('check.permission:admin.hr.staff.accountant');
Route::delete('/delete-attachment/{id}', [ElectricityBillController::class, 'deleteAttachment'])->middleware('check.permission:admin.hr.staff.accountant');
});
// Equipments
Route::group([
'prefix' => 'equipment',
], function () {
Route::get('/', [EquipmentController::class, 'index'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/users', [EquipmentController::class, 'userOptions'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/user-statistics', [EquipmentController::class, 'userStatistics'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/export', [EquipmentController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/by-code', [EquipmentController::class, 'showByCode'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/history/{id}', [EquipmentController::class, 'history'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/create', [EquipmentController::class, 'create'])->middleware('check.permission:admin.hr.staff.accountant');
Route::put('/{id}', [EquipmentController::class, 'update'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/{id}/assign', [EquipmentController::class, 'assign'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/{id}/upload-image', [EquipmentController::class, 'uploadImage'])->middleware('check.permission:admin.hr.staff.accountant');
Route::delete('/{id}/image', [EquipmentController::class, 'deleteImage'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/delete/{id}', [EquipmentController::class, 'delete'])->middleware('check.permission:admin.hr.staff.accountant');
});
Route::group([
'prefix' => 'profile',
], function () {
Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/files', [ProfileController::class, 'getFiles'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/upload-files', [ProfileController::class, 'uploadFiles'])->middleware('check.permission:admin.hr.staff.accountant');
Route::delete('/files/{id}', [ProfileController::class, 'deleteFile'])->middleware('check.permission:admin.hr.staff.accountant');
});
Route::group([
'prefix' => 'criterias',
], function () {
@ -241,11 +182,6 @@ Route::middleware('api')
Route::get('/sprint-review', [EvaluationController::class, 'sprintReview'])->middleware('check.permission:admin');
Route::get('/technical', [EvaluationController::class, 'technical'])->middleware('check.permission:admin');
Route::get('/report', [EvaluationController::class, 'report'])->middleware('check.permission:admin');
Route::get('/report-all-users', [EvaluationController::class, 'reportAllUsers'])->middleware('check.permission:admin');
Route::get('/project-review', [ProjectReviewController::class, 'getListReviews'])->middleware('check.permission:admin');
Route::post('/project-review/create', [ProjectReviewController::class, 'create'])->middleware('check.permission:admin');
Route::post('/project-review/update', [ProjectReviewController::class, 'update'])->middleware('check.permission:admin');
Route::get('/project-review/delete', [ProjectReviewController::class, 'destroy'])->middleware('check.permission:admin');
});
Route::group([
@ -260,32 +196,6 @@ Route::middleware('api')
Route::get('/get-list-user-by-tech-id/{technicalId}', [TechnicalController::class, 'getListUserByTechnicalId'])->middleware('check.permission:admin');
Route::post('/technicals-user/update', [TechnicalController::class, 'updateTechnicalsUser']);
});
Route::group([
'prefix' => 'document',
], function () {
Route::get('/all', [DocumentController::class, 'all'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/create', [DocumentController::class, 'create'])->middleware('check.permission:admin');
Route::post('/update', [DocumentController::class, 'update'])->middleware('check.permission:admin');
Route::get('/delete', [DocumentController::class, 'delete'])->middleware('check.permission:admin');
});
Route::get('/download-file/{filename}', function ($filename) {
$path = "uploads/{$filename}";
if (!Storage::disk('public')->exists($path)) {
return response()->json(['error' => 'File not found'], 404);
}
$headers = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET',
'Access-Control-Allow-Headers' => 'Content-Type',
'Content-Disposition' => 'inline; filename="' . $filename . '"',
];
return response()->file(storage_path("app/public/{$path}"), $headers);
});
});
});
@ -302,5 +212,4 @@ Route::group([
'prefix' => 'v1/admin/jira',
], function () {
Route::get('/send-worklog-report', [JiraController::class, 'sendReport']);
Route::get('/send-warning-mail', [JiraController::class, 'sendWarningMailByAllowcation']);
});

View File

@ -3,23 +3,18 @@
namespace Modules\Auth\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\LeaveDays;
use App\Traits\IsAPI;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Modules\Auth\app\Models\User;
use Illuminate\Support\Str;
use Modules\Admin\app\Models\Category;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
use App\Traits\AnalyzeData;
class UserController extends Controller
{
use IsAPI;
use AnalyzeData;
public function __construct()
{
$this->middleware('jwt.auth');
@ -39,41 +34,9 @@ class UserController extends Controller
]);
if ($request->has('id')) {
$payload = $request->only(['name', 'email', 'permission', 'is_permanent', 'is_separated']);
$payload = $request->only(['name', 'email', 'permission']);
$user = User::find($request->id);
// Không cho chuyển từ chính thức thành lại thử việc
if (!$request->is_permanent && $user->is_permanent) {
return response()->json(['status' => false, 'message' => 'You cannot change an employee from permanent to probationary.']);
}
if (!$request->is_separated && $user->is_separated) {
return response()->json(['status' => false, 'message' => 'You cannot change status of separated employee.']);
}
// Thêm ngày phép khi thành nhân viên chính thức
if ($request->is_permanent && !$user->is_permanent) {
$userLeaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', Carbon::now()->year)
->first();
if ($userLeaveDay) {
$permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
$permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
$userLeaveDay->ld_day_total = $permanentDefault;
$newNote = "Cộng ngày phép cho nhân viên chính thức"; // Thêm ghi chú
if (!empty($userLeaveDay->ld_note)) {
$userLeaveDay->ld_note = $userLeaveDay->ld_note . "\n" . $newNote;
} else {
$userLeaveDay->ld_note = $newNote;
}
$userLeaveDay->save();
}
$payload['permanent_date'] = Carbon::now()->toDateString();
}
$user->update($payload);
return response()->json(['data' => $user, 'status' => true, 'message' => 'Update successful']);
} else {
@ -81,23 +44,9 @@ class UserController extends Controller
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt('Work@1234'),
'permission' => $request->permission,
'is_permanent' => false,
'is_separated' => false
'permission' => $request->permission
]);
// Khởi tạo LeaveDays cho nhân viên mới
LeaveDays::insert([
'ld_user_id' => $user->id,
'ld_day_total' => 0,
'ld_year' => Carbon::now()->year,
'ld_additional_day' => 0,
'ld_note' => '',
'created_at' => now(),
'updated_at' => now(),
]);
$this->createOrUpdateRecordForCurrentMonth(Carbon::now()->month, Carbon::now()->year);
$user_res = [
'name' => $user->name,
'email' => $user->email,
@ -149,6 +98,8 @@ class UserController extends Controller
return response()->json(['data' => ['user' => $user_res, 'gitea' => "dev", 'zulip' => "dev"], 'status' => true, 'message' => 'Create successful']);
}
}
return response()->json(['status' => false, 'message' => 'Process fail']);
}
public function delete(Request $request)

View File

@ -25,10 +25,7 @@ class User extends Authenticatable implements JWTSubject
'name',
'email',
'password',
'permission',
'is_permanent',
'is_separated',
'permanent_date'
'permission'
];
/**

View File

@ -1,24 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\AddMonthlyLeaveDays;
class AddMonthlyLeaveDaysCommand extends Command
{
protected $signature = 'add:monthly-leavedays {month?} {year?}';
protected $description = 'Cộng 1 ngày phép hàng tháng cho tất cả người dùng';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$month = $this->argument('month');
$year = $this->argument('year');
AddMonthlyLeaveDays::dispatch($month, $year);
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Jobs\CheckUserAttendanceJob;
use Illuminate\Console\Command;
class CheckUserAttendanceCommand extends Command
{
// Định nghĩa command signature
protected $signature = 'attendance:check {period?}';
protected $description = 'Kiểm tra check in và check out của người dùng và tạo ticket nếu thiếu';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
// Lấy argument period (Sáng "S" hoặc Chiều "C")
$period = $this->argument('period');
// Dispatch job để kiểm tra check in và check out
CheckUserAttendanceJob::dispatch($period);
}
}

View File

@ -8,7 +8,7 @@ use App\Jobs\InitializeLeaveDays;
class InitializeLeaveDaysCommand extends Command
{
protected $signature = 'initialize:leavedays {year?}';
protected $description = 'Cấp phép năm cho tất cả người dùng';
protected $description = 'Initialize leave days for users';
public function __construct()
{
@ -17,6 +17,7 @@ class InitializeLeaveDaysCommand extends Command
public function handle()
{
InitializeLeaveDays::dispatch();
$year = $this->argument('year');
InitializeLeaveDays::dispatch($year);
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\UpdateTemporaryLeaveDays;
class UpdateTemporaryLeaveDaysCommand extends Command
{
protected $signature = 'update:temporary-leavedays {month?} {year?}';
protected $description = 'Tính lại ngày phép cho các note tạm.';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$month = $this->argument('month');
$year = $this->argument('year');
UpdateTemporaryLeaveDays::dispatch($month, $year);
}
}

View File

@ -3,7 +3,6 @@
namespace App\Console;
use App\Jobs\DeductLeaveDays;
use App\Jobs\AddMonthlyLeaveDays;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -24,21 +23,9 @@ class Kernel extends ConsoleKernel
// $schedule->command('daily:api-call')
// ->dailyAt('18:00');
// Chạy command vào ngày 01/01 lúc 00:00 mỗi năm
$schedule->command('initialize:leavedays')->yearlyOn(1, 1, '00:00');
// Chạy command vào ngày 01/04 lúc 00:00 mỗi năm
$schedule->command('leave:deduct')->yearlyOn(4, 1, '00:00');
// Chạy buổi sáng lúc 12:00
$schedule->command('attendance:check S')->dailyAt('12:00');
// Chạy buổi chiều lúc 17:30
$schedule->command('attendance:check C')->dailyAt('17:30');
// Chạy vào ngày đầu tiên của mỗi tháng
$schedule->command('add:monthly-leavedays')->monthlyOn(1, '00:01');
$schedule->command('update:temporary-leavedays')->monthlyOn(1, '00:05');
// Chạy command vào ngày 31/12 lúc 23:59:59 mỗi năm
$schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59');
$schedule->command('leave:deduct')->yearlyOn(3, 31, '23:59:59');
}
/**

View File

@ -1,194 +0,0 @@
<?php
namespace App\Exports;
use Carbon\Carbon;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
class LeaveManagementExport implements FromArray, WithHeadings, WithStyles, WithEvents
{
protected $data;
protected $year;
public function __construct($data)
{
$this->data = $data;
$this->year = Carbon::now()->year;
}
public function headings(): array
{
$months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return array_merge(
['No.', 'User'],
$months,
['Total', 'Off', 'Remaining', 'Notes']
);
}
public function array(): array
{
$headers = $this->headings(); // Lấy tiêu đề
$rows = [];
$stt = 0;
foreach ($this->data as $index => $user) {
$totalDayOff = 0;
$totalDayLeave = $user['leaveDay']['ld_day_total'] + $user['leaveDay']['ld_additional_day'];
// Tính tổng ngày nghỉ theo tháng
$monthlyLeaves = array_fill(1, 12, 0);
foreach ($user['monthlyLeaveDays'] as $leaveDay) {
$monthlyLeaves[$leaveDay['month']] += $leaveDay['leave_days'];
$totalDayOff += $leaveDay['leave_days'];
}
// Tạo dòng dữ liệu
$row = [
$stt + 1,
$user['user']['name']
];
// Thêm dữ liệu các tháng
for ($month = 1; $month <= 12; $month++) {
$row[] = $monthlyLeaves[$month] ?: '';
}
// Thêm tổng số ngày
$row[] = $totalDayLeave;
$row[] = $totalDayOff;
$row[] = $totalDayLeave - $totalDayOff;
$row[] = $user['leaveDay']['ld_note'] ?? '';
$rows[] = $row;
$stt++;
}
return array_merge([$headers], $rows); // Thêm tiêu đề vào đầu mảng
}
public function registerEvents(): array
{
return [
AfterSheet::class => function(AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastRow = count($this->data) + 2;
$noteColumn = 'R'; // Cột Notes
// Xử lý đặc biệt cho cột Notes
$sheet->getStyle("{$noteColumn}3:{$noteColumn}{$lastRow}")
->getAlignment()
->setWrapText(true)
->setVertical(Alignment::VERTICAL_TOP)
->setHorizontal(Alignment::HORIZONTAL_LEFT);
// Tắt auto-size cho cột Notes và set độ rộng cố định
$sheet->getColumnDimension($noteColumn)
->setAutoSize(false)
->setWidth(60);
// Tự động điều chỉnh chiều cao cho từng dòng có nội dung
for ($row = 3; $row <= $lastRow; $row++) {
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
if (!empty($cellValue)) {
// Tính toán chiều cao dựa trên nội dung
$sheet->getRowDimension($row)->setRowHeight(-1);
// Tính toán lại chiều cao dựa trên số dòng trong nội dung
$lineCount = substr_count($cellValue, "\n") + 1;
$minHeight = max(30, $lineCount * 15); // 15 pixels cho mỗi dòng
// Lấy chiều cao hiện tại sau khi auto-size
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
// Nếu chiều cao tự động nhỏ hơn chiều cao tối thiểu, sử dụng chiều cao tối thiểu
if ($currentHeight < $minHeight) {
$sheet->getRowDimension($row)->setRowHeight($minHeight);
}
} else {
$sheet->getRowDimension($row)->setRowHeight(30);
}
}
// Refresh các tính toán của Excel
$sheet->calculateColumnWidths();
},
];
}
public function styles(Worksheet $sheet)
{
$lastRow = count($this->data) + 2;
$lastColumn = 'R';
// Thêm và style title
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->setCellValue('A1', "DANH SÁCH NGÀY NGHỈ NĂM {$this->year}");
$sheet->getStyle("A1:{$lastColumn}1")->applyFromArray([
'font' => [
'bold' => true,
'size' => 14
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Style cho header (dời xuống row 2)
$sheet->getStyle("A2:{$lastColumn}2")->applyFromArray([
'font' => ['bold' => true],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Style cho toàn bộ bảng (bắt đầu từ row 1)
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray([
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN
]
],
'alignment' => [
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Căn giữa cho các cột số liệu (điều chỉnh range bắt đầu từ row 3)
for ($col = 'C'; $col <= 'P'; $col++) {
$sheet->getStyle("{$col}3:{$col}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER);
}
// Set độ rộng cột
$sheet->getColumnDimension('A')->setWidth(5); // No.
$sheet->getColumnDimension('B')->setWidth(30); // User
// Các tháng
for ($i = 'C'; $i <= 'N'; $i++) {
$sheet->getColumnDimension($i)->setWidth(8);
}
$sheet->getColumnDimension('O')->setWidth(8); // Total
$sheet->getColumnDimension('P')->setWidth(8); // Off
$sheet->getColumnDimension('Q')->setWidth(12); // Remaining
$sheet->getColumnDimension('R')->setWidth(30); // Notes
// Điều chỉnh style cho cột Notes
$sheet->getStyle("R3:R{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_LEFT)
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
return $sheet;
}
}

View File

@ -1,248 +0,0 @@
<?php
namespace App\Exports;
use Carbon\Carbon;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
class TimekeepingExport implements FromArray, WithHeadings, WithStyles, WithEvents
{
protected $data;
protected $month;
protected $year;
protected $workingDays;
protected $daysInMonth;
public function __construct($data, $month, $year, $workingDays)
{
$this->data = $data;
$this->month = $month;
$this->year = $year;
$this->workingDays = $workingDays;
$this->daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
}
public function registerEvents(): array
{
return [
AfterSheet::class => function(AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastRow = count($this->data) + 4;
$noteColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
// Xử lý đặc biệt cho cột Notes
$sheet->getStyle("{$noteColumn}5:{$noteColumn}{$lastRow}")
->getAlignment()
->setWrapText(true)
->setVertical(Alignment::VERTICAL_TOP)
->setHorizontal(Alignment::HORIZONTAL_LEFT);
// Tắt auto-size cho cột Notes và set độ rộng cố định
$sheet->getColumnDimension($noteColumn)
->setAutoSize(false)
->setWidth(60);
// Tự động điều chỉnh chiều cao cho từng dòng có nội dung
for ($row = 5; $row <= $lastRow; $row++) {
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
if (!empty($cellValue)) {
// Tính toán chiều cao dựa trên nội dung
$sheet->getRowDimension($row)->setRowHeight(-1);
// Tính toán lại chiều cao dựa trên số dòng trong nội dung
$lineCount = substr_count($cellValue, "\n") + 1;
$minHeight = max(30, $lineCount * 15); // 15 pixels cho mỗi dòng
// Lấy chiều cao hiện tại sau khi auto-size
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
// Nếu chiều cao tự động nhỏ hơn chiều cao tối thiểu, sử dụng chiều cao tối thiểu
if ($currentHeight < $minHeight) {
$sheet->getRowDimension($row)->setRowHeight($minHeight);
}
} else {
$sheet->getRowDimension($row)->setRowHeight(30);
}
}
// Refresh các tính toán của Excel
$sheet->calculateColumnWidths();
},
];
}
public function array(): array
{
// Lấy headers
$headers = $this->headings();
// Lấy dữ liệu người dùng
$userRows = [];
foreach ($this->data as $user) {
$row = [
$user['user']['name'] ?? 'Unknown',
0, // Total days
$this->workingDays, // Off days (initialize with working days)
];
$totalDays = 0;
// Add data for each day in month
for ($day = 1; $day <= $this->daysInMonth; $day++) {
$dayData = '';
if (isset($user['history'])) {
foreach ($user['history'] as $history) {
if ($history['day'] === $day) {
$total = $history['total'] ?? 0;
if ($total >= 7 * 3600) {
$dayData = '1';
$totalDays += 1;
} else if ($total >= 3.5 * 3600) {
$dayData = '0.5';
$totalDays += 0.5;
}
break;
}
}
}
$row[] = $dayData;
}
// Update total and off days
$row[1] = $totalDays;
$row[2] = $this->workingDays - $totalDays;
// Add Notes column with formatted content
$notes = [];
if (isset($user['history'])) {
foreach ($user['history'] as $history) {
if (!empty($history['notes'])) {
$dayNotes = [];
foreach ($history['notes'] as $note) {
$dayNotes[] = "- {$note['reasonName']} ({$note['timeTypeName']}): {$note['note']}";
}
if (!empty($dayNotes)) {
$notes[] = "Day {$history['day']}:\n" . implode("\n", $dayNotes);
}
}
}
}
$row[] = !empty($notes) ? implode("\n\n", $notes) : '';
$userRows[] = $row;
}
return array_merge($headers, $userRows);
}
public function headings(): array
{
$firstRow = ['Day', '', ''];
// Second row: Day of week
$secondRow = ['', '', ''];
$date = Carbon::create($this->year, $this->month, 1);
for ($day = 1; $day <= $this->daysInMonth; $day++) {
$firstRow[] = $day;
$secondRow[] = $date->format('D');
$date->addDay();
}
// Add Notes column
$firstRow[] = 'Notes';
$secondRow[] = '';
return [$firstRow, $secondRow];
}
public function styles(Worksheet $sheet)
{
$lastColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
$lastRow = count($this->data) + 4;
$noteColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
// Title and working days
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->setCellValue('A1', "DANH SÁCH CHẤM CÔNG THÁNG {$this->month} NĂM {$this->year}");
$sheet->mergeCells("A2:{$lastColumn}2");
$sheet->setCellValue('A2', "Số ngày làm việc: {$this->workingDays}");
// Merge cells for "Day" title and set value
$sheet->mergeCells("A3:C3");
$sheet->setCellValue('A3', 'Day');
// Set values for A4, B4, C4
$sheet->setCellValue('A4', 'User');
$sheet->setCellValue('B4', 'Total');
$sheet->setCellValue('C4', 'Off');
// Styling
$sheet->getStyle("A1:{$lastColumn}1")->getFont()->setBold(true)->setSize(14);
$sheet->getStyle("A2:{$lastColumn}2")->getFont()->setBold(true);
$sheet->getStyle("A3:{$lastColumn}4")->getFont()->setBold(true);
// Border style
$borderStyle = [
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
],
],
];
// Apply borders to the data area
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray($borderStyle);
// Center align all cells except Notes column
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Left align Notes column và bật wrap text
$sheet->getStyle("{$noteColumn}5:{$noteColumn}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_LEFT)
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
// Set width for Note column - tăng độ rộng để hiển thị tốt hơn
$sheet->getColumnDimension($noteColumn)->setWidth(60);
// Tự động điều chỉnh chiều cao cho các dòng có nội dung Notes
for ($row = 5; $row <= $lastRow; $row++) {
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
if (!empty($cellValue)) {
// Đết chiều cao tự động
$sheet->getRowDimension($row)->setRowHeight(-1);
// Đảm bảo chiều cao tối thiểu
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
if ($currentHeight < 30) {
$sheet->getRowDimension($row)->setRowHeight(30);
}
} else {
// Chiều cao mặc định cho các dòng không có note
$sheet->getRowDimension($row)->setRowHeight(30);
}
}
// Set column widths
$sheet->getColumnDimension('A')->setWidth(30);
$sheet->getColumnDimension('B')->setWidth(10);
$sheet->getColumnDimension('C')->setWidth(10);
for ($i = 4; $i <= $this->daysInMonth + 3; $i++) {
$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i))->setWidth(5);
}
return [];
}
}

View File

@ -1,122 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\LeaveDays;
use App\Models\Notes;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Modules\Admin\app\Models\Category;
use Illuminate\Support\Facades\DB;
class AddMonthlyLeaveDays implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $month;
protected $year;
private const ONLEAVE_PER_MONTH = 1; // Ngày phép cộng mỗi tháng
public function __construct($month = null, $year = null)
{
$this->month = $month ?? Carbon::now()->month;
$this->year = $year ?? Carbon::now()->year;
}
public function handle(): void
{
$users = User::get();
foreach ($users as $user) {
// Nếu là nhân viên chưa chính thức, ko cộng phép
if (!$user->is_permanent) {
continue;
}
// Nếu là nhân viên nghỉ việc, ko cộng phép
if ($user->is_separated) {
continue;
}
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
if (!$leaveDay && $this->month > 1) {
// Nếu chưa có dữ liệu năm hiện tại, tạo mới
$previousYearData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year - 1)
->first();
$ld_additional_day = 0;
$ld_note = '';
if ($previousYearData) {
$ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day + $previousYearData->ld_special_leave_day;
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
})
->select(
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_year as year'),
DB::raw('SUM(categories.c_value) as leave_days')
)
->where('notes.n_year', $this->year - 1)
->where('notes.n_user_id', $user->id)
->where('notes.n_reason', 'ONLEAVE')
->groupBy(DB::raw('notes.n_year'))
->first();
if ($totalLeaveDaysByMonth) {
$ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
if ($ld_additional_day < 0) {
$ld_additional_day = 0;
}
}
if ($ld_additional_day > 0) {
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
}
}
$leaveDay = new LeaveDays([
'ld_user_id' => $user->id,
'ld_day_total' => $this->month, // Số ngày phép bằng tháng hiện tại
'ld_year' => $this->year,
'ld_additional_day' => $ld_additional_day,
'ld_note' => $ld_note,
'ld_special_leave_day' => 0,
]);
$leaveDay->save();
} else {
// Check có phải là nhân viên chính thức trong năm nay (Nhân viên mới)
if ($user->permanent_date && $user->permanent_date !== '0000-00-00') {
$permenantYear = Carbon::parse($user->permanent_date)->year;
if ($permenantYear === $this->year) {
$permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
$permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
$permanentMonth = Carbon::parse($user->permanent_date)->month;
if ($this->month > $leaveDay->ld_day_total - ($permanentDefault - $permanentMonth)) {
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
$leaveDay->save();
}
}
}
// Kiểm tra nếu số ngày phép hiện tại nhỏ hơn tháng hiện tại (Nhân viên cũ)
if ($leaveDay->ld_day_total < $this->month) {
// Cộng mỗi tháng 1 ngày phép cho nhân viên
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
$leaveDay->save();
}
}
}
}
}

View File

@ -1,207 +0,0 @@
<?php
namespace App\Jobs;
use App\Mail\TicketMail;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use Modules\Admin\app\Http\Controllers\CategoryController;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Ticket;
use Modules\Admin\app\Models\Tracking;
class CheckUserAttendanceJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $period;
/**
* Create a new job instance.
* @param string|null $period
*/
public function __construct($period = null)
{
$this->period = $period;
}
/**
* Execute the job.
*/
public function handle()
{
// Lấy tất cả người dùng
$users = User::where('permission', 'not like', '%admin%')
->where('permission', 'not like', '%accountant%')->get();
foreach ($users as $key => $user) {
// Check nhân viên nghỉ việc
if ($user->is_separated) {
continue;
}
// Kiểm tra dựa trên period (Sáng 'S' hoặc Chiều 'C')
if ($this->period === 'S') {
$this->checkMorning($user);
} elseif ($this->period === 'C') {
$this->checkAfternoon($user);
} else {
if ($this->period == null) {
// Nếu không có period, kiểm tra cả sáng và chiều
$this->checkMorning($user);
$this->checkAfternoon($user);
}
}
}
}
private function checkAttendance($user, $periodCode, $startTime, $endTime)
{
$today = Carbon::today();
$userId = $user->id;
// Lấy tất cả tracking của user trong khoảng thời gian được chỉ định
$records = Tracking::where('user_id', $userId)
->whereBetween('time_string', [$startTime, $endTime])
->get();
$hasCheckIn = $records->where('status', 'check in')->isNotEmpty();
$hasCheckOut = $records->where('status', 'check out')->isNotEmpty();
// Kiểm tra nếu đã có ticket chờ xử lý hoặc đã được xác nhận, cho cả sáng và chiều
$existingTicket = Ticket::where('user_id', $userId)
->where(function ($query) use ($today, $periodCode) {
// Check for tickets that exactly cover the current day and period
$query->where(function ($subQuery) use ($today, $periodCode) {
$subQuery->where('start_date', $today->format('Y-m-d'))
->where('end_date', $today->format('Y-m-d'))
->where(function ($periodQuery) use ($periodCode) {
// Check if the ticket covers the current period
$periodQuery->where('start_period', $periodCode)
->orWhere(function ($query) {
// Check for a full-day ticket (S -> C)
$query->where('start_period', 'S')
->where('end_period', 'C');
});
});
})
// Check for tickets that span multiple days
->orWhere(function ($subQuery) use ($today, $periodCode) {
$subQuery->where('start_date', '<=', $today->format('Y-m-d'))
->where('end_date', '>=', $today->format('Y-m-d'))
->where(function ($periodQuery) use ($periodCode, $today) {
$periodQuery->where(function ($query) use ($today) {
$query->where('start_date', '<', $today->format('Y-m-d'))
->where('end_date', '>', $today->format('Y-m-d'));
})
->orWhere(function ($query) use ($today, $periodCode) {
$query->where(function ($queryDateStart) use ($today, $periodCode) {
$queryDateStart->where('start_date', '=', $today->format('Y-m-d'))
->where('end_date', '>', $today->format('Y-m-d'))
->where(function ($queryDateStartChild) use ($periodCode) {
$queryDateStartChild->where('start_period', $periodCode)
->orWhere(function ($query) {
$query->where('start_period', 'S');
});
});
})
->orWhere(function ($queryDateEnd) use ($today, $periodCode) {
$queryDateEnd->where('end_date', '=', $today->format('Y-m-d'))
->where('start_date', '<', $today->format('Y-m-d'))
->where(function ($queryDateStartChild) use ($periodCode) {
$queryDateStartChild->where('end_period', $periodCode)
->orWhere(function ($query) {
$query->where('end_period', 'C');
});
});
});
});
});
});
})
->whereIn('status', ['WAITING', 'CONFIRMED'])
->first();
$type = 'ONLEAVE';
$reason = 'KHONG PHEP';
//Check ngày hợp lệ nếu có check và check out của user khác
$dateNow = Tracking::whereBetween('time_string', [$startTime, $endTime])->exists();
// Nếu không có check in/out, không gửi ticket và có check in/out của người khác => tạo ticket không phép
if ($hasCheckIn || $hasCheckOut) {
return;
}
if ($existingTicket) {
return;
}
if ($dateNow) {
$ticket = Ticket::create([
'user_id' => $userId,
'start_date' => $today->format('Y-m-d'),
'start_period' => $periodCode,
'end_date' => $today->format('Y-m-d'),
'end_period' => $periodCode,
'type' => $type,
'reason' => $reason,
'status' => 'WAITING',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
//Send mail
$dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $periodCode);
$dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $periodCode);
$dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type);
$formattedStartDate = $today->format('d/m/Y');
$formattedEndDate = $today->format('d/m/Y');
$admins = Admin::where('permission', 'like', '%admin%')->get();
foreach ($admins as $key => $value) {
$data = array(
"ticket_id" => $ticket->id,
"email_template" => "email.notification_tickets",
"email" => $user->email,
"admin_email" => $value->email,
"name" => $user->name,
"date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
"type" => $dataMasterType->c_name,
"note" => $reason,
"link" => "/tickets-management", //link đến page admin
"subject" => "[Ticket request] Ticket From " . $user->name
);
Mail::to($value->email)->send(new TicketMail($data));
}
}
}
/**
* Kiểm tra check-in/check-out buổi sáng tạo ticket nếu thiếu.
* @param int $userId
*/
public function checkMorning($user)
{
$startTime = Carbon::today()->setTime(6, 0); // Thời gian bắt đầu buổi sáng
$endTime = Carbon::createFromTime(12, 0); // Thời gian kết thúc buổi sáng
$this->checkAttendance($user, 'S', $startTime, $endTime);
}
/**
* Kiểm tra check-in/check-out buổi chiều tạo ticket nếu thiếu.
* @param int $userId
*/
public function checkAfternoon($user)
{
$startTime = Carbon::today()->setTime(12, 0); // Thời gian bắt đầu buổi chiều
$endTime = Carbon::createFromTime(17, 30); // Thời gian kết thúc buổi chiều
$this->checkAttendance($user, 'C', $startTime, $endTime);
}
}

View File

@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class DeductLeaveDays implements ShouldQueue
@ -35,36 +36,44 @@ class DeductLeaveDays implements ShouldQueue
foreach ($users as $user) {
$existingData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->where('ld_additional_day', ">", 0)
->where('ld_date_additional', ">", 0)
->first();
if (!$existingData) {
continue;
}
// Lấy tổng ngày nghỉ phép 3 tháng đầu trong năm
$usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
})
->where('n_user_id', $user->id)
->where('n_year', $this->year)
->where('n_month', "<=", 3)
->where('n_reason', 'ONLEAVE')
->sum('categories.c_value');
->select(
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_year as year'),
DB::raw('SUM(categories.c_value) as leave_days')
)
->where('notes.n_year', $this->year)
->where('notes.n_user_id', $user->id)
->where('notes.n_reason', 'ONLEAVE')
->groupBy(DB::raw('notes.n_year'))
->first();
if ($usedOnleaveDaysTotal) {
if ($existingData->ld_additional_day > $usedOnleaveDaysTotal) {
$ld_note = "Trừ " . $existingData->ld_additional_day - $usedOnleaveDaysTotal . " ngày phép tồn năm trước. \n";
$existingData->ld_note = $existingData->ld_note . $ld_note;
$existingData->ld_additional_day = $usedOnleaveDaysTotal;
if ($totalLeaveDaysByMonth) {
//Nếu ngày phép thừa năm trước chưa sử dụng hết => cập nhật lại ngày đó (Ngày tồn đọng - ngày sử dụng)
if ($existingData->ld_date_additional > $totalLeaveDaysByMonth->leave_days) {
LeaveDays::where('ld_year', $this->year)
->where('ld_user_id', $user->id)
->update([
'ld_date_additional' => $totalLeaveDaysByMonth->leave_days,
]);
}
} else {
$ld_note = "Trừ " . $existingData->ld_additional_day . " ngày phép tồn năm trước. \n";
$existingData->ld_note = $existingData->ld_note . $ld_note;
$existingData->ld_additional_day = 0;
}
$existingData->save();
//Nếu không sử dụng ngày nghỉ còn lại ở nằm rồi thì xóa => theo luật ld
LeaveDays::where('ld_year', $this->year)
->where('ld_user_id', $user->id)
->update([
'ld_date_additional' => "0",
]);
}
}
}
}

View File

@ -34,11 +34,15 @@ class InitializeLeaveDays implements ShouldQueue
public function handle(): void
{
$users = User::get();
$ld_day_total = Carbon::now()->month; // Khởi tạo phép hiện có bằng tháng hiện tại
$ld_day = 12;
foreach ($users as $user) {
// Check nhân viên nghỉ việc
if ($user->is_separated) {
// Kiểm tra xem dữ liệu của user này đã tồn tại cho năm hiện tại chưa
$existingData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
if ($existingData) {
// Nếu dữ liệu đã tồn tại, bỏ qua user này
continue;
}
@ -47,11 +51,11 @@ class InitializeLeaveDays implements ShouldQueue
->where('ld_year', $this->year - 1)
->first();
$ld_additional_day = 0;
$ld_date_additional = 0;
$ld_note = '';
if ($previousYearData) {
$ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day + $previousYearData->ld_special_leave_day;
$ld_date_additional = $previousYearData->ld_day + $previousYearData->ld_date_additional;
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
@ -67,36 +71,20 @@ class InitializeLeaveDays implements ShouldQueue
->groupBy(DB::raw('notes.n_year'))
->first();
if ($totalLeaveDaysByMonth) {
$ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
if ($ld_additional_day < 0) {
$ld_additional_day = 0;
$ld_date_additional = $ld_date_additional - $totalLeaveDaysByMonth->leave_days;
if ($ld_date_additional < 0) {
$ld_date_additional = 0;
}
}
if ($ld_additional_day > 0) {
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
}
}
// Kiểm tra xem dữ liệu của user này đã tồn tại cho năm hiện tại chưa
$existingData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
if ($existingData) {
// Nếu dữ liệu đã tồn tại, update lại phép tồn
$existingData->ld_note = $ld_note;
$existingData->ld_additional_day = $ld_additional_day;
$existingData->save();
continue;
$ld_note = 'Cộng dồn ngày phép năm cũ';
}
// Tạo dữ liệu cho năm hiện tại
LeaveDays::insert([
'ld_user_id' => $user->id,
'ld_day_total' => $user->is_permanent ? $ld_day_total : 0, // Nếu là nhân viên mới, ko cấp phép
'ld_day' => $ld_day,
'ld_year' => $this->year,
'ld_additional_day' => $ld_additional_day,
'ld_date_additional' => $ld_date_additional,
'ld_note' => $ld_note,
'created_at' => now(),
'updated_at' => now(),

View File

@ -1,220 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\LeaveDays;
use App\Models\Notes;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Admin\app\Models\Category;
class UpdateTemporaryLeaveDays implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $month;
protected $year;
public function __construct($month = null, $year = null)
{
$this->month = $month ?? Carbon::now()->month;
$this->year = $year ?? Carbon::now()->year;
}
/**
* Execute the job.
*/
public function handle()
{
$users = User::get();
foreach ($users as $user) {
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
$notes = Notes::where('n_reason', 'TEMPORARY_ONLEAVE')
->where('n_user_id', $user->id)
->where('n_year', $this->year)
->where('n_month', $this->month)
->whereExists(function ($query) use ($user) {
$query->select(DB::raw(1))
->from('tickets')
->where('tickets.user_id', $user->id)
->where('tickets.status', 'CONFIRMED')
->where('tickets.type', 'ONLEAVE');
})
->get();
$maxDaysPerMonth = $this->getMaxLeaveDaysPerMonth();
// Tổng ngày nghỉ sẽ dùng trong tháng
$willUsedDaysInMonth = 0;
foreach ($notes as $note) {
$willUsedDaysInMonth += $note->n_time_type == 'ALL' ? 1.0 : 0.5;
}
// Tổng phép đang có
$onleaveDaysTotal = $leaveDay->ld_day_total + $leaveDay->ld_additional_day + $leaveDay->ld_special_leave_day;
// Phép đã sử dụng tới tháng hiện tại
$usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
})
->where('n_user_id', $user->id)
->where('n_year', $this->year)
->where('n_month', "<=", $this->month)
->where('n_reason', 'ONLEAVE')
->sum('categories.c_value');
// Phép còn lại
$remainingOnleaveDays = $onleaveDaysTotal - $usedOnleaveDaysTotal;
// Log::debug("User {$user->name}\n");
// Log::debug(
// "📊 Thống kê ngày phép:\n" .
// " - Tháng: {$this->month}\n" .
// " - Tổng ngày nghỉ sẽ dùng trong tháng: $willUsedDaysInMonth\n" .
// " - Tổng ngày phép: $onleaveDaysTotal\n" .
// " - Tổng ngày phép đã nghỉ: $usedOnleaveDaysTotal\n" .
// " - Tổng ngày phép còn lại: $remainingOnleaveDays\n"
// );
$onleave_days_will_use = 0; // Ngày phép sẽ dùng
$nopay_days_will_use = 0; // Ngày ko phép sẽ dùng
// Ngày phép còn lại <= 0 (Hết phép)
if ($remainingOnleaveDays <= 0) {
$onleave_days_will_use = 0;
$nopay_days_will_use = $willUsedDaysInMonth;
Log::debug("--- Hết phép trong tháng ---");
}
// Ngày phép còn lại < ngày yêu cầu (Không đủ phép)
else if ($remainingOnleaveDays < $willUsedDaysInMonth) {
// Vượt limit
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
// Phép còn lại > limit
if ($remainingOnleaveDays > $maxDaysPerMonth) {
$onleave_days_will_use = $maxDaysPerMonth;
$nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
}
// Phép còn lại < limit
else {
$onleave_days_will_use = $remainingOnleaveDays;
$nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
}
Log::debug("--- Không đủ phép trong tháng, vượt quá limit ---",);
}
// Không vượt limit
else {
$onleave_days_will_use = $remainingOnleaveDays;
$nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
Log::debug("--- Không đủ phép trong tháng, ko vượt limit ---");
}
}
// Ngày phép còn lại >= ngày yêu cầu (Đủ phép)
else {
// Vượt limit
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
$onleave_days_will_use = $maxDaysPerMonth;
$nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
Log::debug("--- Đủ phép, vượt limit ---");
}
// Không vượt limit
else {
$onleave_days_will_use = $willUsedDaysInMonth;
$nopay_days_will_use = 0;
Log::debug("--- Đủ phép ---");
}
}
Log::debug("", [
"Phep" => $onleave_days_will_use,
"Khong Phep" => $nopay_days_will_use
]);
// Có nghỉ không phép
if ($nopay_days_will_use > 0) {
foreach ($notes as $note) {
$value = ($note->n_time_type === 'ALL') ? 1.0 : 0.5;
if ($note->n_time_type === 'ALL' && $onleave_days_will_use == 0.5) {
// Chỉ còn 0.5 phép, chia thành 2 bản ghi: 1 phép, 1 không phép
// Ưu tiên phép cho buổi sáng (S), không phép cho buổi chiều (C)
Notes::create([
'n_user_id' => $user->id,
'n_day' => $note->n_day,
'n_month' => $note->n_month,
'n_year' => $note->n_year,
'n_time_type' => 'S',
'n_reason' => 'ONLEAVE',
'n_note' => $note->n_note,
'ticket_id' => $note->ticket_id
]);
Notes::create([
'n_user_id' => $user->id,
'n_day' => $note->n_day,
'n_month' => $note->n_month,
'n_year' => $note->n_year,
'n_time_type' => 'C',
'n_reason' => 'LEAVE_WITHOUT_PAY',
'n_note' => $note->n_note,
'ticket_id' => $note->ticket_id
]);
$note->delete();
$onleave_days_will_use = 0;
$nopay_days_will_use -= 0.5;
} elseif ($onleave_days_will_use > 0) {
// Dùng ngày phép trước
$use = min($onleave_days_will_use, $value);
$note->update([
'n_reason' => "ONLEAVE"
]);
$onleave_days_will_use -= $use;
} elseif ($nopay_days_will_use > 0) {
// Hết phép, chuyển sang không phép
$use = min($nopay_days_will_use, $value);
$note->update([
'n_reason' => "LEAVE_WITHOUT_PAY"
]);
$nopay_days_will_use -= $use;
}
}
}
// Đủ phép
else {
foreach ($notes as $note) {
$note->update([
'n_reason' => "ONLEAVE"
]);
}
}
}
}
private function getMaxLeaveDaysPerMonth(): int
{
$limitLeaveMonth = Category::where('c_type', 'LIMIT_LEAVE_MONTH')->where('c_code', "LIMIT")->first();
if ($limitLeaveMonth) {
$maxDaysPerMonth = (int)$limitLeaveMonth->c_value;
} else {
$maxDaysPerMonth = 3; // default nếu k có setting
}
return $maxDaysPerMonth;
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Illuminate\Mail\Mailables\Address;
class CheckinLateMail extends Mailable
{
use Queueable, SerializesModels;
public $data;
/**
* Create a new message instance.
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
// replyTo: [
// new Address($this->data["email"], $this->data["name"]),
// ],
subject: $this->data["subject"],
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: ($this->data["email_template"])
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Mail;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WarningLongTask extends Mailable
{
use Queueable, SerializesModels;
public $data;
/**
* Create a new message instance.
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '[SYSTEM] - Task are forgotten - '.$this->data[0]['fields']['assignee']['displayName'],
);
}
public function build(): self
{
return $this->view('email.warningLongTask', ['data'=> $this->data]);
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Files extends Model
{
use HasFactory;
protected $fillable = [
'name',
'url',
'type',
'description',
'user_id'
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -10,7 +10,7 @@ class LeaveDays extends Model
use HasFactory;
protected $fillable = [
'id', 'ld_user_id', 'ld_day_total', 'ld_year', 'ld_additional_day', 'ld_note'
'id', 'ld_user_id', 'ld_day', 'ld_year', 'ld_date_additional', 'ld_note'
];
protected $table = 'leave_days';

View File

@ -11,14 +11,7 @@ class Notes extends Model
use HasFactory;
protected $fillable = [
'n_user_id',
'n_day',
'n_month',
'n_year',
'n_time_type',
'n_reason',
'n_note',
'ticket_id'
'n_user_id', 'n_day', 'n_month', 'n_year', 'n_time_type', 'n_reason', 'n_note',
];
/**
@ -32,7 +25,7 @@ class Notes extends Model
{
return self::leftJoin("categories as reason", function ($join) {
$join->on('n_reason', '=', 'reason.c_code');
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
})
->leftJoin("categories as timeTypes", function ($join) {
$join->on('n_time_type', '=', 'timeTypes.c_code');
@ -54,18 +47,4 @@ class Notes extends Model
)
->get();
}
public static function getNotesByMonthAndYearAndUserId($month, $year, $userId, $idNote)
{
return self::where('n_reason', 'ONLEAVE')->where('n_month', $month)->where('n_year', $year)
->where('n_user_id', $userId)
->where('id', '!=', $idNote)->get();
}
public static function getNotesByMonthAndYearAndUserIdAndReason($month, $year, $userId, $reason)
{
return self::where('n_reason', $reason)->where('n_month', $month)->where('n_year', $year)
->where('n_user_id', $userId)
->orderBy('n_day', 'asc')->orderBy('n_time_type', 'desc')->get();
}
}

View File

@ -23,9 +23,7 @@ class JiraService
'Authorization' => $this->authHeader,
'Accept' => 'application/json',
'Content-Type' => 'application/json'
],
'timeout' => 60, // Tăng thời gian timeout lên 60 giây
'connect_timeout' => 30 // Tăng thời gian chờ kết nối lên 30 giây
]
]);
}
@ -232,7 +230,6 @@ class JiraService
$groupedIssues = [];
$users_data = [];
$user_warning = [];
$ignore_projects = ['PJ_tracking'];
foreach ($users as $user) {
$user = (array)$user[0];
$users_data[$user['displayName']]['user'] = $user;
@ -255,22 +252,14 @@ class JiraService
$issues = json_decode($response->getBody()->getContents(), true);
// Lọc các issue không thuộc các project bị ignore
$filtered_issues = array_filter($issues['issues'], function ($issue) use ($ignore_projects) {
return !in_array($issue['fields']['project']['name'], $ignore_projects);
});
$issues['issues'] = $filtered_issues;
if (count($issues['issues']) == 0) {
$user_warning[] = $user;
}
foreach ($issues['issues'] as $issue) {
$projectName = $issue['fields']['project']['name'];
if (!in_array($projectName, $ignore_projects)) {
$username = $issue['fields']['assignee']['displayName'];
$issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
if (!isset($groupedIssues[$projectName])) {
$groupedIssues[$projectName] = [];
$groupedIssues[$projectName]['project'] = $issue['fields']['project'];
@ -291,7 +280,6 @@ class JiraService
$users_data[$user['displayName']]['total_est'] = $users_data[$user['displayName']]['total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
}
}
}
@ -326,18 +314,4 @@ class JiraService
$response = $this->client->get('/rest/agile/1.0/sprint/' . $id . '/issue');
return json_decode($response->getBody()->getContents(), true);
}
public function getWeeklyReport()
{
$body = [
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'worklog'],
'jql' => 'worklogDate >= startOfWeek() AND worklogDate < startOfWeek(1) order by created DESC',
'maxResults' => 1000
];
$response = $this->client->post('/rest/api/3/search', [
'body' => json_encode($body)
]);
return json_decode($response->getBody()->getContents(), true);
}
}

View File

@ -2,16 +2,11 @@
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": [
"laravel",
"framework"
],
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-debugbar": "^3.9",
"barryvdh/laravel-dompdf": "^2.0",
"doctrine/dbal": "^3.10",
"drnxloc/laravel-simple-html-dom": "^1.9",
"guzzlehttp/guzzle": "^7.8",
"laravel/framework": "^10.10",

3079
BACKEND/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -59,8 +59,6 @@ return [
'asset_url' => env('ASSET_URL'),
'client_url' => env('ADMIN_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone

View File

@ -15,7 +15,7 @@ return [
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie', 'storage/*'],
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],

View File

@ -101,7 +101,7 @@ return [
|
*/
'ttl' => env('JWT_TTL', 60*24*365),
'ttl' => env('JWT_TTL', 60*24),
/*
|--------------------------------------------------------------------------

View File

@ -1,30 +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('project_reviews', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('role');
$table->longText('note');
$table->foreignId('user_id')->constrained('users')->onDelete('cascade'); // Khóa ngoại tới bảng users
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_reviews');
}
};

View File

@ -1,32 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('uri');
$table->enum('type', ['file', 'link'])->default("file");
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('documents');
}
};

View File

@ -1,22 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RenameLdDayToLdDayTotalInLeaveDaysTable extends Migration
{
public function up()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->renameColumn('ld_day', 'ld_day_total');
});
}
public function down()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->renameColumn('ld_day_total', 'ld_day');
});
}
}

View File

@ -1,22 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddLdSpecialLeaveDayToLeaveDaysTable extends Migration
{
public function up()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->float('ld_special_leave_day')->default(0); // Adding the new field
});
}
public function down()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->dropColumn('ld_special_leave_day'); // Dropping the field if needed
});
}
}

View File

@ -1,22 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RenameLdDateAdditionalToLdAdditionalDayInLeaveDaysTable extends Migration
{
public function up()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->renameColumn('ld_date_additional', 'ld_additional_day');
});
}
public function down()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->renameColumn('ld_date_additional', 'ld_additional_day');
});
}
}

View File

@ -1,41 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('categories')->insert([
[
'c_code' => 'LEAVE_WITHOUT_PAY',
'c_name' => 'Nghỉ không hưởng lương',
'c_type' => 'REASON',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
DB::table('categories')
->where('c_name', 'Nghỉ phép')
->update(['c_name' => 'Nghỉ phép năm']);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('categories')
->where('c_name', 'Nghỉ phép năm')
->update(['c_name' => 'Nghỉ phép']);
}
};

View File

@ -1,32 +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('files', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('url');
$table->string('type');
$table->text('description')->nullable();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('files');
}
};

View File

@ -1,52 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class UpdateLeaveCategories extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Xóa item với type REASON và code LEAVE_WITHOUT_PAY
DB::table('categories')
->where('c_type', 'REASON')
->where('c_code', 'LEAVE_WITHOUT_PAY')
->delete();
// Cập nhật tên "Nghỉ phép năm" thành "Nghỉ phép"
DB::table('categories')
->where('c_name', 'Nghỉ phép năm')
->update(['c_name' => 'Nghỉ phép']);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Khôi phục item đã xóa
DB::table('categories')->insert([
'c_code' => 'LEAVE_WITHOUT_PAY',
'c_name' => 'Không phép',
'c_type' => 'REASON',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
// Khôi phục tên cũ
DB::table('categories')
->where('c_name', 'Nghỉ phép')
->update(['c_name' => 'Nghỉ phép năm']);
}
}

View File

@ -1,40 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddLimitLeaveMonthCategory extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('categories')->insert([
'c_code' => 'LIMIT',
'c_name' => 'Giới hạn số ngày nghỉ có phép/tháng',
'c_type' => 'LIMIT_LEAVE_MONTH',
'c_value' => '3',
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('categories')
->where('c_code', 'LIMIT')
->where('c_type', 'LIMIT_LEAVE_MONTH')
->delete();
}
}

View File

@ -1,40 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddSaturdayWorkScheduleCategory extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('categories')->insert([
'c_code' => '10-05-2025',
'c_name' => 'Ngày bắt đầu làm việc thứ 7 trong năm',
'c_type' => 'SATURDAY_WORK_SCHEDULE',
'c_value' => '2025',
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('categories')
->where('c_code', '10-05-2025')
->where('c_type', 'SATURDAY_WORK_SCHEDULE')
->delete();
}
}

View File

@ -1,40 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddDayWorkSpecialCategory extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('categories')->insert([
'c_code' => '17-05-2025',
'c_name' => 'Ngày làm việc đặc biệt',
'c_type' => 'DAY_WORK_SPECIAL',
'c_value' => '2025',
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('categories')
->where('c_code', '17-05-2025')
->where('c_type', 'DAY_WORK_SPECIAL')
->delete();
}
}

View File

@ -1,60 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddLeaveCategories extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('categories')->insert([
[
'c_code' => 'LEAVE_WITHOUT_PAY',
'c_name' => 'Không phép',
'c_type' => 'REASON_NOTES',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'c_code' => 'WFH',
'c_name' => 'Work From Home',
'c_type' => 'REASON_NOTES',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'c_code' => 'ONLEAVE',
'c_name' => 'Nghỉ phép',
'c_type' => 'REASON_NOTES',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('categories')
->whereIn('c_code', ['LEAVE_WITHOUT_PAY', 'WFH', 'ONLEAVE'])
->where('c_type', 'REASON_NOTES')
->delete();
}
}

View File

@ -1,26 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->foreignId('ticket_id')
->nullable()
->constrained('tickets')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->dropForeign(['ticket_id']);
$table->dropColumn('ticket_id');
});
}
};

View File

@ -1,31 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_permanent')->default(true);
$table->date('permanent_date');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_permanent');
$table->dropColumn('permanent_date');
});
}
};

View File

@ -1,45 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('categories')->insert([
[
'c_code' => 'PERMANENT',
'c_name' => 'Phép cộng nhân viên chính thức',
'c_type' => 'PERMANENT_ONLEAVE',
'c_value' => 1,
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'c_code' => 'TEMPORARY_ONLEAVE',
'c_name' => 'Nghỉ dự kiến',
'c_type' => 'REASON_NOTES',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('categories')->where('c_code', 'PERMANENT')->where('c_type', 'PERMANENT_ONLEAVE')->delete();
DB::table('categories')->where('c_code', 'TEMPORARY_ONLEAVE')->where('c_type', 'REASON_NOTES')->delete();
}
};

View File

@ -1,29 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_separated')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_separated');
});
}
};

View File

@ -1,39 +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('electricity_bills', function (Blueprint $table) {
$table->id();
$table->date('billing_date')->comment('Ngày lập hóa đơn');
$table->decimal('previous_reading', 12, 2)->comment('Số điện kỳ trước');
$table->decimal('current_reading', 12, 2)->comment('Số điện kỳ này');
$table->decimal('unit_price', 12, 2)->comment('Đơn giá điện');
$table->decimal('total_amount', 12, 2)->comment('Tổng tiền điện');
$table->string('notes')->nullable()->comment('Ghi chú');
$table->string('file_path')->nullable()->comment('Đường dẫn file PDF');
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->timestamps();
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
$table->foreign('updated_by')->references('id')->on('users')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('electricity_bills');
}
};

View File

@ -1,23 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('electricity_bills', function (Blueprint $table) {
$table->string('attachment_path')->nullable()->after('file_path')
->comment('User-uploaded attachment (PDF or image)');
});
}
public function down(): void
{
Schema::table('electricity_bills', function (Blueprint $table) {
$table->dropColumn('attachment_path');
});
}
};

View File

@ -1,50 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('equipments', function (Blueprint $table) {
$table->id();
$table->string('code')->unique()->comment('Mã thiết bị (dùng để scan barcode)');
$table->string('name')->comment('Tên thiết bị');
$table->text('note')->nullable()->comment('Ghi chú');
$table->unsignedBigInteger('current_user_id')->nullable()->comment('Người đang sử dụng, null = đang ở kho');
$table->timestamp('assigned_at')->nullable()->comment('Thời điểm giao thiết bị hiện tại');
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->timestamps();
$table->foreign('current_user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
$table->foreign('updated_by')->references('id')->on('users')->onDelete('set null');
});
Schema::create('equipment_assignments', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('equipment_id');
$table->unsignedBigInteger('user_id')->nullable()->comment('null = trả về kho');
$table->timestamp('assigned_at')->comment('Bắt đầu sử dụng');
$table->timestamp('returned_at')->nullable()->comment('Trả thiết bị');
$table->text('note')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
$table->foreign('equipment_id')->references('id')->on('equipments')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
$table->index(['equipment_id', 'returned_at']);
});
}
public function down(): void
{
Schema::dropIfExists('equipment_assignments');
Schema::dropIfExists('equipments');
}
};

View File

@ -1,23 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('equipments', function (Blueprint $table) {
$table->string('image_path')->nullable()->after('note')
->comment('Ảnh thiết bị (tùy chọn)');
});
}
public function down(): void
{
Schema::table('equipments', function (Blueprint $table) {
$table->dropColumn('image_path');
});
}
};

View File

@ -18,10 +18,5 @@ class DatabaseSeeder extends Seeder
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
$this->call([
UserSeeder::class,
FileSeeder::class,
]);
}
}

View File

@ -1,153 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
font-family: "Arial", sans-serif;
}
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f7fafc;
}
.content {
background-color: #fffefe;
padding: 2rem 0;
border-radius: 0.5rem;
width: 100vw;
}
.my-0 {
margin-bottom: 0;
margin-top: 0;
}
</style>
<title>{{ $data['subject'] }}</title>
</head>
<body>
<body style="
font-family: Arial, Helvetica, sans-serif;
background-color: #edf2f7;
">
<table style="margin: 0 auto">
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
<img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
alt="Logo" border="0" width="100"
style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
background-color: #ffffff;
width: 768px;
padding: 24px;
">
<tr>
<td>
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
Hi {{ $data['name'] }},
</h3>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
{{ $data['message1'] }}
</p>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
{{ $data['message2'] }}
</p>
</td>
</tr>
<tr>
<td>
<p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
<a href="{{ $data['url'] }}"
style="
color: #fff;
border-radius: 10px;
background-color: rgba(68,115,196);
background-image: linear-gradient(to top left,rgba(0,0,0,.2),rgba(0,0,0,.2) 30%,rgba(0,0,0,0));
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Check now</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td>
<h5
style="
color: #222222;
text-align: center;
padding: 10px 36px;
margin: 0;
">
<p>© 2024 APAC Tech.</p>
</h5>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</body>
</html>

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html lang="en">
@ -36,6 +37,8 @@
<title>{{ $data['subject'] }}</title>
</head>
<body>
<body style="
font-family: Arial, Helvetica, sans-serif;
background-color: #edf2f7;
@ -80,7 +83,7 @@
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a <a href="{{ config('app.client_url') . $data['link'] }}"> request ticket</a>, the specific content is as follows:
Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a request ticket, the specific content is as follows:
</p>
</td>
</tr>
@ -95,80 +98,29 @@
</div>
</td>
</tr>
<tr>
<td>
<p style="text-align: center">
You can quick
<span style="font-weight: bold">Confirm</span> or
<span style="font-weight: bold">Refuse</span> here:
<p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
<a href='{{ config('app.url') . $data['link'] }}'
style="
color: #fff;
border-radius: 10px;
background-color: rgba(68,115,196);
background-image: linear-gradient(to top left,rgba(0,0,0,.2),rgba(0,0,0,.2) 30%,rgba(0,0,0,0));
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Check now</a>
</p>
<div
style="
display: flex;
justify-content: center;
gap: 10px;
margin-top: 10px;
">
<a
href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'confirm', 'admin_email' => $data['admin_email']]) }}"
style="
color: #fff;
border-radius: 10px;
background-color: #12b886;
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Confirm</a>
<a
href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'refuse', 'admin_email' => $data['admin_email']]) }}"
style="
color: #fff;
border-radius: 10px;
background-color: #f03e3e;
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Refuse</a>
</div>
</td>
</tr>
<tr>
<td style="color: #222222;">
<div style="margin-top: 3rem">
<p><span style="font-weight: bold">Note</span>: If you are redirected to a <span style="font-weight: bold">404 page</span>, it means:</p>
<p>1. The ticket has already been approved by another admin.</p>
<p>2. The ticket has been deleted.</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
@ -198,5 +150,6 @@
</tr>
</table>
</body>
</body>
</html>

View File

@ -1,129 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
font-family: "Arial", sans-serif;
}
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f7fafc;
}
.content {
background-color: #fffefe;
padding: 2rem 0;
border-radius: 0.5rem;
width: 100vw;
}
.my-0 {
margin-bottom: 0;
margin-top: 0;
}
</style>
<title>[SYSTEM] - Task are forgotten</title>
</head>
<body>
<body style="
font-family: Arial, Helvetica, sans-serif;
background-color: #edf2f7;
">
<table style="margin: 0 auto">
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
<img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
alt="Logo" border="0" width="100"
style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
background-color: #ffffff;
width: 768px;
padding: 24px;
">
<tr>
<td>
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
Hi {{ $data[0]['fields']['assignee']['displayName'] }},
</h3>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
You currently have some tasks that have not been updated for a long time. Please check and update them!
</p>
</td>
</tr>
<tr>
<td>
@foreach($data as $iss)
<a href="https://apactechvn.atlassian.net/browse/{{$iss['key']}}">{{$iss['fields']['summary']}} - <b>haven't updated in {{$iss['daysRemaining']}} days</b></a><br>
@endforeach
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td>
<h5
style="
color: #222222;
text-align: center;
padding: 10px 36px;
margin: 0;
">
<p>© 2024 APAC Tech.</p>
</h5>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</body>
</html>

View File

@ -1,72 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Thông báo tải lên file mới</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #1a73e8;
color: white;
padding: 20px;
text-align: center;
}
.content {
padding: 20px;
background-color: #f9f9f9;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #1a73e8;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
}
.footer {
text-align: center;
padding: 20px;
background-color: #f5f5f5;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Thông báo tải lên file mới</h1>
</div>
<div class="content">
<p>Xin chào {{ $user->name }},</p>
<p>{{ $description }} hệ thống APAC Tech.</p>
<p>Note: {{ $note }}</p>
<p>Vui lòng kiểm tra ngay thông tin bằng cách nhấn nút bên dưới:</p>
<div style="text-align: center;">
<a href="{{ $url }}" class="button">Kiểm tra ngay</a>
</div>
<p>Trân trọng,<br>Đội ngũ APAC Tech</p>
</div>
<div class="footer">
<p>Email này được gửi tự động từ hệ thống APAC Tech. Vui lòng không trả lời email này.</p>
</div>
</div>
</body>
</html>

View File

@ -1,13 +0,0 @@
<?php
use Carbon\Carbon;
require_once __DIR__ . "/../vendor/autoload.php";
$app = include_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
// Cập nhật lại data cho tới tháng hiện tại
$currentMonth = Carbon::now()->month;
$tmpClass = $app->make('Modules\Admin\app\Http\Controllers\TicketController');
$tmpClass->updateOldData($currentMonth, 2025); // Params: month, year

View File

@ -1,2 +0,0 @@
dataset
test

View File

@ -1,28 +0,0 @@
import os
import shutil
def organize_files_by_username(folder_path, dest_folder_path):
# Lấy danh sách các tệp trong thư mục
files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
for file in files:
# Kiểm tra định dạng tên tệp: <username>_checkin_date.png
if "_" in file and file.endswith(".png"):
username = file.split("_")[0] # Lấy phần username từ tên tệp
# Tạo đường dẫn thư mục con
subfolder_path = os.path.join(folder_path, username)
# Tạo thư mục con nếu chưa tồn tại
if not os.path.exists(subfolder_path):
os.makedirs(subfolder_path)
# Di chuyển tệp vào thư mục con
shutil.move(os.path.join(folder_path, file), os.path.join(subfolder_path, file))
print("Hoàn thành sắp xếp tệp theo username.")
# Đường dẫn tới thư mục chứa các tệp
folder_path = "/home/joseph/screenshot"
dest_folder_path = "/home/joseph/DetectFace/dataset"
organize_files_by_username(folder_path, dest_folder_path)

View File

@ -1,77 +0,0 @@
import cv2
import face_recognition
import os
import numpy as np
import pickle
datasetPath = "dataset"
images = []
classNames = []
lisFileTrain = os.listdir(datasetPath)
for file in lisFileTrain:
currentImg = cv2.imread(f"{datasetPath}/{file}")
images.append(currentImg)
classNames.append(os.path.splitext(file)[0].split('_')[0])
print(len(images))
def encodeImgs(images, save_path="encodings.pkl"):
if os.path.exists(save_path):
print(f"Loading encodings from {save_path}...")
with open(save_path, "rb") as f:
return pickle.load(f)
encodeList = []
for i, img in enumerate(images):
print(i+1)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
encode = face_recognition.face_encodings(img)
if encode: # Check if encodings list is not empty
encodeList.append(encode[0])
else:
print("No face detected in an image. Skipping...")
os.remove(f"{datasetPath}/{lisFileTrain[i]}")
# Lưu encodeList vào file
print(f"Saving encodings to {save_path}...")
with open(save_path, "wb") as f:
pickle.dump(encodeList, f)
return encodeList
encodeListKnow = encodeImgs(images)
print("Load data success")
print(len(encodeListKnow))
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
frameS = cv2.resize(frame, (0,0), None, fx=1, fy=1)
frameS = cv2.cvtColor(frameS, cv2.COLOR_BGR2RGB)
faceCurFrame = face_recognition.face_locations(frameS)
encodeCurFrame = face_recognition.face_encodings(frameS)
for encodeFace, faceLoc in zip(encodeCurFrame, faceCurFrame):
matches = face_recognition.compare_faces(encodeListKnow, encodeFace)
faceDis = face_recognition.face_distance(encodeListKnow, encodeFace)
print(faceDis)
matchIndex = np.argmin(faceDis)
if faceDis[matchIndex] < 0.3:
name = classNames[matchIndex].upper()
else:
name = "Unknow"
y1, x2, y2, x1 = faceLoc
y1, x2, y2, x1 = y1, x2, y2, x1
cv2.rectangle(frame, (x1,y1), (x2,y2), (0,255,0), 2)
cv2.putText(frame, name + f"({(1 - round(faceDis[matchIndex], 2))*100}%)", (x2, y2), cv2.FONT_HERSHEY_COMPLEX, 1, (255,255,255), 2)
cv2.imshow('Face decting', frame)
if cv2.waitKey(1) == ord("q"):
break
cap.release()
cv2.destroyAllWindows()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

View File

@ -1,3 +1 @@
VITE_BACKEND_URL=http://localhost:8000/
VITE_URL_DRAWIO="https://viewer.diagrams.net/?tags=%7B%7D&lightbox=1&highlight=0000ff&edit=_blank&layers=1&nav=1&title=Test%20Draw.drawio&dark=auto#Uhttps%3A%2F%2Fdrive.google.com%2Fuc%3Fid%3D1LmB9wCac9DonQPFU-53g1nhI9SfvWuWK%26export%3Ddownload"

View File

@ -1,425 +0,0 @@
#!/usr/bin/env bash
{ # this ensures the entire script is downloaded #
nvm_has() {
type "$1" > /dev/null 2>&1
}
nvm_default_install_dir() {
[ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm"
}
nvm_install_dir() {
if [ -n "$NVM_DIR" ]; then
printf %s "${NVM_DIR}"
else
nvm_default_install_dir
fi
}
nvm_latest_version() {
echo "v0.35.0"
}
nvm_profile_is_bash_or_zsh() {
local TEST_PROFILE
TEST_PROFILE="${1-}"
case "${TEST_PROFILE-}" in
*"/.bashrc" | *"/.bash_profile" | *"/.zshrc")
return
;;
*)
return 1
;;
esac
}
#
# Outputs the location to NVM depending on:
# * The availability of $NVM_SOURCE
# * The method used ("script" or "git" in the script, defaults to "git")
# NVM_SOURCE always takes precedence unless the method is "script-nvm-exec"
#
nvm_source() {
local NVM_METHOD
NVM_METHOD="$1"
local NVM_SOURCE_URL
NVM_SOURCE_URL="$NVM_SOURCE"
if [ "_$NVM_METHOD" = "_script-nvm-exec" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/nvm-exec"
elif [ "_$NVM_METHOD" = "_script-nvm-bash-completion" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/bash_completion"
elif [ -z "$NVM_SOURCE_URL" ]; then
if [ "_$NVM_METHOD" = "_script" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/nvm.sh"
elif [ "_$NVM_METHOD" = "_git" ] || [ -z "$NVM_METHOD" ]; then
NVM_SOURCE_URL="https://github.com/nvm-sh/nvm.git"
else
echo >&2 "Unexpected value \"$NVM_METHOD\" for \$NVM_METHOD"
return 1
fi
fi
echo "$NVM_SOURCE_URL"
}
#
# Node.js version to install
#
nvm_node_version() {
echo "$NODE_VERSION"
}
nvm_download() {
if nvm_has "curl"; then
curl --compressed -q "$@"
elif nvm_has "wget"; then
# Emulate curl with wget
ARGS=$(echo "$*" | command sed -e 's/--progress-bar /--progress=bar /' \
-e 's/-L //' \
-e 's/--compressed //' \
-e 's/-I /--server-response /' \
-e 's/-s /-q /' \
-e 's/-o /-O /' \
-e 's/-C - /-c /')
# shellcheck disable=SC2086
eval wget $ARGS
fi
}
install_nvm_from_git() {
local INSTALL_DIR
INSTALL_DIR="$(nvm_install_dir)"
if [ -d "$INSTALL_DIR/.git" ]; then
echo "=> nvm is already installed in $INSTALL_DIR, trying to update using git"
command printf '\r=> '
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch origin tag "$(nvm_latest_version)" --depth=1 2> /dev/null || {
echo >&2 "Failed to update nvm, run 'git fetch' in $INSTALL_DIR yourself."
exit 1
}
else
# Cloning to $INSTALL_DIR
echo "=> Downloading nvm from git to '$INSTALL_DIR'"
command printf '\r=> '
mkdir -p "${INSTALL_DIR}"
if [ "$(ls -A "${INSTALL_DIR}")" ]; then
command git init "${INSTALL_DIR}" || {
echo >&2 'Failed to initialize nvm repo. Please report this!'
exit 2
}
command git --git-dir="${INSTALL_DIR}/.git" remote add origin "$(nvm_source)" 2> /dev/null \
|| command git --git-dir="${INSTALL_DIR}/.git" remote set-url origin "$(nvm_source)" || {
echo >&2 'Failed to add remote "origin" (or set the URL). Please report this!'
exit 2
}
command git --git-dir="${INSTALL_DIR}/.git" fetch origin tag "$(nvm_latest_version)" --depth=1 || {
echo >&2 'Failed to fetch origin with tags. Please report this!'
exit 2
}
else
command git -c advice.detachedHead=false clone "$(nvm_source)" -b "$(nvm_latest_version)" --depth=1 "${INSTALL_DIR}" || {
echo >&2 'Failed to clone nvm repo. Please report this!'
exit 2
}
fi
fi
command git -c advice.detachedHead=false --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" checkout -f --quiet "$(nvm_latest_version)"
if [ -n "$(command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" show-ref refs/heads/master)" ]; then
if command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet 2>/dev/null; then
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet -D master >/dev/null 2>&1
else
echo >&2 "Your version of git is out of date. Please update it!"
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch -D master >/dev/null 2>&1
fi
fi
echo "=> Compressing and cleaning up git repository"
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" reflog expire --expire=now --all; then
echo >&2 "Your version of git is out of date. Please update it!"
fi
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" gc --auto --aggressive --prune=now ; then
echo >&2 "Your version of git is out of date. Please update it!"
fi
return
}
#
# Automatically install Node.js
#
nvm_install_node() {
local NODE_VERSION_LOCAL
NODE_VERSION_LOCAL="$(nvm_node_version)"
if [ -z "$NODE_VERSION_LOCAL" ]; then
return 0
fi
echo "=> Installing Node.js version $NODE_VERSION_LOCAL"
nvm install "$NODE_VERSION_LOCAL"
local CURRENT_NVM_NODE
CURRENT_NVM_NODE="$(nvm_version current)"
if [ "$(nvm_version "$NODE_VERSION_LOCAL")" == "$CURRENT_NVM_NODE" ]; then
echo "=> Node.js version $NODE_VERSION_LOCAL has been successfully installed"
else
echo >&2 "Failed to install Node.js $NODE_VERSION_LOCAL"
fi
}
install_nvm_as_script() {
local INSTALL_DIR
INSTALL_DIR="$(nvm_install_dir)"
local NVM_SOURCE_LOCAL
NVM_SOURCE_LOCAL="$(nvm_source script)"
local NVM_EXEC_SOURCE
NVM_EXEC_SOURCE="$(nvm_source script-nvm-exec)"
local NVM_BASH_COMPLETION_SOURCE
NVM_BASH_COMPLETION_SOURCE="$(nvm_source script-nvm-bash-completion)"
# Downloading to $INSTALL_DIR
mkdir -p "$INSTALL_DIR"
if [ -f "$INSTALL_DIR/nvm.sh" ]; then
echo "=> nvm is already installed in $INSTALL_DIR, trying to update the script"
else
echo "=> Downloading nvm as script to '$INSTALL_DIR'"
fi
nvm_download -s "$NVM_SOURCE_LOCAL" -o "$INSTALL_DIR/nvm.sh" || {
echo >&2 "Failed to download '$NVM_SOURCE_LOCAL'"
return 1
} &
nvm_download -s "$NVM_EXEC_SOURCE" -o "$INSTALL_DIR/nvm-exec" || {
echo >&2 "Failed to download '$NVM_EXEC_SOURCE'"
return 2
} &
nvm_download -s "$NVM_BASH_COMPLETION_SOURCE" -o "$INSTALL_DIR/bash_completion" || {
echo >&2 "Failed to download '$NVM_BASH_COMPLETION_SOURCE'"
return 2
} &
for job in $(jobs -p | command sort)
do
wait "$job" || return $?
done
chmod a+x "$INSTALL_DIR/nvm-exec" || {
echo >&2 "Failed to mark '$INSTALL_DIR/nvm-exec' as executable"
return 3
}
}
nvm_try_profile() {
if [ -z "${1-}" ] || [ ! -f "${1}" ]; then
return 1
fi
echo "${1}"
}
#
# Detect profile file if not specified as environment variable
# (eg: PROFILE=~/.myprofile)
# The echo'ed path is guaranteed to be an existing file
# Otherwise, an empty string is returned
#
nvm_detect_profile() {
if [ "${PROFILE-}" = '/dev/null' ]; then
# the user has specifically requested NOT to have nvm touch their profile
return
fi
if [ -n "${PROFILE}" ] && [ -f "${PROFILE}" ]; then
echo "${PROFILE}"
return
fi
local DETECTED_PROFILE
DETECTED_PROFILE=''
if [ -n "${BASH_VERSION-}" ]; then
if [ -f "$HOME/.bashrc" ]; then
DETECTED_PROFILE="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
DETECTED_PROFILE="$HOME/.bash_profile"
fi
elif [ -n "${ZSH_VERSION-}" ]; then
DETECTED_PROFILE="$HOME/.zshrc"
fi
if [ -z "$DETECTED_PROFILE" ]; then
for EACH_PROFILE in ".profile" ".bashrc" ".bash_profile" ".zshrc"
do
if DETECTED_PROFILE="$(nvm_try_profile "${HOME}/${EACH_PROFILE}")"; then
break
fi
done
fi
if [ -n "$DETECTED_PROFILE" ]; then
echo "$DETECTED_PROFILE"
fi
}
#
# Check whether the user has any globally-installed npm modules in their system
# Node, and warn them if so.
#
nvm_check_global_modules() {
command -v npm >/dev/null 2>&1 || return 0
local NPM_VERSION
NPM_VERSION="$(npm --version)"
NPM_VERSION="${NPM_VERSION:--1}"
[ "${NPM_VERSION%%[!-0-9]*}" -gt 0 ] || return 0
local NPM_GLOBAL_MODULES
NPM_GLOBAL_MODULES="$(
npm list -g --depth=0 |
command sed -e '/ npm@/d' -e '/ (empty)$/d'
)"
local MODULE_COUNT
MODULE_COUNT="$(
command printf %s\\n "$NPM_GLOBAL_MODULES" |
command sed -ne '1!p' | # Remove the first line
wc -l | command tr -d ' ' # Count entries
)"
if [ "${MODULE_COUNT}" != '0' ]; then
# shellcheck disable=SC2016
echo '=> You currently have modules installed globally with `npm`. These will no'
# shellcheck disable=SC2016
echo '=> longer be linked to the active version of Node when you install a new node'
# shellcheck disable=SC2016
echo '=> with `nvm`; and they may (depending on how you construct your `$PATH`)'
# shellcheck disable=SC2016
echo '=> override the binaries of modules installed with `nvm`:'
echo
command printf %s\\n "$NPM_GLOBAL_MODULES"
echo '=> If you wish to uninstall them at a later point (or re-install them under your'
# shellcheck disable=SC2016
echo '=> `nvm` Nodes), you can remove them from the system Node as follows:'
echo
echo ' $ nvm use system'
echo ' $ npm uninstall -g a_module'
echo
fi
}
nvm_do_install() {
if [ -n "${NVM_DIR-}" ] && ! [ -d "${NVM_DIR}" ]; then
if [ -e "${NVM_DIR}" ]; then
echo >&2 "File \"${NVM_DIR}\" has the same name as installation directory."
exit 1
fi
if [ "${NVM_DIR}" = "$(nvm_default_install_dir)" ]; then
mkdir "${NVM_DIR}"
else
echo >&2 "You have \$NVM_DIR set to \"${NVM_DIR}\", but that directory does not exist. Check your profile files and environment."
exit 1
fi
fi
if [ -z "${METHOD}" ]; then
# Autodetect install method
if nvm_has git; then
install_nvm_from_git
elif nvm_has nvm_download; then
install_nvm_as_script
else
echo >&2 'You need git, curl, or wget to install nvm'
exit 1
fi
elif [ "${METHOD}" = 'git' ]; then
if ! nvm_has git; then
echo >&2 "You need git to install nvm"
exit 1
fi
install_nvm_from_git
elif [ "${METHOD}" = 'script' ]; then
if ! nvm_has nvm_download; then
echo >&2 "You need curl or wget to install nvm"
exit 1
fi
install_nvm_as_script
else
echo >&2 "The environment variable \$METHOD is set to \"${METHOD}\", which is not recognized as a valid installation method."
exit 1
fi
echo
local NVM_PROFILE
NVM_PROFILE="$(nvm_detect_profile)"
local PROFILE_INSTALL_DIR
PROFILE_INSTALL_DIR="$(nvm_install_dir | command sed "s:^$HOME:\$HOME:")"
SOURCE_STR="\\nexport NVM_DIR=\"${PROFILE_INSTALL_DIR}\"\\n[ -s \"\$NVM_DIR/nvm.sh\" ] && \\. \"\$NVM_DIR/nvm.sh\" # This loads nvm\\n"
# shellcheck disable=SC2016
COMPLETION_STR='[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion\n'
BASH_OR_ZSH=false
if [ -z "${NVM_PROFILE-}" ] ; then
local TRIED_PROFILE
if [ -n "${PROFILE}" ]; then
TRIED_PROFILE="${NVM_PROFILE} (as defined in \$PROFILE), "
fi
echo "=> Profile not found. Tried ${TRIED_PROFILE-}~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile."
echo "=> Create one of them and run this script again"
echo " OR"
echo "=> Append the following lines to the correct file yourself:"
command printf "${SOURCE_STR}"
echo
else
if nvm_profile_is_bash_or_zsh "${NVM_PROFILE-}"; then
BASH_OR_ZSH=true
fi
if ! command grep -qc '/nvm.sh' "$NVM_PROFILE"; then
echo "=> Appending nvm source string to $NVM_PROFILE"
command printf "${SOURCE_STR}" >> "$NVM_PROFILE"
else
echo "=> nvm source string already in ${NVM_PROFILE}"
fi
# shellcheck disable=SC2016
if ${BASH_OR_ZSH} && ! command grep -qc '$NVM_DIR/bash_completion' "$NVM_PROFILE"; then
echo "=> Appending bash_completion source string to $NVM_PROFILE"
command printf "$COMPLETION_STR" >> "$NVM_PROFILE"
else
echo "=> bash_completion source string already in ${NVM_PROFILE}"
fi
fi
if ${BASH_OR_ZSH} && [ -z "${NVM_PROFILE-}" ] ; then
echo "=> Please also append the following lines to the if you are using bash/zsh shell:"
command printf "${COMPLETION_STR}"
fi
# Source nvm
# shellcheck source=/dev/null
\. "$(nvm_install_dir)/nvm.sh"
nvm_check_global_modules
nvm_install_node
nvm_reset
echo "=> Close and reopen your terminal to start using nvm or run the following to use it now:"
command printf "${SOURCE_STR}"
if ${BASH_OR_ZSH} ; then
command printf "${COMPLETION_STR}"
fi
}
#
# Unsets the various functions defined
# during the execution of the install script
#
nvm_reset() {
unset -f nvm_has nvm_install_dir nvm_latest_version nvm_profile_is_bash_or_zsh \
nvm_source nvm_node_version nvm_download install_nvm_from_git nvm_install_node \
install_nvm_as_script nvm_try_profile nvm_detect_profile nvm_check_global_modules \
nvm_do_install nvm_reset nvm_default_install_dir
}
[ "_$NVM_ENV" = "_testing" ] || nvm_do_install
} # this ensures the entire script is downloaded #

File diff suppressed because it is too large Load Diff

View File

@ -11,20 +11,19 @@
"test": "npm run jest"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.2",
"@mantine/charts": "^7.16.3",
"@mantine/core": "^7.13.2",
"@mantine/dates": "^7.13.2",
"@mantine/form": "^7.13.2",
"@mantine/hooks": "^7.13.2",
"@mantine/notifications": "^7.13.2",
"@mantine/vanilla-extract": "^7.13.2",
"@reduxjs/toolkit": "^2.3.0",
"@tabler/icons-react": "^3.19.0",
"@uiw/react-codemirror": "^4.23.5",
"@vanilla-extract/css": "^1.16.0",
"@zxing/browser": "^0.2.0",
"@zxing/library": "^0.23.0",
"@codemirror/lang-javascript": "^6.2.1",
"@mantine/charts": "^7.4.1",
"@mantine/core": "7.2.1",
"@mantine/dates": "^7.3.2",
"@mantine/ds": "^7.2.2",
"@mantine/form": "^7.2.2",
"@mantine/hooks": "7.2.1",
"@mantine/notifications": "^7.2.2",
"@mantine/vanilla-extract": "7.2.1",
"@reduxjs/toolkit": "^1.9.7",
"@tabler/icons-react": "^2.40.0",
"@uiw/react-codemirror": "^4.21.21",
"@vanilla-extract/css": "^1.13.0",
"axios": "^1.6.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.10",
@ -34,13 +33,11 @@
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-doc-viewer": "^0.1.14",
"react-dom": "^18.2.0",
"react-file-viewer": "^1.2.1",
"react-redux": "^8.1.3",
"react-router-dom": "^6.19.0",
"reactstrap": "^9.2.2",
"recharts": "^2.15.1",
"recharts": "^2.11.0",
"tailwind-merge": "^2.0.0",
"tests": "^0.4.2"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

@ -11,7 +11,6 @@ import { BrowserRouter, useRoutes } from 'react-router-dom'
import mainRoutes from '@/routes/main'
import classes from '@/App.module.css'
import '@mantine/dates/styles.css'
import '@mantine/charts/styles.css'
export const App = () => {
const element = useRoutes(mainRoutes)

View File

@ -7,7 +7,6 @@ export const getListTracking = API_URL + 'v1/admin/tracking'
export const addTracking = API_URL + 'v1/admin/tracking/create'
export const updateTracking = API_URL + 'v1/admin/tracking/update'
export const deleteTracking = API_URL + 'v1/admin/tracking/delete'
export const getListTrackingSummary = API_URL + 'v1/admin/tracking/summary'
// Worklogs
export const fetchAllIssues = API_URL + 'v1/admin/jira/fetch-issues'
@ -18,12 +17,9 @@ export const getAllIssuesByProject =
export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs'
export const getAllUserDoing = API_URL + 'v1/admin/jira/allocation'
export const getDetailIssByKey = API_URL + 'v1/admin/jira/issue/detail'
export const getPJParticipating =
API_URL + 'v1/admin/jira/project-participating'
//Timekeeping
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
export const exportTimekeeping = API_URL + 'v1/admin/timekeeping/export'
export const updateMultipleUserWorkingTime =
API_URL + 'v1/admin/timekeeping/addMutilple'
export const updateNote = API_URL + 'v1/admin/timekeeping/addNote'
@ -35,24 +31,17 @@ export const updateWorkingDays =
//Category
export const getListMaster = API_URL + 'v1/admin/category/get-list-master'
export const getWorkDay = API_URL + 'v1/admin/category/work-days'
export const updateWorkDay = API_URL + 'v1/admin/category/update-work-days'
//LeaveManagement
export const getLeaveManagement = API_URL + 'v1/admin/leave-management'
export const updateNoteLeave =
API_URL + 'v1/admin/leave-management/saveNoteLeave'
export const updateNoteStatus =
API_URL + 'v1/admin/leave-management/updateNoteStatus'
export const exportLeaveManagement =
API_URL + 'v1/admin/leave-management/export'
//Tickets
export const getTickets = API_URL + 'v1/admin/ticket/all'
export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId'
export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
export const addTicket = API_URL + 'v1/admin/ticket/create'
export const updateTicket = API_URL + 'v1/admin/ticket/update'
export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket'
//Users
@ -88,82 +77,16 @@ export const updateProfilesData =
API_URL + 'v1/admin/criterias/profiles-data/update'
export const listUserTechnical = API_URL + 'v1/admin/technical/get-tech-of-user'
export const updateUserTechnical =
API_URL + 'v1/admin/technical/technicals-user/update'
export const updateUserTechnical = API_URL + 'v1/admin/technical/technicals-user/update'
export const getAllUser = API_URL + 'v1/admin/technical/get-all-user'
export const getAllTechByUserId =
API_URL + 'v1/admin/technical/get-tech-by-user-id'
export const evaluation = API_URL + 'v1/admin/evaluation/report'
export const evaluationReportAllUsers =
API_URL + 'v1/admin/evaluation/report-all-users'
export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review'
export const projectReview = API_URL + 'v1/admin/evaluation/project-review'
export const projectReviewAdd =
API_URL + 'v1/admin/evaluation/project-review/create'
export const projectReviewUpdate =
API_URL + 'v1/admin/evaluation/project-review/update'
export const projectReviewDelete =
API_URL + 'v1/admin/evaluation/project-review/delete'
export const getAllFilesInProfiles = API_URL + 'v1/admin/profile/all-files'
export const updateProfileFolder = API_URL + 'v1/admin/profile/update-profile'
export const deleteFile = API_URL + 'v1/admin/profile/delete-profile-file'
//Technical
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
export const createTechnical = API_URL + 'v1/admin/technical/create'
export const deleteTechnical = API_URL + 'v1/admin/technical/delete'
// Document
export const listDocument = API_URL + 'v1/admin/document/all'
export const createDocument = API_URL + 'v1/admin/document/create'
export const updateDocument = API_URL + 'v1/admin/document/update'
export const deleteDocument = API_URL + 'v1/admin/document/delete'
// Download File
export const downloadFile = API_URL + 'v1/admin/download-file'
// Electricity Bills
export const getElectricityBills = API_URL + 'v1/admin/electricity-bill'
export const getElectricityBillById = (id: number) =>
API_URL + `v1/admin/electricity-bill/${id}`
export const createElectricityBill =
API_URL + 'v1/admin/electricity-bill/create'
export const updateElectricityBill = (id: number) =>
API_URL + `v1/admin/electricity-bill/${id}`
export const deleteElectricityBill = (id: number) =>
API_URL + `v1/admin/electricity-bill/delete/${id}`
export const exportElectricityBillPdf = (id: number) =>
API_URL + `v1/admin/electricity-bill/export-pdf/${id}`
export const uploadElectricityBillAttachment = (id: number) =>
API_URL + `v1/admin/electricity-bill/upload-attachment/${id}`
export const deleteElectricityBillAttachment = (id: number) =>
API_URL + `v1/admin/electricity-bill/delete-attachment/${id}`
// Equipments
export const getEquipments = API_URL + 'v1/admin/equipment'
export const getEquipmentUserOptions = API_URL + 'v1/admin/equipment/users'
export const getEquipmentUserStatistics =
API_URL + 'v1/admin/equipment/user-statistics'
export const exportEquipments = API_URL + 'v1/admin/equipment/export'
export const getEquipmentByCode = API_URL + 'v1/admin/equipment/by-code'
export const getEquipmentHistory = (id: number) =>
API_URL + `v1/admin/equipment/history/${id}`
export const createEquipment = API_URL + 'v1/admin/equipment/create'
export const updateEquipment = (id: number) =>
API_URL + `v1/admin/equipment/${id}`
export const assignEquipment = (id: number) =>
API_URL + `v1/admin/equipment/${id}/assign`
export const uploadEquipmentImage = (id: number) =>
API_URL + `v1/admin/equipment/${id}/upload-image`
export const deleteEquipmentImage = (id: number) =>
API_URL + `v1/admin/equipment/${id}/image`
export const deleteEquipment = (id: number) =>
API_URL + `v1/admin/equipment/delete/${id}`
// Files APIs
export const getFiles = API_URL + 'v1/admin/profile/files'
export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
export const deleteFileById = API_URL + 'v1/admin/profile/files'

View File

@ -11,7 +11,6 @@ import {
MultiSelect,
Pagination,
RadioGroup,
ScrollArea,
Select,
Skeleton,
Table,
@ -85,9 +84,6 @@ export const DataTableAll = ({
checkBox,
size,
infoTotal,
componentRight,
height = 600,
keyHighlight = '',
}: {
data: any[]
columns: Column[]
@ -96,9 +92,6 @@ export const DataTableAll = ({
checkBox?: boolean
size: string
infoTotal?: React.ReactNode // Set the type to ReactNode to allow JSX elements
componentRight?: React.ReactNode
height?: number
keyHighlight?: string
}) => {
const [Tdata, setTData] = useState<any[]>(data)
// const [tempData, setTempData] = useState<any[]>([])
@ -211,9 +204,6 @@ export const DataTableAll = ({
? 'var(--mantine-color-blue-light)'
: undefined
}
style={{
backgroundColor: element[keyHighlight] ? '#ff70704d' : 'transparent',
}}
>
<Table.Td display={checkBox ? 'block' : 'none'}>
<Checkbox
@ -250,12 +240,10 @@ export const DataTableAll = ({
if (query !== '') {
setTData(
data.filter((obj) =>
Object.values(obj)?.find((c: any) =>
c
.toString()
.normalize('NFC')
.toLowerCase()
.includes(query.normalize('NFC').toLowerCase()),
Object.values(obj).some(
(value: any) =>
value !== null &&
value.toString().toLowerCase().includes(query.toLowerCase()),
),
),
)
@ -337,7 +325,10 @@ export const DataTableAll = ({
</Text>
)}
</Box>
<Box className={classes.totalBox} display={infoTotal ? 'flex' : 'none'}>
<Box
className={classes.totalBox}
display={infoTotal ? 'flex' : 'none'}
>
<Text fz={'sm'} ta={'right'}>
{infoTotal}
</Text>
@ -377,10 +368,8 @@ export const DataTableAll = ({
}}
/>
</Box>
{componentRight}
</Box>
<Box className={classes.box} style={{ height: height + 50 }}>
<ScrollArea h={height}>
<Box className={classes.box}>
<Table
stickyHeader
stickyHeaderOffset={-1}
@ -417,7 +406,6 @@ export const DataTableAll = ({
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</ScrollArea>
</Box>
</Container>
)
@ -463,7 +451,7 @@ export const DataTablePagination = ({
})
const [selectedRows, setSelectedRows] = useState<any[]>([])
const navigate = useNavigate()
let urlParams = new URLSearchParams(location.search)
const urlParams = new URLSearchParams(location.search)
// Render headers
const headers = columns.map((col) => (
@ -603,7 +591,7 @@ export const DataTablePagination = ({
// Remove specific parameters
params.delete(name)
urlParams.delete(name)
// Update the URL without reloading the page
window.history.replaceState({}, document.title, url.toString())
}
@ -667,8 +655,9 @@ export const DataTablePagination = ({
date_used_to: date_used,
})
}
// Add all attributes in 'params' to URL params
urlParams = new URLSearchParams(Object.entries(params))
Object.entries(params).forEach((param) => urlParams.set(...param))
Object.entries(dataFilter).forEach(([key, value]) => {
const typeFilter = filterInfo.find((o) => o.key === key).type
const hasType = {
@ -685,16 +674,17 @@ export const DataTablePagination = ({
if (hasType.Date) {
value = value ? Date.parse(String(value)) / 1000 : '' // to unix timestamp
}
console.log(String(value))
String(value).length
? urlParams.set(key, String(value))
: urlParams.delete(key)
})
// Request to get data API
const res = await get(url, Object.fromEntries(urlParams.entries()))
if (res.status) {
setBaseData(res.data?.links ? res?.data : res)
setTData(res.data?.data ? res.data?.data : res.data)
setBaseData(res)
setTData(res.data)
setSkeletion(false)
navigate({
pathname: location.pathname,
@ -770,7 +760,7 @@ export const DataTablePagination = ({
if (order_by_) {
const sortParam = {
name: order_by_.split('=')[0].split('_').slice(2).join('_'),
name: order_by_.split('=')[0].split('_')[2],
status: order_by_.split('=')[1],
}
if (JSON.stringify(sortParam) !== JSON.stringify(statusSort)) {

View File

@ -1,25 +0,0 @@
import DocViewer, { DocViewerRenderers } from "react-doc-viewer";
export default function FilePreview() {
const docs = [
// {
// uri:
// "http://localhost:9000/uploads/ULRYB3ATJ56B/Screenshot%202021-04-28%20at%2014.04.23.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T142426Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=761187860be22801088ab8c212733f7f52af8f62d638f1341ee2ae4c18944251"
// // "http://localhost:9000/uploads/6QK5HJ84MAEM/RAS-118_CompanyCodes__SalesOffices.xlsx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T110429Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=c20f9b77ffdc1a15910cea5acd3420b6583a1d4d38ce5716da30f1d0ea4315d5"
// // "https://res.cloudinary.com/cloudinaryforme/image/upload/v1618339571/workplace-1245776_1920_i9ayae.jpg"
// },
// {
// uri:
// "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf"
// },
{ uri: "https://s28.q4cdn.com/392171258/files/doc_downloads/test.pdf" },
];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<DocViewer pluginRenderers={DocViewerRenderers} documents={docs} />
</div>
);
}

View File

@ -0,0 +1,9 @@
// import { render } from 'tests'
// import Footer from './Footer'
// describe('Footer component', () => {
// it('renders "Footer"', () => {
// render(<Footer />)
// // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
// })
// })

View File

@ -0,0 +1,107 @@
import { Text, Container, ActionIcon, Group, rem } from '@mantine/core'
import {
IconBrandTwitter,
IconBrandYoutube,
IconBrandInstagram,
} from '@tabler/icons-react'
import { MantineLogo } from '@mantine/ds'
import * as classes from './FooterLinks.module.css.ts'
const data = [
{
title: 'About',
links: [
{ label: 'Features', link: '#' },
{ label: 'Pricing', link: '#' },
{ label: 'Support', link: '#' },
{ label: 'Forums', link: '#' },
],
},
{
title: 'Project',
links: [
{ label: 'Contribute', link: '#' },
{ label: 'Media assets', link: '#' },
{ label: 'Changelog', link: '#' },
{ label: 'Releases', link: '#' },
],
},
{
title: 'Community',
links: [
{ label: 'Join Discord', link: '#' },
{ label: 'Follow on Twitter', link: '#' },
{ label: 'Email newsletter', link: '#' },
{ label: 'GitHub discussions', link: '#' },
],
},
]
const Footer = () => {
const groups = data.map((group) => {
const links = group.links.map((link, index) => (
<Text<'a'>
key={index}
className={classes.link}
component="a"
href={link.link}
onClick={(event) => event.preventDefault()}
>
{link.label}
</Text>
))
return (
<div className={classes.wrapper} key={group.title}>
<Text className={classes.title}>{group.title}</Text>
{links}
</div>
)
})
return (
<footer className={classes.footer}>
<Container className={classes.inner}>
<div className={classes.logo}>
<MantineLogo size={30} />
<Text size="xs" c="dimmed" className={classes.description}>
Build fully functional accessible web applications faster than ever
</Text>
</div>
<div className={classes.groups}>{groups}</div>
</Container>
<Container className={classes.afterFooter}>
<Text c="dimmed" size="sm">
© 2023 APACTECH.dev. All rights reserved.
</Text>
<Group
gap={0}
className={classes.social}
justify="flex-end"
wrap="nowrap"
>
<ActionIcon size="lg" color="gray" variant="subtle">
<IconBrandTwitter
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
/>
</ActionIcon>
<ActionIcon size="lg" color="gray" variant="subtle">
<IconBrandYoutube
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
/>
</ActionIcon>
<ActionIcon size="lg" color="gray" variant="subtle">
<IconBrandInstagram
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
/>
</ActionIcon>
</Group>
</Container>
</footer>
)
}
export default Footer

View File

@ -0,0 +1,104 @@
import { rem } from '@mantine/core'
import { vars } from '@/theme'
import { style } from '@vanilla-extract/css'
export const footer = style({
paddingTop: rem(vars.spacing.xl),
paddingBottom: rem(vars.spacing.xl),
backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
borderTop: `${rem(1)} solid light-dark(${vars.colors.gray[2]}, transparent)`,
})
export const logo = style({
maxWidth: rem(200),
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
},
})
export const description = style({
marginTop: rem(5),
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
marginTop: rem(vars.spacing.xs),
textAlign: 'center',
},
},
})
export const inner = style({
display: 'flex',
justifyContent: 'space-between',
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
flexDirection: 'column',
alignItems: 'center',
},
},
})
export const groups = style({
display: 'flex',
flexWrap: 'wrap',
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
display: 'none',
},
},
})
export const wrapper = style({
width: rem(160),
})
export const link = style({
display: 'block',
color: `light-dark(${vars.colors.gray[6]}, ${vars.colors.dark[1]})`,
fontSize: vars.fontSizes.sm,
paddingTop: rem(3),
paddingBottom: rem(3),
':hover': {
textDecoration: 'underline',
},
})
export const title = style({
fontSize: vars.fontSizes.lg,
fontWeight: 700,
fontFamily: `Greycliff CF, ${vars.fontFamily}`,
marginBottom: Number(vars.spacing.xs) / 2,
color: `light-dark(${vars.colors.black}, ${vars.colors.white})`,
})
export const afterFooter = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: vars.spacing.xl,
paddingTop: vars.spacing.xl,
paddingBottom: vars.spacing.xl,
borderTop: `rem(1) solid light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[4]})`,
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
flexDirection: 'column',
},
},
})
export const social = style({
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
marginTop: vars.spacing.xs,
},
},
})

View File

@ -0,0 +1,62 @@
import { style, styleVariants } from '@vanilla-extract/css'
import { vars } from '../../theme'
import { rem } from '@mantine/core'
export const header = style({
paddingTop: rem(vars.spacing.sm),
backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
borderBottom: `${rem(1)} solid light-dark(${
vars.colors.gray[2]
}, transparent)`,
marginBottom: rem(120),
})
export const mainSection = style({
paddingBottom: rem(vars.spacing.sm),
})
export const user = style({
color: `light-dark(${vars.colors.black}, ${vars.colors.dark[0]})`,
padding: `${rem(vars.spacing.xs)} ${rem(vars.spacing.sm)}`,
borderRadius: rem(vars.radius.sm),
transition: 'background-color 100ms ease',
':hover': {
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
},
'@media': {
[`(max-width: ${vars.breakpoints.xs})`]: {
display: 'none',
},
},
})
export const userActive = style({
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
})
export const tabsList = style({
'::before': {
display: 'none',
},
})
export const tab = styleVariants({
default: {
fontWeight: 500,
height: rem(38),
backgroundColor: 'transparent',
position: 'relative',
bottom: '-1px',
':hover': {
backgroundColor: `light-dark(${vars.colors.gray[1]}, ${vars.colors.dark[5]})`,
},
},
active: {
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[7]})`,
borderColor: `light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[7]})`,
borderBottomColor: 'transparent',
},
})

View File

@ -0,0 +1,11 @@
// import { render } from '@tests'
// import Header from './Header'
// describe('Header component', () => {
// it('renders "Header"', () => {
// render(<Header header={''} setHeader={function(): void {
// throw new Error('Function not implemented.')
// } } />)
// // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
// })
// })

View File

@ -0,0 +1,210 @@
import { logout } from '@/rtk/dispatches/auth'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
import {
Avatar,
Burger,
Container,
Group,
Menu,
Tabs,
Text,
UnstyledButton,
rem,
} from '@mantine/core'
import { MantineLogo } from '@mantine/ds'
import { useDisclosure } from '@mantine/hooks'
import {
IconChevronDown,
IconLogout,
IconSettings,
IconSwitchHorizontal,
} from '@tabler/icons-react'
import cx from 'clsx'
import { useCallback, useState } from 'react'
import { ColorSchemeToggle } from '../ColorSchemeToggle/ColorSchemeToggle'
import LanguagePicker from '../LanguagePicker/LanguagePicker'
import * as classes from './DoubleHeader.module.css.ts'
import { useNavigate } from 'react-router-dom'
// const user = {
// name: 'Jane Spoonfighter',
// email: 'janspoon@fighter.dev',
// image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png',
// };
const tabs = ['Home', 'Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5']
const Header = ({
header,
setHeader,
}: {
header: string
setHeader: (newHeader: string) => void
}) => {
const [opened, { toggle }] = useDisclosure(false)
const [userMenuOpened, setUserMenuOpened] = useState(false)
const dispatch = useAppDispatch()
const navigate = useNavigate()
const user = useAppSelector((state) => state.authentication.user.user)
const items = tabs.map((tab) => (
<Tabs.Tab value={tab} key={tab} onClick={() => setHeader(tab)}>
{tab}
</Tabs.Tab>
))
const handleLogout = useCallback(() => {
dispatch(logout(navigate))
}, [dispatch, navigate])
return (
<div className={classes.header}>
<Container className={classes.mainSection} size="xl">
<Group justify="space-between">
<MantineLogo size={28} />
<Burger opened={opened} onClick={toggle} hiddenFrom="xs" size="sm" />
<Group>
<LanguagePicker />
<ColorSchemeToggle />
<Menu
width={260}
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}
onClose={() => setUserMenuOpened(false)}
onOpen={() => setUserMenuOpened(true)}
withinPortal
>
<Menu.Target>
<UnstyledButton
className={cx(classes.user, {
[classes.userActive]: userMenuOpened,
})}
>
<Group gap={7}>
<Avatar
src={
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png'
}
alt={user.name}
radius="xl"
size={25}
/>
<Text fw={500} size="sm" lh={1} mr={3}>
{user.name}
</Text>
<IconChevronDown
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
{/* <Menu.Item
leftSection={
<IconHeart
style={{ width: rem(16), height: rem(16) }}
color={theme.colors.red[6]}
stroke={1.5}
/>
}
>
Liked posts
</Menu.Item>
<Menu.Item
leftSection={
<IconStar
style={{ width: rem(16), height: rem(16) }}
color={theme.colors.yellow[6]}
stroke={1.5}
/>
}
>
Saved posts
</Menu.Item>
<Menu.Item
leftSection={
<IconMessage
style={{ width: rem(16), height: rem(16) }}
color={theme.colors.blue[6]}
stroke={1.5}
/>
}
>
Your comments
</Menu.Item> */}
<Menu.Label>Settings</Menu.Label>
<Menu.Item
leftSection={
<IconSettings
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
Account settings
</Menu.Item>
<Menu.Item
leftSection={
<IconSwitchHorizontal
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
Change account
</Menu.Item>
<Menu.Item
leftSection={
<IconLogout
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
onClick={handleLogout}
>
Logout
</Menu.Item>
{/* <Menu.Divider />
<Menu.Label>Danger zone</Menu.Label>
<Menu.Item
leftSection={
<IconPlayerPause style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
}
>
Pause subscription
</Menu.Item>
<Menu.Item
color="red"
leftSection={<IconTrash style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
>
Delete account
</Menu.Item> */}
</Menu.Dropdown>
</Menu>
</Group>
</Group>
</Container>
<Container size="md">
<Tabs
defaultValue={header}
variant="outline"
visibleFrom="sm"
classNames={{
// root: classes.tab.active,
list: classes.tabsList,
tab: classes.tab.default,
}}
>
<Tabs.List justify="center">{items}</Tabs.List>
</Tabs>
</Container>
</div>
)
}
export default Header

View File

@ -4,7 +4,6 @@ import { logout } from '@/rtk/dispatches/auth'
import { get, post } from '@/rtk/helpers/apiService'
import { requirementsPassword } from '@/rtk/helpers/variables'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
import { checkPermissions } from '@/utils/checkRoles'
import {
Avatar,
Box,
@ -27,8 +26,6 @@ import {
IconCalendarClock,
IconChartDots2,
IconDevices,
IconFileInvoice,
IconFolders,
IconLayoutSidebarLeftExpand,
IconLayoutSidebarRightExpand,
IconListCheck,
@ -39,7 +36,6 @@ import {
IconReport,
IconScan,
IconSettings,
IconShredder,
IconSun,
IconTicket,
IconUsersGroup,
@ -49,6 +45,7 @@ import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
import classes from './NavbarSimpleColored.module.css'
import { checkPermissions } from '@/utils/checkRoles'
const data = [
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
@ -56,15 +53,15 @@ const data = [
link: '/timekeeping',
label: 'Timekeeping',
icon: IconCalendar,
permissions: 'admin,hr,staff,tester,accountant',
permissions: 'admin,hr,staff,tester',
group: 'staff',
},
{
link: '/tracking',
label: 'Check in/out',
icon: IconScan,
permissions: 'staff,hr,admin',
group: 'staff',
permissions: 'hr,admin',
group: 'other',
},
{
link: '/worklogs',
@ -73,18 +70,11 @@ const data = [
permissions: 'admin,hr,staff,tester',
group: 'staff',
},
{
link: '/documents',
label: 'Documents',
icon: IconFileInvoice,
permissions: 'admin,hr,staff,tester,accountant',
group: 'staff',
},
{
link: '/leave-management',
label: 'Leave Management',
icon: IconCalendarClock,
permissions: 'admin,hr,staff,tester,accountant',
permissions: 'admin,hr,staff,tester',
group: 'staff',
},
{
@ -147,22 +137,8 @@ const data = [
link: '/organization-settings',
label: 'Organization Settings',
icon: IconSettings,
group: 'other',
permissions: 'admin,hr',
},
{
link: '/profiles',
label: 'Profiles',
icon: IconFolders,
group: 'admin',
permissions: 'admin,accountant',
},
{
link: '/office-support',
label: 'Office Support',
icon: IconShredder,
group: 'other',
permissions: 'admin,hr,accountant',
permissions: 'admin',
},
// { link: '/jira', label: 'Jira', icon: IconSubtask },
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
@ -207,7 +183,7 @@ const Navbar = ({
confirm_password: '',
})
const [countSpam, setCountSpam] = useState(0)
const [avatar, setAvatar] = useState(user?.user?.avatar)
const [avatar, setAvatar] = useState(user.user.avatar)
const navigate = useNavigate()
const dispatch = useAppDispatch()
@ -255,11 +231,7 @@ const Navbar = ({
// })
const group = [
{
name: 'staff',
label: 'General',
permissions: 'admin,hr,staff,tester,accountant',
},
{ name: 'staff', label: 'General', permissions: 'admin,hr,staff,tester' },
{ name: 'admin', label: 'Admin', permissions: 'admin' },
{ name: 'other', label: 'Other', permissions: 'admin,hr' },
{ name: 'test', label: 'Test', permissions: 'admin,tester' },
@ -329,7 +301,7 @@ const Navbar = ({
const res = await post(
changePassword,
{
email: user?.user?.email,
email: user.user.email,
password: dataChange.password,
new_password: dataChange.new_password,
confirm_password: dataChange.confirm_password,
@ -445,7 +417,7 @@ const Navbar = ({
cursor: 'pointer',
}}
>
{user?.user?.name}
{user.user.name}
</Code>
</Box>
<Box
@ -597,7 +569,7 @@ const Navbar = ({
<Box p="sm">
<TextInput
label="E-mail"
value={user?.user?.email}
value={user.user.email}
disabled
mb={'md'}
></TextInput>

View File

@ -3,8 +3,3 @@ declare module '*.css'
declare module '@codemirror/lang-javascript'
declare const __VITE_BACKEND_URL__: string
declare module 'react-file-viewer' {
const FileViewer: any
export default FileViewer
}

View File

@ -1,28 +0,0 @@
.root {
/* font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; */
font-size: 14px;
}
.label[data-hovered] {
background-color: var(--mantine-color-gray-0);
}
/* Add the light and dark logic using classes, for example */
.label[data-hovered].light {
background-color: var(--mantine-color-gray-0);
}
.label[data-hovered].dark {
background-color: var(--mantine-color-dark-6);
color: var(--mantine-color-white);
}
.label[data-selected] {
font-weight: 700;
}
.label[data-selected].light {
background-color: var(--mantine-color-blue-0);
color: var(--mantine-color-black);
}
.label[data-selected].dark {
background-color: rgba(var(--mantine-color-blue-8), 0.35);
color: var(--mantine-color-blue-0);
}

View File

@ -1,335 +0,0 @@
import { getFiles, uploadFiles } from '@/api/Admin'
import { get } from '@/rtk/helpers/apiService'
import { getAccessToken } from '@/rtk/localStorage'
import {
Box,
Button,
Card,
Collapse,
Group,
Modal,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core'
import { notifications } from '@mantine/notifications'
import {
IconChevronDown,
IconDownload,
IconFileTypeDocx,
IconFileTypePdf,
IconFolder,
IconListCheck,
IconPhoto,
IconSearch,
IconTrash,
} from '@tabler/icons-react'
import axios from 'axios'
import { useEffect, useState } from 'react'
import FileUploadForm from '../Profile/components/FileUploadForm'
import classes from './AllProfiles.module.css'
interface FileData {
id: number
name: string
url: string
type: string
description?: string
created_at: string
}
interface GroupedFiles {
[key: string]: FileData[]
}
const AllProfiles = () => {
const [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
const [currentUser, setCurrentUser] = useState<string>('')
const [openedProfile, setOpenedProfile] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [expandedFolders, setExpandedFolders] = useState<{
[key: string]: boolean
}>({})
const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
const toggleFolder = (userName: string) => {
setExpandedFolders((prev) => ({
...prev,
[userName]: !prev[userName],
}))
}
const getFileIcon = (type: string) => {
switch (type) {
case 'document':
return <IconFileTypeDocx size={16} />
case 'image':
return <IconPhoto size={16} />
default:
return <IconFileTypePdf size={16} />
}
}
const handleSubmit = async (
e: React.FormEvent,
fileName: string,
description: string,
currentUser: string
) => {
e.preventDefault()
setIsLoading(true)
const formData = new FormData()
if (selectedFile) {
formData.append('file', selectedFile)
formData.append('name', fileName)
formData.append('description', description)
formData.append('user_name', currentUser)
try {
const token = await getAccessToken()
const response = await axios.post(uploadFiles, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
})
if (response.status === 200) {
setSelectedFile(null)
await getAllFiles()
return true
}
return false
} catch (error) {
console.error('Error uploading file:', error)
throw error
} finally {
setIsLoading(false)
}
}
return false
}
const getAllFiles = async () => {
try {
const res = await get(getFiles)
if (res.status === true) {
setGroupedFiles(res.data)
}
} catch (error) {
console.log(error)
}
}
const removeFile = async (id: number) => {
try {
const token = await getAccessToken();
const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.status === 200) {
notifications.show({
title: 'Thành công',
message: 'Xóa file thành công',
color: 'green',
});
await getAllFiles();
}
} catch (error) {
console.log(error);
notifications.show({
title: 'Lỗi',
message: 'Không thể xóa file',
color: 'red',
});
}
}
useEffect(() => {
getAllFiles()
}, [])
const filterFiles = (files: FileData[], searchTerm: string) => {
return files.filter(
(file) =>
file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(file.description &&
file.description.toLowerCase().includes(searchTerm.toLowerCase())),
)
}
return (
<div>
<div className={classes.title}>
<h3>
<Text size="sm">Admin/</Text>
Files Management
</h3>
</div>
<Box ml={'md'}>
<Stack gap="xs">
{Object.entries(groupedFiles).map(([userName, files]) => (
<Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
<Group
justify="space-between"
mb="xs"
gap="xs"
onClick={() => toggleFolder(userName)}
style={{ cursor: 'pointer' }}
>
<Group gap="xs">
<IconFolder size={18} color="var(--mantine-color-yellow-9)" />
<Title order={5}>{userName}</Title>
</Group>
<Group gap="xs">
<Button
size="xs"
variant="subtle"
color="gray"
onClick={() => toggleFolder(userName)}
leftSection={
<IconChevronDown
size={14}
style={{
transform: expandedFolders[userName]
? 'rotate(180deg)'
: 'none',
transition: 'transform 0.2s',
}}
/>
}
>
{expandedFolders[userName] ? 'Collapse' : 'Expand'}
</Button>
<Button
size="xs"
variant="light"
color="blue"
onClick={() => {
setCurrentUser(userName)
setOpenedProfile(true)
}}
>
<Group gap={2}>
<IconListCheck size={14} />
<Text size="xs">Upload Files</Text>
</Group>
</Button>
</Group>
</Group>
<Collapse in={expandedFolders[userName]}>
<Stack gap="xs">
<TextInput
placeholder="Search files by name or description..."
size="xs"
leftSection={<IconSearch size={14} />}
value={searchTerms[userName] || ''}
onChange={(e) =>
setSearchTerms((prev) => ({
...prev,
[userName]: e.target.value,
}))
}
onClick={(e) => e.stopPropagation()}
/>
{filterFiles(files, searchTerms[userName] || '')
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.map((file: FileData) => (
<Card
key={file.id}
shadow="xs"
padding="xs"
radius="sm"
withBorder
>
<Group justify="space-between" gap="xs">
<Group gap="xs">
{getFileIcon(file.type)}
<Box>
<Text size="xs" fw={500}>
{file.name}
</Text>
{file.description && (
<Text size="xs" c="dimmed">
{file.description}
</Text>
)}
<Text size="xs" c="dimmed">
Uploaded:{' '}
{new Date(file.created_at).toLocaleDateString()}
</Text>
</Box>
</Group>
<Group gap="xs">
<Button
size="xs"
variant="light"
color="blue"
component='a'
href={`${import.meta.env.VITE_BACKEND_URL}${
import.meta.env.VITE_BACKEND_URL?.includes(
'localhost',
)
? ''
: 'image/'
}${file.url.slice(1)}`}
target="_blank"
>
<Group gap={2}>
<IconDownload size={12} />
<Text size="xs">Download</Text>
</Group>
</Button>
<Button
size="xs"
variant="light"
color="red"
onClick={() => removeFile(file.id)}
>
<Group gap={2}>
<IconTrash size={12} />
<Text size="xs">Delete</Text>
</Group>
</Button>
</Group>
</Group>
</Card>
),
)}
</Stack>
</Collapse>
</Card>
))}
</Stack>
<Modal
size="lg"
opened={openedProfile}
onClose={() => {
setOpenedProfile(false)
setCurrentUser('')
setSelectedFile(null)
}}
>
<Box>
<FileUploadForm
data={groupedFiles[currentUser] || []}
handleSubmit={handleSubmit}
handleFileChange={(file) => file && setSelectedFile(file)}
removeFile={removeFile}
isLoading={isLoading}
currentUser={currentUser}
/>
</Box>
</Modal>
</Box>
</div>
)
}
export default AllProfiles

View File

@ -10,7 +10,6 @@ import {
Popover,
Text,
Tooltip,
Switch,
} from '@mantine/core'
import { IconInnerShadowTopRightFilled } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
@ -81,31 +80,6 @@ const Allocation = () => {
const [opened, setOpened] = useState(false)
const [issDetail, setIssDetail] = useState('')
const [data, setData] = useState<any>({})
const [showDrawio, setShowDrawio] = useState(true)
useEffect(() => {
// Check if iframe already exists
const existingIframe = document.querySelector('#drawio iframe')
if (existingIframe) return
// Add iframe only if it doesn't exist
const iframe = document.createElement('iframe')
iframe.src = import.meta.env.VITE_URL_DRAWIO ?? ''
iframe.style.width = '100%'
iframe.style.height = '500px'
const drawioDiv = document.getElementById('drawio')
if (drawioDiv) {
drawioDiv.appendChild(iframe)
}
return () => {
const iframe = document.querySelector('#drawio iframe')
if (iframe) {
iframe.remove()
}
}
}, [])
const getAll = async () => {
try {
const res = await get(getAllUserDoing)
@ -143,14 +117,6 @@ const Allocation = () => {
}, [])
return (
<div>
<Box style={{ display: 'flex', alignItems: 'center', margin: '10px' }}>
<Switch
label="Show Diagram"
checked={showDrawio}
onChange={(event) => setShowDrawio(event.currentTarget.checked)}
/>
</Box>
<div id="drawio" style={{ display: showDrawio ? 'block' : 'none' }}></div>
<div>
<Box
style={{
@ -172,12 +138,12 @@ const Allocation = () => {
}}
>
<Box>
{/* <Text fw={600} fz={'md'}>
<Text fw={600} fz={'md'}>
Admin/
</Text>
<Text fw={700} fz={'lg'}>
Personnel Allocation
</Text> */}
</Text>
<Text fw={600} fz={'sm'} c={'gray'} fs={'italic'} ml={'md'}>
"P:" is the timspent/timeestimate number within the project itself
</Text>
@ -258,34 +224,23 @@ const Allocation = () => {
alignItems: 'center',
}}
>
<Box w="10%">
<Box w='10%'>
<IconInnerShadowTopRightFilled
style={{ color: 'orange' }}
height={20}
width={20}
display={
userData.issues?.filter(
display={userData.issues?.filter(
(iss: Issue) =>
iss.fields.status.name ===
'In Progress' &&
Date.now() -
new Date(
iss.changelog?.histories[0]?.created,
).getTime() >
172800000,
).length > 0
? 'block'
: 'none'
}
iss.fields.status.name === 'In Progress' &&
((Date.now() - (new Date(iss.changelog?.histories[0]?.created)).getTime()) > 172800000)
).length > 0 ? 'block' :'none'}
/>
</Box>
<Box display={'flex'}>
<Avatar
size={'sm'}
ml={'5px'}
src={
userData.user.avatarUrls['48x48']
}
src={userData.user.avatarUrls['48x48']}
/>
<Text ml={'md'} fw={600}>
{user}
@ -305,12 +260,9 @@ const Allocation = () => {
}}
ml={'md'}
p="0 20px"
>
<Text
ml={'md'}
fw={700}
fz={'sm'}
>{`P: `}</Text>
<Text ml={'md'} fw={700} fz={'sm'}>{`P: `}</Text>
<Text fw={700} c="green" fz={'sm'}>{`${
userData.p_total_spent / 60 / 60
}h/`}</Text>
@ -364,7 +316,7 @@ const Allocation = () => {
<Box
className={
Date.now() - date.getTime() >
172800000 * 5 // 10 days
172800000 * 5
? classes['blinking-background']
: ''
}
@ -374,7 +326,7 @@ const Allocation = () => {
borderRadius: '10px',
cursor: 'pointer',
backgroundColor: issStatus
? issLastHistory < 172800000 // 2 days
? issLastHistory < 172800000
? '#d1f3d1'
: '#ffff8a'
: '',

View File

@ -1,37 +0,0 @@
.title {
background-color: light-dark(var(white), var(--mantine-color-dark-7));
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
var(--mantine-spacing-sm);
border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
}
.optionIcon {
display: flex;
justify-content: space-evenly;
}
.deleteIcon {
color: red;
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.editIcon {
color: rgb(9, 132, 132);
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.editIcon:hover {
background-color: rgba(203, 203, 203, 0.809);
}
.deleteIcon:hover {
background-color: rgba(203, 203, 203, 0.809);
}

Some files were not shown because too many files have changed in this diff Show More