ManagementSystem/BACKEND/Modules/Admin/app/Http/Controllers/EquipmentController.php

518 lines
19 KiB
PHP

<?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());
}
}
}