From 94ddddf9437cdbb82c8cf6feda07ce1911b841b6 Mon Sep 17 00:00:00 2001 From: Aaron Jackson Date: Tue, 30 Jun 2026 00:10:25 +0100 Subject: [PATCH] Add process for torting / requesting removal of projects --- app/HMS/Entities/Members/Project.php | 50 +++++++++++++ .../HMS.Entities.Members.Project.dcm.yml | 8 ++- .../Controllers/Members/ProjectController.php | 68 +++++++++++++++++- app/Notifications/ProjectRemovalRequest.php | 72 +++++++++++++++++++ config/roles.php | 2 + ...0260629214620_add_project_tort_details.php | 26 +++++++ database/seeds/ContentBlockSeeder.php | 10 +++ .../membership/projectRemoval.blade.php | 17 +++++ .../views/members/project/show.blade.php | 30 ++++++++ .../views/members/project/tort.blade.php | 24 +++++++ routes/web.php | 6 ++ 11 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 app/Notifications/ProjectRemovalRequest.php create mode 100644 database/migrations_doctrine/Version20260629214620_add_project_tort_details.php create mode 100644 resources/views/emails/membership/projectRemoval.blade.php create mode 100644 resources/views/members/project/tort.blade.php diff --git a/app/HMS/Entities/Members/Project.php b/app/HMS/Entities/Members/Project.php index 05b33eb79..522cd57d7 100644 --- a/app/HMS/Entities/Members/Project.php +++ b/app/HMS/Entities/Members/Project.php @@ -47,6 +47,16 @@ class Project */ protected $user; + /** + * @var null|string + */ + protected $tortReason; + + /** + * @var null|Carbon + */ + protected $tortDate; + /** * Gets the value of id. * @@ -233,4 +243,44 @@ public function setUser(User $user) return $this; } + + /** + * @return null|string + */ + public function getTortReason() + { + return $this->tortReason; + } + + /** + * @param null|string $tortReason + * + * @return self + */ + public function setTortReason(?string $tortReason) + { + $this->tortReason = $tortReason; + + return $this; + } + + /** + * @return null|Carbon + */ + public function getTortDate() + { + return $this->tortDate; + } + + /** + * @param null|Carbon $tortDate + * + * @return self + */ + public function setTortDate(?Carbon $tortDate) + { + $this->tortDate = $tortDate; + + return $this; + } } diff --git a/app/HMS/Mappings/HMS.Entities.Members.Project.dcm.yml b/app/HMS/Mappings/HMS.Entities.Members.Project.dcm.yml index 887c2b18a..2b05c3a06 100644 --- a/app/HMS/Mappings/HMS.Entities.Members.Project.dcm.yml +++ b/app/HMS/Mappings/HMS.Entities.Members.Project.dcm.yml @@ -26,6 +26,12 @@ HMS\Entities\Members\Project: nullable: true state: type: integer + tortReason: + type: text + nullable: true + tortDate: + type: date + nullable: true manyToOne: user: targetEntity: \HMS\Entities\User @@ -33,4 +39,4 @@ HMS\Entities\Members\Project: name: user_id referencedColumnName: id nullable: false - \ No newline at end of file + diff --git a/app/Http/Controllers/Members/ProjectController.php b/app/Http/Controllers/Members/ProjectController.php index 7348580e5..5d3efa7d9 100644 --- a/app/Http/Controllers/Members/ProjectController.php +++ b/app/Http/Controllers/Members/ProjectController.php @@ -4,11 +4,15 @@ use App\Events\Labels\ProjectPrint; use App\Http\Controllers\Controller; +use App\Notifications\ProjectRemovalRequest; +use Carbon\Carbon; use Doctrine\ORM\EntityNotFoundException; use HMS\Entities\Members\Project; +use HMS\Entities\Role; use HMS\Entities\User; use HMS\Factories\Members\ProjectFactory; use HMS\Repositories\Members\ProjectRepository; +use HMS\Repositories\RoleRepository; use HMS\Repositories\UserRepository; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -31,21 +35,29 @@ class ProjectController extends Controller */ protected $userRepository; + /** + * @var RoleRepository + */ + protected $roleRepository; + /** * Create a new controller instance. * * @param ProjectRepository $projectRepository * @param ProjectFactory $projectFactory * @param UserRepository $userRepository + * @param RoleRepository $roleRepository */ public function __construct( ProjectRepository $projectRepository, ProjectFactory $projectFactory, - UserRepository $userRepository + UserRepository $userRepository, + RoleRepository $roleRepository, ) { $this->projectRepository = $projectRepository; $this->projectFactory = $projectFactory; $this->userRepository = $userRepository; + $this->roleRepository = $roleRepository; $this->middleware('feature:projects'); $this->middleware('can:project.view.self')->only(['index', 'show']); @@ -53,6 +65,7 @@ public function __construct( $this->middleware('can:project.edit.self')->only(['edit', 'update', 'markActive', 'markComplete']); $this->middleware('can:project.edit.all')->only(['markAbandoned']); $this->middleware(['can:project.printLabel.self', 'feature:label_printer'])->only(['printLabel']); + $this->middleware('can:project.tort')->only(['tort', 'clearTort']); } /** @@ -274,4 +287,57 @@ public function markComplete(Project $project) return back(); } + + /** + * Tort a project (request removal). + * + * @param Project $project + * + * @return \Illuminate\Http\Response + */ + public function tort(Project $project) + { + return view('members.project.tort') + ->with(['project' => $project]); + } + + /** + * Perform a tort a project (request removal). + * + * @param Project $project + * + * @return \Illuminate\Http\Response + */ + public function performTort(Request $request, Project $project) + { + $this->validate($request, [ + 'tortReason' => 'required|string', + ]); + + $project->setTortReason($request->tortReason); + $project->setTortDate(Carbon::now()); + $this->projectRepository->save($project); + + $trusteesTeamRole = $this->roleRepository->findOneByName(Role::TEAM_TRUSTEES); + $trusteesTeamRole->notify(new ProjectRemovalRequest($project)); + $project->getUser()->notify(new ProjectRemovalRequest($project)); + + return redirect()->route('projects.show', ['project' => $project->getId()]); + } + + /** + * Clear an earlier tort request. + * + * @param Project $project + * + * @return \Illuminate\Http\Response + */ + public function clearTort(Project $project) + { + $project->setTortReason(null); + $project->setTortDate(null); + $this->projectRepository->save($project); + + return redirect()->route('projects.show', ['project' => $project->getId()]); + } } diff --git a/app/Notifications/ProjectRemovalRequest.php b/app/Notifications/ProjectRemovalRequest.php new file mode 100644 index 000000000..c5d72a9d2 --- /dev/null +++ b/app/Notifications/ProjectRemovalRequest.php @@ -0,0 +1,72 @@ +project = $project; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * + * @return array + */ + public function via($notifiable) + { + $channels = ['mail']; + + return $channels; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + $roleRepository = App::make(RoleRepository::class); + $trusteesTeamRole = $roleRepository->findOneByName(Role::TEAM_TRUSTEES); + + return (new MailMessage) + ->subject(config('branding.space_name') . ': Torts (Interference with Goods) Act 1977 - Removal requested') + ->from($trusteesTeamRole->getEmail()) + ->markdown( + 'emails.membership.projectRemoval', + [ + 'fullname' => $this->project->getUser()->getFullname(), + 'projectNumber' => $this->project->getId(), + 'projectName' => $this->project->getProjectName(), + 'removalReason' => $this->project->getTortReason(), + ] + ); + } +} diff --git a/config/roles.php b/config/roles.php index f2c23f727..e16e0afe2 100644 --- a/config/roles.php +++ b/config/roles.php @@ -46,6 +46,7 @@ 'project.edit.all', 'project.printLabel.self', 'project.printLabel.all', + 'project.tort', 'box.buy.self', 'box.issue.all', 'box.view.self', @@ -401,6 +402,7 @@ 'project.view.all', 'project.edit.all', 'project.printLabel.all', + 'project.tort', 'box.issue.all', 'box.view.all', 'box.edit.all', diff --git a/database/migrations_doctrine/Version20260629214620_add_project_tort_details.php b/database/migrations_doctrine/Version20260629214620_add_project_tort_details.php new file mode 100644 index 000000000..17793e636 --- /dev/null +++ b/database/migrations_doctrine/Version20260629214620_add_project_tort_details.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE member_projects ADD tort_reason LONGTEXT DEFAULT NULL, ADD tort_date DATE DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE member_projects DROP tort_reason, DROP tort_date'); + } +} diff --git a/database/seeds/ContentBlockSeeder.php b/database/seeds/ContentBlockSeeder.php index 3b635c397..61f4f8ba8 100644 --- a/database/seeds/ContentBlockSeeder.php +++ b/database/seeds/ContentBlockSeeder.php @@ -265,6 +265,16 @@ class ContentBlockSeeder extends Seeder 'block' => 'main', 'content' => '', ], + [ + 'view' => 'emails.project.tort', + 'block' => 'main', + 'content' => 'In accordance with the Torts (Interference with Goods) Act 1977 we are obligated to inform you that if your goods have not been collected from Nottingham Hackspace at the address below within 14 days of the date of this email, they will be disposed of', + ], + [ + 'view' => 'emails.project.tort', + 'block' => 'additional', + 'content' => 'Should you wish to collect these items at any point, you can do so by getting in touch with a trustee, who can let you into Nottingham hackspace. A friend can also collect your items for you if you wish - please inform us by email that you are happy for someone else to collect the items.', + ], ]; /** diff --git a/resources/views/emails/membership/projectRemoval.blade.php b/resources/views/emails/membership/projectRemoval.blade.php new file mode 100644 index 000000000..386ffdcaa --- /dev/null +++ b/resources/views/emails/membership/projectRemoval.blade.php @@ -0,0 +1,17 @@ +@component('mail::message') +# Dear {{ $fullname }}, + +@content('emails.project.tort', 'main') + + +The project details are: + +* Project Number: {{ $projectNumber }} +* Project Name: {{ $projectName }} +* Removal Reason: {{ $removalReason }} + +@content('emails.project.tort', 'additional') + + +{{ config('branding.community_name') }} Trustees +@endcomponent diff --git a/resources/views/members/project/show.blade.php b/resources/views/members/project/show.blade.php index ed1d5e486..f025e7e6b 100644 --- a/resources/views/members/project/show.blade.php +++ b/resources/views/members/project/show.blade.php @@ -4,6 +4,16 @@ @section('content')
+ + @if ($project->getTortReason()) + + @endif +
@@ -90,5 +100,25 @@ @endif @endif + + @if (\Auth::user()->can('project.tort')) + @if ($project->getTortReason()) + + @else + + @endif + @endif @endsection diff --git a/resources/views/members/project/tort.blade.php b/resources/views/members/project/tort.blade.php new file mode 100644 index 000000000..d0d1e718f --- /dev/null +++ b/resources/views/members/project/tort.blade.php @@ -0,0 +1,24 @@ +@extends('layouts.app') + +@section('pageTitle', 'Project Removal Request') + +@section('content') +
+

Please enter the reason for requesting removal. This will be included in the email after the project details.

+
+ @csrf + +
+ + + @if ($errors->has('tortReason')) +

+ {{ $errors->first('tortReason') }} +

+ @endif +
+ + + +
+@endsection diff --git a/routes/web.php b/routes/web.php index 481e6a137..cbbed3c1e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -223,6 +223,12 @@ ->name('projects.markAbandoned'); Route::patch('projects/{project}/markComplete', [ProjectController::class, 'markComplete']) ->name('projects.markComplete'); + Route::get('projects/{project}/tort', [ProjectController::class, 'tort']) + ->name('projects.tort'); + Route::post('projects/{project}/tort', [ProjectController::class, 'performTort']) + ->name('projects.tort'); + Route::patch('projects/{project}/clear-tort', [ProjectController::class, 'clearTort']) + ->name('projects.clearTort'); Route::get('projects/{project}/print', [ProjectController::class, 'printLabel'])->name('projects.print'); Route::resource('projects', ProjectController::class) ->except(['destroy']);