518 lines
19 KiB
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());
|
|
}
|
|
}
|
|
}
|