2026-05-25 15:57:06 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Livewire\Client;
|
|
|
|
|
|
|
|
|
|
use Livewire\Component;
|
|
|
|
|
use App\Models\Project;
|
2026-05-25 19:08:06 +02:00
|
|
|
use App\Models\ChangeOrder;
|
2026-05-25 15:57:06 +02:00
|
|
|
|
|
|
|
|
class ClientProjects extends Component
|
|
|
|
|
{
|
2026-06-17 10:36:44 +02:00
|
|
|
public $projects = [];
|
2026-05-25 15:57:06 +02:00
|
|
|
public $selectedProject = null;
|
2026-06-17 10:36:44 +02:00
|
|
|
public $projectDetails = [];
|
|
|
|
|
public $galleryImages = [];
|
|
|
|
|
public $changeOrders = [];
|
2026-05-25 15:57:06 +02:00
|
|
|
|
|
|
|
|
public function mount()
|
|
|
|
|
{
|
|
|
|
|
$this->loadProjects();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function loadProjects()
|
|
|
|
|
{
|
|
|
|
|
$user = auth()->user();
|
|
|
|
|
$this->projects = $user->projects()
|
|
|
|
|
->wherePivot('role_in_project', 'client')
|
2026-06-17 10:36:44 +02:00
|
|
|
->with(['phases' => function ($query) {
|
2026-05-25 15:57:06 +02:00
|
|
|
$query->select('id', 'project_id', 'name', 'progress_percent');
|
|
|
|
|
}])
|
|
|
|
|
->get()
|
|
|
|
|
->toArray();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 10:36:44 +02:00
|
|
|
/**
|
|
|
|
|
* Return only project IDs the current user can access as client.
|
|
|
|
|
*/
|
|
|
|
|
private function accessibleProjectIds(): \Illuminate\Support\Collection
|
|
|
|
|
{
|
|
|
|
|
return auth()->user()->projects()
|
|
|
|
|
->wherePivot('role_in_project', 'client')
|
|
|
|
|
->pluck('projects.id');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 15:57:06 +02:00
|
|
|
public function selectProject($projectId)
|
|
|
|
|
{
|
2026-06-17 10:36:44 +02:00
|
|
|
// Verify the project is one the user is a client on
|
|
|
|
|
if (!$this->accessibleProjectIds()->contains((int) $projectId)) {
|
|
|
|
|
abort(403);
|
|
|
|
|
}
|
|
|
|
|
$this->selectedProject = (int) $projectId;
|
2026-05-25 15:57:06 +02:00
|
|
|
$this->loadProjectDetails();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function loadProjectDetails()
|
|
|
|
|
{
|
|
|
|
|
if (!$this->selectedProject) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 10:36:44 +02:00
|
|
|
// Re-verify ownership on every load
|
|
|
|
|
if (!$this->accessibleProjectIds()->contains($this->selectedProject)) {
|
|
|
|
|
abort(403);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 15:57:06 +02:00
|
|
|
$project = Project::with([
|
2026-06-17 10:36:44 +02:00
|
|
|
'phases',
|
|
|
|
|
'changeOrders',
|
2026-05-25 15:57:06 +02:00
|
|
|
])->find($this->selectedProject);
|
|
|
|
|
|
|
|
|
|
if (!$project) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->projectDetails = [
|
2026-06-17 10:36:44 +02:00
|
|
|
'id' => $project->id,
|
|
|
|
|
'name' => $project->name,
|
|
|
|
|
'description'=> $project->description ?? '',
|
2026-05-25 15:57:06 +02:00
|
|
|
'start_date' => $project->start_date,
|
2026-06-17 10:36:44 +02:00
|
|
|
'end_date' => $project->end_date_estimated,
|
|
|
|
|
'status' => $project->status,
|
|
|
|
|
'progress' => round($project->phases->avg('progress_percent') ?? 0),
|
2026-05-25 15:57:06 +02:00
|
|
|
];
|
|
|
|
|
|
2026-05-25 19:08:06 +02:00
|
|
|
$mediaImages = $project->media()
|
|
|
|
|
->where('category', 'image')
|
|
|
|
|
->latest()
|
|
|
|
|
->take(3)
|
|
|
|
|
->get()
|
2026-06-17 10:36:44 +02:00
|
|
|
->map(fn ($media) => [
|
|
|
|
|
'url' => $media->url,
|
|
|
|
|
'title' => $media->name,
|
|
|
|
|
'date' => $media->created_at->format('d/m/Y'),
|
|
|
|
|
])
|
2026-05-25 19:08:06 +02:00
|
|
|
->toArray();
|
2026-05-25 15:57:06 +02:00
|
|
|
|
2026-06-17 10:36:44 +02:00
|
|
|
$this->galleryImages = $mediaImages ?: [];
|
2026-05-25 19:08:06 +02:00
|
|
|
|
|
|
|
|
$this->changeOrders = $project->changeOrders
|
2026-06-17 10:36:44 +02:00
|
|
|
->sortByDesc('requested_at')
|
|
|
|
|
->map(fn ($order) => [
|
|
|
|
|
'id' => $order->id,
|
|
|
|
|
'title' => $order->title,
|
|
|
|
|
'description' => $order->description,
|
|
|
|
|
'status' => $order->status,
|
|
|
|
|
'requested_at' => $order->requested_at?->format('d/m/Y') ?? '',
|
|
|
|
|
'amount' => $order->amount,
|
|
|
|
|
])
|
|
|
|
|
->values()
|
2026-05-25 19:08:06 +02:00
|
|
|
->toArray();
|
2026-05-25 15:57:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function approveChangeOrder($orderId)
|
|
|
|
|
{
|
2026-06-17 10:36:44 +02:00
|
|
|
$changeOrder = ChangeOrder::where('id', $orderId)
|
|
|
|
|
->where('project_id', $this->selectedProject)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$changeOrder) {
|
|
|
|
|
abort(403);
|
2026-05-25 15:57:06 +02:00
|
|
|
}
|
2026-06-17 10:36:44 +02:00
|
|
|
|
|
|
|
|
// Verify this project is accessible by the current user
|
|
|
|
|
if (!$this->accessibleProjectIds()->contains($this->selectedProject)) {
|
|
|
|
|
abort(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$changeOrder->update([
|
|
|
|
|
'status' => 'approved',
|
|
|
|
|
'responded_at' => now()->toDateString(),
|
|
|
|
|
'responded_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->loadProjectDetails();
|
|
|
|
|
$this->dispatch('changeOrderUpdated', ['id' => $orderId, 'status' => 'approved']);
|
2026-05-25 15:57:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function rejectChangeOrder($orderId)
|
|
|
|
|
{
|
2026-06-17 10:36:44 +02:00
|
|
|
$changeOrder = ChangeOrder::where('id', $orderId)
|
|
|
|
|
->where('project_id', $this->selectedProject)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$changeOrder) {
|
|
|
|
|
abort(403);
|
2026-05-25 15:57:06 +02:00
|
|
|
}
|
2026-06-17 10:36:44 +02:00
|
|
|
|
|
|
|
|
// Verify this project is accessible by the current user
|
|
|
|
|
if (!$this->accessibleProjectIds()->contains($this->selectedProject)) {
|
|
|
|
|
abort(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$changeOrder->update([
|
|
|
|
|
'status' => 'rejected',
|
|
|
|
|
'responded_at' => now()->toDateString(),
|
|
|
|
|
'responded_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->loadProjectDetails();
|
|
|
|
|
$this->dispatch('changeOrderUpdated', ['id' => $orderId, 'status' => 'rejected']);
|
2026-05-25 15:57:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function render()
|
|
|
|
|
{
|
|
|
|
|
return view('livewire.client.client-projects');
|
|
|
|
|
}
|
|
|
|
|
}
|