diff --git a/app/Console/Commands/SendTerrafundReportRemindersCommand.php b/app/Console/Commands/SendTerrafundReportRemindersCommand.php index 5cddec3f7..a46fc3bba 100644 --- a/app/Console/Commands/SendTerrafundReportRemindersCommand.php +++ b/app/Console/Commands/SendTerrafundReportRemindersCommand.php @@ -32,10 +32,13 @@ public function handle(): int ->chunkById(100, function ($programmes) { $programmes->each(function ($programme) { if ($programme->users->count()) { - Mail::to($programme->users->pluck('email_address'))->queue(new TerrafundReportReminder($programme->id)); $programme->users->each(function ($user) use ($programme) { + Mail::to($user->email_address) + ->queue(new TerrafundReportReminder($programme->id, $user)); + NotifyTerrafundReportReminderJob::dispatch($user, $programme); }); + } }); }); diff --git a/app/Console/Commands/SendTerrafundSiteAndNurseryRemindersCommand.php b/app/Console/Commands/SendTerrafundSiteAndNurseryRemindersCommand.php index 39d7801bb..f45aec3dc 100644 --- a/app/Console/Commands/SendTerrafundSiteAndNurseryRemindersCommand.php +++ b/app/Console/Commands/SendTerrafundSiteAndNurseryRemindersCommand.php @@ -31,7 +31,10 @@ public function handle(): int ->chunkById(100, function ($programmes) { $programmes->each(function ($programme) { if ($programme->users->count()) { - Mail::to($programme->users->pluck('email_address'))->queue(new TerrafundSiteAndNurseryReminder($programme->id)); + $programme->users->each(function ($user) use ($programme) { + Mail::to($user->email_address) + ->queue(new TerrafundSiteAndNurseryReminder($programme->id, $user)); + }); } }); }); diff --git a/app/Console/Commands/UploadShapefileCommand.php b/app/Console/Commands/UploadShapefileCommand.php index 2dbfc15dc..2155a2553 100644 --- a/app/Console/Commands/UploadShapefileCommand.php +++ b/app/Console/Commands/UploadShapefileCommand.php @@ -9,7 +9,7 @@ class UploadShapefileCommand extends Command { - protected $signature = 'shapefile:upload {file} {--site_uuid=}'; + protected $signature = 'shapefile:upload {file} {--site_uuid=} {--submit_polygon_loaded=}'; protected $description = 'Upload a shapefile to the application'; @@ -17,6 +17,7 @@ public function handle() { $filePath = $this->argument('file'); $siteUuid = $this->option('site_uuid'); + $submitPolygonLoaded = $this->option('submit_polygon_loaded'); if (! file_exists($filePath)) { $this->error("File not found: $filePath"); @@ -33,12 +34,12 @@ public function handle() true // Set test mode to true to prevent the file from being moved ); - // Create a fake request with the uploaded file and site UUID $request = new Request(); $request->files->set('file', $uploadedFile); - $request->request->set('uuid', $siteUuid); - - // Instantiate the controller and call the method + $request->merge([ + 'uuid' => $siteUuid, + 'submit_polygon_loaded' => $submitPolygonLoaded, + ]); $controller = new TerrafundCreateGeometryController(); $response = $controller->uploadShapefile($request); diff --git a/app/Helpers/GeometryHelper.php b/app/Helpers/GeometryHelper.php index 7a374c4cd..667ea1a63 100755 --- a/app/Helpers/GeometryHelper.php +++ b/app/Helpers/GeometryHelper.php @@ -78,9 +78,6 @@ public function updateProjectCentroid(string $projectUuid) 'long' => $longitude, ]); - - Log::info("Centroid updated for projectUuid: $projectUuid"); - return response()->json([ 'message' => 'Centroid updated', 'centroid' => $centroid, diff --git a/app/Http/Controllers/InterestsController.php b/app/Http/Controllers/InterestsController.php index 04e34bcb8..c1927c32d 100644 --- a/app/Http/Controllers/InterestsController.php +++ b/app/Http/Controllers/InterestsController.php @@ -37,8 +37,8 @@ public function createAction(StoreInterestRequest $request): JsonResponse } $initiator = $data['initiator'] == 'offer' ? $offer : $pitch; $this->authorize('update', $initiator); - $me = Auth::user(); - $data['organisation_id'] = $me->organisation_id; + $user = Auth::user(); + $data['organisation_id'] = $user->organisation_id; $exists = InterestModel::where('organisation_id', '=', $data['organisation_id']) ->where('initiator', '=', $data['initiator']) ->where('offer_id', '=', $data['offer_id']) @@ -50,7 +50,7 @@ public function createAction(StoreInterestRequest $request): JsonResponse $interest = new InterestModel($data); $interest->saveOrFail(); $interest->refresh(); - NotifyInterestJob::dispatch($interest); + NotifyInterestJob::dispatch($interest, $user); return JsonResponseHelper::success(new InterestResource($interest), 201); } @@ -88,14 +88,14 @@ public function deleteAction(InterestModel $interest): JsonResponse public function readAllByTypeAction(Request $request, string $type): JsonResponse { $this->authorize('readAll', \App\Models\Interest::class); - $me = Auth::user(); + $user = Auth::user(); switch ($type) { case 'initiated': - $ids = InterestHelper::findInitiated($me->organisation_id); + $ids = InterestHelper::findInitiated($user->organisation_id); break; case 'received': - $ids = InterestHelper::findReceived($me->organisation_id); + $ids = InterestHelper::findReceived($user->organisation_id); break; default: diff --git a/app/Http/Controllers/V2/Applications/AdminIndexApplicationController.php b/app/Http/Controllers/V2/Applications/AdminIndexApplicationController.php index 7c914349d..191e16a6a 100644 --- a/app/Http/Controllers/V2/Applications/AdminIndexApplicationController.php +++ b/app/Http/Controllers/V2/Applications/AdminIndexApplicationController.php @@ -44,7 +44,7 @@ public function __invoke(Request $request): ApplicationsCollection } if (! empty($request->query('search'))) { - $ids = Application::search(trim($request->query('search')))->get()->pluck('id')->toArray(); + $ids = Application::searchApplications(trim($request->query('search')))->pluck('id')->toArray(); $qry->whereIn('id', $ids); } diff --git a/app/Http/Controllers/V2/AuditStatus/GetAuditStatusController.php b/app/Http/Controllers/V2/AuditStatus/GetAuditStatusController.php index 34218167c..13686ae5f 100644 --- a/app/Http/Controllers/V2/AuditStatus/GetAuditStatusController.php +++ b/app/Http/Controllers/V2/AuditStatus/GetAuditStatusController.php @@ -37,9 +37,13 @@ private function getAudits($auditable) } $audits = $auditable->audits() - ->orderBy('updated_at', 'desc') - ->orderBy('created_at', 'desc') - ->get(); + ->where(function ($query) { + $query->where('created_at', '<', '2024-09-01') + ->orWhere('updated_at', '<', '2024-09-01'); + }) + ->orderByDesc('updated_at') + ->orderByDesc('created_at') + ->get(); return $audits->map(function ($audit) { return AuditStatusDTO::fromAudits($audit); diff --git a/app/Http/Controllers/V2/AuditStatus/StoreAuditStatusController.php b/app/Http/Controllers/V2/AuditStatus/StoreAuditStatusController.php index 39cfd9349..d7a40a5c8 100644 --- a/app/Http/Controllers/V2/AuditStatus/StoreAuditStatusController.php +++ b/app/Http/Controllers/V2/AuditStatus/StoreAuditStatusController.php @@ -5,9 +5,12 @@ use App\Http\Controllers\Controller; use App\Http\Requests\V2\AuditStatus\AuditStatusCreateRequest; use App\Http\Resources\V2\AuditStatusResource; +use App\Jobs\V2\SendProjectManagerJob as SendProjectManagerJobs; use App\Models\Traits\SaveAuditStatusTrait; use App\Models\V2\AuditableModel; use App\Models\V2\AuditStatus\AuditStatus; +use App\Models\V2\Sites\Site; +use App\Models\V2\Sites\SitePolygon; class StoreAuditStatusController extends Controller { @@ -26,6 +29,13 @@ public function __invoke(AuditStatusCreateRequest $auditStatusCreateRequest, Aud $auditStatus = $this->saveAuditStatus(get_class($auditable), $auditable->id, $body['status'], $body['comment'], $body['type'], $body['is_active'], $body['request_removed']); } else { $auditStatus = $this->saveAuditStatus(get_class($auditable), $auditable->id, $body['status'], $body['comment'], $body['type']); + if ($body['type'] == 'comment' && get_class($auditable) != SitePolygon::class) { + SendProjectManagerJobs::dispatch($auditable); + } + if (get_class($auditable) === SitePolygon::class) { + $sitePolygon = Site::where('uuid', $auditable->site_id)->first(); + SendProjectManagerJobs::dispatch($sitePolygon); + } } $auditStatus->entity_name = $auditable->name; diff --git a/app/Http/Controllers/V2/Entities/AdminSendReminderController.php b/app/Http/Controllers/V2/Entities/AdminSendReminderController.php new file mode 100644 index 000000000..40640291b --- /dev/null +++ b/app/Http/Controllers/V2/Entities/AdminSendReminderController.php @@ -0,0 +1,24 @@ +validated(); + + SendReportReminderEmailsJob::dispatch($entity, data_get($data, 'feedback')); + $this->saveAuditStatusAdminSendReminder($entity, data_get($data, 'feedback')); + + return response()->json(['message' => 'Reminder sent successfully.']); + } +} diff --git a/app/Http/Controllers/V2/Entities/AdminStatusEntityController.php b/app/Http/Controllers/V2/Entities/AdminStatusEntityController.php index 4182c39b6..a0a39d560 100644 --- a/app/Http/Controllers/V2/Entities/AdminStatusEntityController.php +++ b/app/Http/Controllers/V2/Entities/AdminStatusEntityController.php @@ -4,12 +4,15 @@ use App\Http\Controllers\Controller; use App\Http\Requests\V2\UpdateRequests\StatusChangeRequest; +use App\Models\Traits\SaveAuditStatusTrait; use App\Models\V2\EntityModel; use App\Models\V2\Sites\Site; use Illuminate\Http\JsonResponse; class AdminStatusEntityController extends Controller { + use SaveAuditStatusTrait; + public function __invoke(StatusChangeRequest $request, EntityModel $entity, string $status) { $data = $request->validated(); @@ -18,17 +21,20 @@ public function __invoke(StatusChangeRequest $request, EntityModel $entity, stri switch($status) { case 'approve': $entity->approve(data_get($data, 'feedback')); + $this->saveAuditStatusAdminApprove($data, $entity); break; case 'moreinfo': $entity->needsMoreInformation(data_get($data, 'feedback'), data_get($data, 'feedback_fields')); + $this->saveAuditStatusAdminMoreInfo($data, $entity); break; case 'restoration-in-progress': if (get_class($entity) === Site::class) { $entity->restorationInProgress(); + $this->saveAuditStatusAdminRestorationInProgress($entity); break; } diff --git a/app/Http/Controllers/V2/Entities/SubmitEntityWithFormController.php b/app/Http/Controllers/V2/Entities/SubmitEntityWithFormController.php index 9ca1ebee1..eb8ea202e 100644 --- a/app/Http/Controllers/V2/Entities/SubmitEntityWithFormController.php +++ b/app/Http/Controllers/V2/Entities/SubmitEntityWithFormController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\V2\Entities; use App\Http\Controllers\Controller; +use App\Jobs\V2\SendProjectManagerJob as SendProjectManagerJobs; use App\Models\Traits\SaveAuditStatusTrait; use App\Models\V2\Action; use App\Models\V2\EntityModel; @@ -34,6 +35,7 @@ public function __invoke(EntityModel $entity, Request $request) $entity->submitForApproval(); } + SendProjectManagerJobs::dispatch($entity); Action::forTarget($entity)->delete(); return $entity->createSchemaResource(); diff --git a/app/Http/Controllers/V2/Exports/ExportImageController.php b/app/Http/Controllers/V2/Exports/ExportImageController.php new file mode 100644 index 000000000..455bfa119 --- /dev/null +++ b/app/Http/Controllers/V2/Exports/ExportImageController.php @@ -0,0 +1,27 @@ +validate([ + 'imageUrl' => 'required|url', + ]); + + $imageUrl = $request->input('imageUrl'); + + $imageContent = Http::get($imageUrl)->body(); + + $filename = basename($imageUrl); + + return response($imageContent) + ->header('Content-Type', 'image/jpeg') + ->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); + } +} diff --git a/app/Http/Controllers/V2/Files/Gallery/ViewNurseryGalleryController.php b/app/Http/Controllers/V2/Files/Gallery/ViewNurseryGalleryController.php index 4a100e466..2ddcb0149 100644 --- a/app/Http/Controllers/V2/Files/Gallery/ViewNurseryGalleryController.php +++ b/app/Http/Controllers/V2/Files/Gallery/ViewNurseryGalleryController.php @@ -19,6 +19,9 @@ public function __invoke(Request $request, Nursery $nursery): GallerysCollection $perPage = $request->query('per_page') ?? config('app.pagination_default', 15); $entity = $request->query('model_name'); + $searchTerm = $request->query('search'); + $isGeotagged = $request->query('is_geotagged'); + $sortOrder = $request->query('sort_order', 'asc'); $models = []; ! empty($entity) && $entity != 'nurseries' ?: $models[] = ['type' => get_class($nursery), 'ids' => [$nursery->id]]; @@ -33,11 +36,42 @@ public function __invoke(Request $request, Nursery $nursery): GallerysCollection } }); + if (! empty($searchTerm)) { + $mediaIds = Media::where('name', 'LIKE', "%{$searchTerm}%") + ->orWhere('file_name', 'LIKE', "%{$searchTerm}%") + ->pluck('id'); + $mediaQueryBuilder->whereIn('media.id', $mediaIds); + } + if ($isGeotagged === '1') { + $mediaQueryBuilder->whereNotNull('lat')->whereNotNull('lng'); + } elseif ($isGeotagged === '2') { + $mediaQueryBuilder->whereNull('lat')->whereNull('lng'); + } + + // Map model types to classes + $modelTypeMap = [ + 'nurseries' => [Nursery::class], + 'reports' => [NurseryReport::class], + ]; + $query = QueryBuilder::for($mediaQueryBuilder) ->allowedFilters([ AllowedFilter::exact('file_type'), AllowedFilter::exact('is_public'), - ]); + AllowedFilter::callback('model_type', function ($query, $value) use ($modelTypeMap) { + $classNames = $modelTypeMap[$value] ?? null; + if ($classNames) { + $query->where(function ($subQuery) use ($classNames) { + foreach ($classNames as $className) { + $subQuery->orWhere('model_type', $className); + } + }); + } + }), + ]) + ->allowedSorts(['created_at']); + + $query->orderBy('created_at', $sortOrder); $collection = $query->paginate($perPage) ->appends(request()->query()); diff --git a/app/Http/Controllers/V2/Files/Gallery/ViewProjectGalleryController.php b/app/Http/Controllers/V2/Files/Gallery/ViewProjectGalleryController.php index 3e6df332c..419947b93 100644 --- a/app/Http/Controllers/V2/Files/Gallery/ViewProjectGalleryController.php +++ b/app/Http/Controllers/V2/Files/Gallery/ViewProjectGalleryController.php @@ -19,37 +19,88 @@ class ViewProjectGalleryController extends Controller { public function __invoke(Request $request, Project $project): GallerysCollection { - $this->authorize('read', $project); - - $perPage = $request->query('per_page') ?? config('app.pagination_default', 15); - $entity = $request->query('model_name'); - - $models = []; - ! empty($entity) && $entity != 'projects' ?: $models[] = ['type' => get_class($project), 'ids' => [$project->id]]; - ! empty($entity) && $entity != 'sites' ?: $models[] = ['type' => Site::class, 'ids' => $project->sites->pluck('id')->toArray()]; - ! empty($entity) && $entity != 'nurseries' ?: $models[] = ['type' => Nursery::class, 'ids' => $project->nurseries->pluck('id')->toArray()]; - ! empty($entity) && $entity != 'project-reports' ?: $models[] = ['type' => ProjectReport::class, 'ids' => $project->reports->pluck('id')->toArray()]; - ! empty($entity) && $entity != 'site-reports' ?: $models[] = ['type' => SiteReport::class, 'ids' => $project->siteReports->pluck('id')->toArray()]; - ! empty($entity) && $entity != 'nursery-reports' ?: $models[] = ['type' => NurseryReport::class, 'ids' => $project->nurseryReports->pluck('id')->toArray()]; - - $mediaQueryBuilder = Media::query()->where(function ($query) use ($models) { - foreach ($models as $model) { - $query->orWhere(function ($query) use ($model) { - $query->where('model_type', $model['type']) - ->whereIn('model_id', $model['ids']); - }); + try { + $this->authorize('read', $project); + + $perPage = $request->query('per_page') ?? config('app.pagination_default', 15); + $entity = $request->query('model_name'); + $searchTerm = $request->query('search'); + $isGeotagged = $request->query('is_geotagged'); + $sortOrder = $request->query('sort_order', 'asc'); + $models = []; + ! empty($entity) && $entity != 'projects' ?: $models[] = ['type' => get_class($project), 'ids' => [$project->id]]; + ! empty($entity) && $entity != 'sites' ?: $models[] = ['type' => Site::class, 'ids' => $project->sites->pluck('id')->toArray()]; + ! empty($entity) && $entity != 'nurseries' ?: $models[] = ['type' => Nursery::class, 'ids' => $project->nurseries->pluck('id')->toArray()]; + ! empty($entity) && $entity != 'project-reports' ?: $models[] = ['type' => ProjectReport::class, 'ids' => $project->reports->pluck('id')->toArray()]; + ! empty($entity) && $entity != 'site-reports' ?: $models[] = ['type' => SiteReport::class, 'ids' => $project->siteReports->pluck('id')->toArray()]; + ! empty($entity) && $entity != 'nursery-reports' ?: $models[] = ['type' => NurseryReport::class, 'ids' => $project->nurseryReports->pluck('id')->toArray()]; + + $mediaQueryBuilder = Media::query()->where(function ($query) use ($models) { + foreach ($models as $model) { + $query->orWhere(function ($query) use ($model) { + $query->where('model_type', $model['type']) + ->whereIn('model_id', $model['ids']); + }); + } + }); + + if (! empty($searchTerm)) { + $mediaIds = Media::where('name', 'LIKE', "%{$searchTerm}%") + ->orWhere('file_name', 'LIKE', "%{$searchTerm}%") + ->pluck('id'); + + $siteIds = Site::where('name', 'LIKE', "%{$searchTerm}%")->pluck('id'); + $nurseryIds = Nursery::where('name', 'LIKE', "%{$searchTerm}%")->pluck('id'); + + $additionalMediaIds = Media::whereIn('model_type', [Site::class, Nursery::class]) + ->whereIn('model_id', $siteIds->merge($nurseryIds)) + ->pluck('id'); + + $allMediaIds = $mediaIds->merge($additionalMediaIds)->unique(); + + $mediaQueryBuilder->whereIn('media.id', $allMediaIds); } - }); - $query = QueryBuilder::for($mediaQueryBuilder) - ->allowedFilters([ - AllowedFilter::exact('file_type'), - AllowedFilter::exact('is_public'), - ]); + if ($isGeotagged === '1') { + $mediaQueryBuilder->whereNotNull('lat')->whereNotNull('lng'); + } elseif ($isGeotagged === '2') { + $mediaQueryBuilder->whereNull('lat')->whereNull('lng'); + } + + $modelTypeMap = [ + 'projects' => [Project::class], + 'sites' => [Site::class], + 'nurseries' => [Nursery::class], + 'project-reports' => [ProjectReport::class], + 'site-reports' => [SiteReport::class], + 'nursery-reports' => [NurseryReport::class], + 'reports' => [ProjectReport::class, SiteReport::class, NurseryReport::class], + ]; + + $query = QueryBuilder::for($mediaQueryBuilder) + ->allowedFilters([ + AllowedFilter::exact('file_type'), + AllowedFilter::exact('is_public'), + AllowedFilter::callback('model_type', function ($query, $value) use ($modelTypeMap) { + $classNames = $modelTypeMap[$value] ?? null; + if ($classNames) { + $query->where(function ($subQuery) use ($classNames) { + foreach ($classNames as $className) { + $subQuery->orWhere('model_type', $className); + } + }); + } + }), + ]) + ->allowedSorts(['created_at']); + $query->orderBy('created_at', $sortOrder); - $collection = $query->paginate($perPage) - ->appends(request()->query()); + $collection = $query->paginate($perPage) + ->appends(request()->query()); - return new GallerysCollection($collection); + return new GallerysCollection($collection); + } catch(\Exception $e) { + return response()->json(['error' => $e->getMessage()], 500); + } } } diff --git a/app/Http/Controllers/V2/Files/Gallery/ViewSiteGalleryController.php b/app/Http/Controllers/V2/Files/Gallery/ViewSiteGalleryController.php index cf4abf10e..65ee05fb6 100644 --- a/app/Http/Controllers/V2/Files/Gallery/ViewSiteGalleryController.php +++ b/app/Http/Controllers/V2/Files/Gallery/ViewSiteGalleryController.php @@ -19,6 +19,9 @@ public function __invoke(Request $request, Site $site): GallerysCollection $perPage = $request->query('per_page') ?? config('app.pagination_default', 15); $entity = $request->query('model_name'); + $searchTerm = $request->query('search'); + $isGeotagged = $request->query('is_geotagged'); + $sortOrder = $request->query('sort_order', 'asc'); $models = []; ! empty($entity) && $entity != 'sites' ?: $models[] = ['type' => get_class($site), 'ids' => [$site->id]]; @@ -33,11 +36,42 @@ public function __invoke(Request $request, Site $site): GallerysCollection } }); + if (! empty($searchTerm)) { + $mediaIds = Media::where('name', 'LIKE', "%{$searchTerm}%") + ->orWhere('file_name', 'LIKE', "%{$searchTerm}%") + ->pluck('id'); + $mediaQueryBuilder->whereIn('media.id', $mediaIds); + } + if ($isGeotagged === '1') { + $mediaQueryBuilder->whereNotNull('lat')->whereNotNull('lng'); + } elseif ($isGeotagged === '2') { + $mediaQueryBuilder->whereNull('lat')->whereNull('lng'); + } + + // Map model types to classes + $modelTypeMap = [ + 'sites' => [Site::class], + 'reports' => [SiteReport::class], + ]; + $query = QueryBuilder::for($mediaQueryBuilder) ->allowedFilters([ AllowedFilter::exact('file_type'), AllowedFilter::exact('is_public'), - ]); + AllowedFilter::callback('model_type', function ($query, $value) use ($modelTypeMap) { + $classNames = $modelTypeMap[$value] ?? null; + if ($classNames) { + $query->where(function ($subQuery) use ($classNames) { + foreach ($classNames as $className) { + $subQuery->orWhere('model_type', $className); + } + }); + } + }), + ]) + ->allowedSorts(['created_at']); + + $query->orderBy('created_at', $sortOrder); $collection = $query->paginate($perPage) ->appends(request()->query()); diff --git a/app/Http/Controllers/V2/Files/UploadController.php b/app/Http/Controllers/V2/Files/UploadController.php index 7a00279cb..400e52a33 100644 --- a/app/Http/Controllers/V2/Files/UploadController.php +++ b/app/Http/Controllers/V2/Files/UploadController.php @@ -134,6 +134,7 @@ private function saveAdditionalFileProperties($media, $data, $config) $media->file_type = $this->getType($media, $config); $media->is_public = $data['is_public'] ?? true; $media->created_by = Auth::user()->id; + $media->photographer = Auth::user()->name; $media->save(); } diff --git a/app/Http/Controllers/V2/Forms/UpdateFormSubmissionStatusController.php b/app/Http/Controllers/V2/Forms/UpdateFormSubmissionStatusController.php index f0f6cbb05..edc79de95 100644 --- a/app/Http/Controllers/V2/Forms/UpdateFormSubmissionStatusController.php +++ b/app/Http/Controllers/V2/Forms/UpdateFormSubmissionStatusController.php @@ -12,12 +12,15 @@ use App\Models\Framework; use App\Models\Notification; use App\Models\V2\Forms\FormSubmission; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Mail; class UpdateFormSubmissionStatusController extends Controller { public function __invoke(FormSubmission $formSubmission, UpdateFormSubmissionStatusRequest $updateFormSubmissionStatusRequest): FormSubmissionResource { + $user = Auth::user(); + $formSubmission->update([ 'status' => $updateFormSubmissionStatusRequest->status, 'feedback' => $updateFormSubmissionStatusRequest->feedback, @@ -27,7 +30,7 @@ public function __invoke(FormSubmission $formSubmission, UpdateFormSubmissionSta switch ($updateFormSubmissionStatusRequest->status) { case FormSubmission::STATUS_REQUIRES_MORE_INFORMATION: Mail::to($formSubmission->user->email_address)->queue( - new FormSubmissionFeedbackReceived(data_get($updateFormSubmissionStatusRequest, 'feedback', null)) + new FormSubmissionFeedbackReceived(data_get($updateFormSubmissionStatusRequest, 'feedback', null), $user) ); $notification = new Notification([ 'user_id' => $formSubmission->user->id, @@ -42,7 +45,7 @@ public function __invoke(FormSubmission $formSubmission, UpdateFormSubmissionSta break; case FormSubmission::STATUS_REJECTED: Mail::to($formSubmission->user->email_address)->queue( - new FormSubmissionRejected(data_get($updateFormSubmissionStatusRequest, 'feedback', null)) + new FormSubmissionRejected(data_get($updateFormSubmissionStatusRequest, 'feedback', null), $user) ); $notification = new Notification([ 'user_id' => $formSubmission->user->id, @@ -60,14 +63,14 @@ public function __invoke(FormSubmission $formSubmission, UpdateFormSubmissionSta $framework = Framework::where('name', 'Terrafund')->first(); if ($framework) { Mail::to($formSubmission->user->email_address)->queue( - new FormSubmissionFinalStageApproved(data_get($updateFormSubmissionStatusRequest, 'feedback', null)) + new FormSubmissionFinalStageApproved(data_get($updateFormSubmissionStatusRequest, 'feedback', null), $user) ); $formSubmission->user->frameworks()->syncWithoutDetaching([$framework->id]); } } else { Mail::to($formSubmission->user->email_address)->queue( - new FormSubmissionApproved(data_get($updateFormSubmissionStatusRequest, 'feedback', null)) + new FormSubmissionApproved(data_get($updateFormSubmissionStatusRequest, 'feedback', null), $user) ); } diff --git a/app/Http/Controllers/V2/Geometry/GeometryController.php b/app/Http/Controllers/V2/Geometry/GeometryController.php index 57f8c3815..736618706 100644 --- a/app/Http/Controllers/V2/Geometry/GeometryController.php +++ b/app/Http/Controllers/V2/Geometry/GeometryController.php @@ -15,6 +15,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; use stdClass; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -61,30 +62,92 @@ protected function storeAndValidateGeometries($geometries): array /** @var PolygonService $service */ $service = App::make(PolygonService::class); $results = []; - foreach ($geometries as $geometry) { - $results[] = ['polygon_uuids' => $service->createGeojsonModels($geometry, ['source' => PolygonService::GREENHOUSE_SOURCE])]; - } - - // Do the validation in a separate step so that all of the existing polygons are taken into account - // for things like overlapping and estimated area. - foreach ($results as $index => $result) { - $polygonErrors = []; - foreach ($result['polygon_uuids'] as $polygonUuid) { - $validationErrors = $this->runStoredGeometryValidations($polygonUuid); - $estAreaErrors = $this->runStoredGeometryEstAreaValidation($polygonUuid); - $allErrors = array_merge($validationErrors, $estAreaErrors); - if (! empty($allErrors)) { - $polygonErrors[$polygonUuid] = $allErrors; - } - } + $groupedGeometries = $this->groupGeometriesBySiteId($geometries); + + foreach ($groupedGeometries as $siteId => $siteGeometries) { + $groupedByType = $this->groupGeometriesByType($siteGeometries); - // Send an empty object instead of empty array if there are no errors to keep the response shape consistent. - data_set($results, "$index.errors", empty($polygonErrors) ? new stdClass() : $polygonErrors); + foreach ($groupedByType as $type => $typeGeometries) { + $polygonUuids = $service->createGeojsonModels($typeGeometries, ['source' => PolygonService::GREENHOUSE_SOURCE]); + $polygonErrors = $this->validateStoredGeometries($polygonUuids); + + $results[] = [ + 'site_id' => $siteId, + 'geometry_type' => $type, + 'polygon_uuids' => $polygonUuids, + 'errors' => empty($polygonErrors) ? new stdClass() : $polygonErrors, + ]; + } } return $results; } + protected function validateStoredGeometries(array $polygonUuids): array + { + $polygonErrors = []; + + foreach ($polygonUuids as $polygonUuid) { + $validationErrors = $this->runStoredGeometryValidations($polygonUuid); + $estAreaErrors = $this->runStoredGeometryEstAreaValidation($polygonUuid); + $allErrors = array_merge($validationErrors, $estAreaErrors); + + if (! empty($allErrors)) { + $polygonErrors[$polygonUuid] = $allErrors; + } + } + + return $polygonErrors; + } + + protected function groupGeometriesByType(array $siteGeometries): array + { + $groupedByType = []; + + foreach ($siteGeometries['features'] as $feature) { + $geometryType = data_get($feature, 'geometry.type'); + + if (! isset($groupedByType[$geometryType])) { + $groupedByType[$geometryType] = [ + 'type' => 'FeatureCollection', + 'features' => [], + ]; + } + + $groupedByType[$geometryType]['features'][] = $feature; + } + + return $groupedByType; + } + + protected function groupGeometriesBySiteId(array $geometries): array + { + $grouped = []; + + foreach ($geometries as $geometryCollection) { + if (! isset($geometryCollection['features'])) { + Log::warning('No features found in this geometry collection', $geometryCollection); + + continue; // Skip if there are no features + } + + foreach ($geometryCollection['features'] as $feature) { + $siteId = data_get($feature, 'properties.site_id'); + + if (! isset($grouped[$siteId])) { + $grouped[$siteId] = [ + 'type' => 'FeatureCollection', + 'features' => [], + ]; + } + + $grouped[$siteId]['features'][] = $feature; + } + } + + return $grouped; + } + public function validateGeometries(Request $request): JsonResponse { $request->validate([ diff --git a/app/Http/Controllers/V2/MediaController.php b/app/Http/Controllers/V2/MediaController.php index 0f06266c2..607ba26da 100644 --- a/app/Http/Controllers/V2/MediaController.php +++ b/app/Http/Controllers/V2/MediaController.php @@ -3,11 +3,19 @@ namespace App\Http\Controllers\V2; use App\Http\Controllers\Controller; +use App\Http\Resources\V2\Files\FileResource; +use App\Models\V2\Nurseries\Nursery; +use App\Models\V2\Nurseries\NurseryReport; +use App\Models\V2\Projects\Project; +use App\Models\V2\Projects\ProjectReport; +use App\Models\V2\Sites\Site; +use App\Models\V2\Sites\SiteReport; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Spatie\MediaLibrary\MediaCollections\Models\Media; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -57,4 +65,77 @@ public function bulkDelete(Request $request): JsonResponse return response()->json(['success' => 'media has been deleted'], 202); } + + public function updateMedia(Request $request, string $uuid): JsonResponse + { + $media = Media::where('uuid', $uuid)->first(); + + DB::transaction(function () use ($media, $request) { + $updateData = []; + + if ($request->has('name')) { + $updateData['name'] = $request->input('name'); + } + if ($request->has('description')) { + $updateData['description'] = $request->input('description'); + } + + if ($request->has('photographer')) { + $updateData['photographer'] = $request->input('photographer'); + } + + if ($request->has('is_public')) { + $updateData['is_public'] = $request->input('is_public'); + } + + if (! empty($updateData)) { + $media->update($updateData); + } + }); + + return response()->json(new FileResource($media), 200); + } + + public function updateIsCover(Request $request, Project $project, string $mediaUuid): JsonResponse + { + try { + $this->authorize('read', $project); + + DB::transaction(function () use ($project, $mediaUuid) { + $this->resetCoverForProjectMedia($project); + + $media = Media::where('uuid', $mediaUuid)->firstOrFail(); + $media->update(['is_cover' => true]); + }); + + return response()->json(['message' => 'Cover image updated successfully', 'mediaUuid' => $mediaUuid], 200); + } catch (ModelNotFoundException $e) { + return response()->json(['message' => 'Media not found'], 404); + } + } + + private function resetCoverForProjectMedia(Project $project) + { + $relatedModelTypes = [ + Project::class, + Site::class, + Nursery::class, + ProjectReport::class, + SiteReport::class, + NurseryReport::class, + ]; + + $relatedModelIds = collect([ + $project->id, + $project->sites->pluck('id'), + $project->nurseries->pluck('id'), + $project->reports->pluck('id'), + $project->siteReports->pluck('id'), + $project->nurseryReports->pluck('id'), + ])->flatten()->toArray(); + + Media::whereIn('model_type', $relatedModelTypes) + ->whereIn('model_id', $relatedModelIds) + ->update(['is_cover' => false]); + } } diff --git a/app/Http/Controllers/V2/Nurseries/AdminIndexNurseriesController.php b/app/Http/Controllers/V2/Nurseries/AdminIndexNurseriesController.php index 9fa510528..30bc9f7be 100644 --- a/app/Http/Controllers/V2/Nurseries/AdminIndexNurseriesController.php +++ b/app/Http/Controllers/V2/Nurseries/AdminIndexNurseriesController.php @@ -49,7 +49,7 @@ public function __invoke(Request $request): NurseriesCollection ]); if (! empty($request->query('search'))) { - $ids = Nursery::search(trim($request->query('search')))->get()->pluck('id')->toArray(); + $ids = Nursery::searchNurseries(trim($request->query('search')))->pluck('id')->toArray(); $query->whereIn('v2_nurseries.id', $ids); } diff --git a/app/Http/Controllers/V2/Organisations/AdminOrganisationController.php b/app/Http/Controllers/V2/Organisations/AdminOrganisationController.php index 2cd9519fe..554353108 100644 --- a/app/Http/Controllers/V2/Organisations/AdminOrganisationController.php +++ b/app/Http/Controllers/V2/Organisations/AdminOrganisationController.php @@ -40,7 +40,7 @@ public function index(Request $request): OrganisationsCollection } if ($request->query('search')) { - $ids = Organisation::search(trim($request->query('search')))->get()->pluck('id')->toArray(); + $ids = Organisation::searchOrganisations(trim($request->query('search')))->pluck('id')->toArray(); if (empty($ids)) { return new OrganisationsCollection([]); diff --git a/app/Http/Controllers/V2/Organisations/OrganisationListingController.php b/app/Http/Controllers/V2/Organisations/OrganisationListingController.php index 564707f39..499c9fa85 100644 --- a/app/Http/Controllers/V2/Organisations/OrganisationListingController.php +++ b/app/Http/Controllers/V2/Organisations/OrganisationListingController.php @@ -14,9 +14,9 @@ public function __invoke(Request $request): ListingCollection $this->authorize('listing', Organisation::class); if (! empty($request->query('search'))) { - $qry = Organisation::search($request->query('search')) + $qry = Organisation::searchOrganisations($request->query('search')) ->where('status', Organisation::STATUS_APPROVED); - $qry->limit = 25; + $qry->limit(25); } else { $qry = Organisation::isStatus(Organisation::STATUS_APPROVED) ->limit(25); diff --git a/app/Http/Controllers/V2/Polygons/ViewSitesPolygonsForProjectController.php b/app/Http/Controllers/V2/Polygons/ViewSitesPolygonsForProjectController.php index 7b37d13ea..13ba31604 100644 --- a/app/Http/Controllers/V2/Polygons/ViewSitesPolygonsForProjectController.php +++ b/app/Http/Controllers/V2/Polygons/ViewSitesPolygonsForProjectController.php @@ -19,8 +19,8 @@ public function __invoke(Request $request, Project $project): GeojsonCollection if (empty($request->query('search'))) { $qry = Site::whereIn('id', $siteIds); } else { - $qry = Site::search(trim($request->query('search'))) - ->whereIn('id', $siteIds); + $qry = Site::searchSites(trim($request->query('search'))) + ->whereIn('v2_sites.id', $siteIds); } $qry->orderBy('created_at', 'desc'); diff --git a/app/Http/Controllers/V2/ProjectPitches/AdminIndexProjectPitchController.php b/app/Http/Controllers/V2/ProjectPitches/AdminIndexProjectPitchController.php index 6bb7e5af5..4b504f8df 100644 --- a/app/Http/Controllers/V2/ProjectPitches/AdminIndexProjectPitchController.php +++ b/app/Http/Controllers/V2/ProjectPitches/AdminIndexProjectPitchController.php @@ -38,8 +38,7 @@ public function __invoke(Request $request): ProjectPitchesCollection } if ($request->query('search')) { - $ids = ProjectPitch::search(trim($request->get('search')) ?? '') - ->get() + $ids = ProjectPitch::searchProjectPitches(trim($request->get('search')) ?? '') ->pluck('id') ->toArray(); diff --git a/app/Http/Controllers/V2/ProjectPitches/IndexProjectPitchController.php b/app/Http/Controllers/V2/ProjectPitches/IndexProjectPitchController.php index 45716422d..9793df3e6 100644 --- a/app/Http/Controllers/V2/ProjectPitches/IndexProjectPitchController.php +++ b/app/Http/Controllers/V2/ProjectPitches/IndexProjectPitchController.php @@ -41,8 +41,7 @@ public function __invoke(Request $request): ProjectPitchesCollection ]); if ($request->query('search')) { - $ids = ProjectPitch::search(trim($request->get('search')) ?? '') - ->get() + $ids = ProjectPitch::searchProjectPitches(trim($request->get('search')) ?? '') ->pluck('id') ->toArray(); diff --git a/app/Http/Controllers/V2/Projects/AdminIndexProjectsController.php b/app/Http/Controllers/V2/Projects/AdminIndexProjectsController.php index 1edfd2eeb..ff07b2feb 100644 --- a/app/Http/Controllers/V2/Projects/AdminIndexProjectsController.php +++ b/app/Http/Controllers/V2/Projects/AdminIndexProjectsController.php @@ -44,7 +44,7 @@ public function __invoke(Request $request): ProjectsCollection ]); if (! empty($request->query('search'))) { - $ids = Project::search(trim($request->query('search')))->get()->pluck('id')->toArray(); + $ids = Project::searchProjects(trim($request->query('search')))->pluck('id')->toArray(); $query->whereIn('v2_projects.id', $ids); } diff --git a/app/Http/Controllers/V2/Projects/CreateProjectInviteController.php b/app/Http/Controllers/V2/Projects/CreateProjectInviteController.php index 0e7743019..2c4dbf946 100644 --- a/app/Http/Controllers/V2/Projects/CreateProjectInviteController.php +++ b/app/Http/Controllers/V2/Projects/CreateProjectInviteController.php @@ -42,7 +42,7 @@ public function __invoke(CreateProjectInviteRequest $request, Project $project): $existingUser->update(['organisation_id' => $project->organisation_id]); } $url = str_replace('/reset-password', '/login', $url); - Mail::to($data['email_address'])->queue(new V2ProjectMonitoringNotification($project->name, $url)); + Mail::to($data['email_address'])->queue(new V2ProjectMonitoringNotification($project->name, $url, $existingUser)); } else { $user = User::create([ 'email_address' => $data['email_address'], @@ -55,7 +55,7 @@ public function __invoke(CreateProjectInviteRequest $request, Project $project): $organisation = Organisation::where('id', $project->organisation_id)->first(); $projectInvite = $project->invites()->create($data); - Mail::to($data['email_address'])->queue(new V2ProjectInviteReceived($project->name, $organisation->name, $url)); + Mail::to($data['email_address'])->queue(new V2ProjectInviteReceived($project->name, $organisation->name, $url, $user)); } return new ProjectInviteResource($projectInvite); diff --git a/app/Http/Controllers/V2/Projects/ViewProjectNurseriesController.php b/app/Http/Controllers/V2/Projects/ViewProjectNurseriesController.php index 1c0f72aaa..ea86c6c93 100644 --- a/app/Http/Controllers/V2/Projects/ViewProjectNurseriesController.php +++ b/app/Http/Controllers/V2/Projects/ViewProjectNurseriesController.php @@ -36,7 +36,7 @@ public function __invoke(Request $request, Project $project): NurseriesCollectio } if (! empty($request->query('search'))) { - $ids = Nursery::search(trim($request->query('search')))->get()->pluck('id')->toArray(); + $ids = Nursery::searchNurseries(trim($request->query('search')))->pluck('id')->toArray(); if (empty($ids)) { return new NurseriesCollection(collect()); diff --git a/app/Http/Controllers/V2/Projects/ViewProjectSitesController.php b/app/Http/Controllers/V2/Projects/ViewProjectSitesController.php index 198d774fe..5f47dabcc 100644 --- a/app/Http/Controllers/V2/Projects/ViewProjectSitesController.php +++ b/app/Http/Controllers/V2/Projects/ViewProjectSitesController.php @@ -52,7 +52,7 @@ public function __invoke(Request $request, Project $project) if (is_numeric($search)) { $qry->where('v2_sites.ppc_external_id', $search); } else { - $ids = Site::search(trim($request->query('search'))) + $ids = Site::searchSites(trim($request->query('search'))) ->get() ->pluck('id') ->toArray(); diff --git a/app/Http/Controllers/V2/Sites/AdminIndexSitesController.php b/app/Http/Controllers/V2/Sites/AdminIndexSitesController.php index 2f5783411..c15354b1e 100644 --- a/app/Http/Controllers/V2/Sites/AdminIndexSitesController.php +++ b/app/Http/Controllers/V2/Sites/AdminIndexSitesController.php @@ -58,7 +58,7 @@ public function __invoke(Request $request): V2SitesCollection if (is_numeric($search)) { $query->where('v2_sites.ppc_external_id', $search); } else { - $ids = Site::search($search)->get()->pluck('id')->toArray(); + $ids = Site::searchSites($search)->pluck('id')->toArray(); $query->whereIn('v2_sites.id', $ids); } } diff --git a/app/Http/Controllers/V2/Terrafund/TerrafundCreateGeometryController.php b/app/Http/Controllers/V2/Terrafund/TerrafundCreateGeometryController.php index 001c9cc73..f00e405ec 100755 --- a/app/Http/Controllers/V2/Terrafund/TerrafundCreateGeometryController.php +++ b/app/Http/Controllers/V2/Terrafund/TerrafundCreateGeometryController.php @@ -5,6 +5,7 @@ use App\Helpers\GeometryHelper; use App\Http\Controllers\Controller; use App\Models\V2\PolygonGeometry; +use App\Models\V2\Sites\Site; use App\Models\V2\Sites\SitePolygon; use App\Models\V2\WorldCountryGeneralized; use App\Services\PolygonService; @@ -21,6 +22,7 @@ use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response as HttpResponse; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; @@ -90,7 +92,7 @@ public function insertGeojsonToDB(string $geojsonFilename, ?string $entity_uuid } } catch (Exception $e) { - return ['error' => $e->getMessage()]; + return response()->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR); } } @@ -363,7 +365,6 @@ public function uploadShapefile(Request $request) { ini_set('max_execution_time', self::MAX_EXECUTION_TIME); ini_set('memory_limit', '-1'); - Log::debug('Upload Shape file data', ['request' => $request->all()]); $rules = [ 'file' => 'required|file|mimes:zip', ]; @@ -375,62 +376,67 @@ public function uploadShapefile(Request $request) return response()->json(['errors' => $errors], 422); } - $body = $request->all(); - $site_id = $request->input('uuid'); - $file = $request->file('file'); - if ($file->getClientOriginalExtension() !== 'zip') { - return response()->json(['error' => 'Only ZIP files are allowed'], 400); - } - $tempDir = sys_get_temp_dir(); - $directory = $tempDir . DIRECTORY_SEPARATOR . uniqid('shapefile_'); - mkdir($directory, 0755, true); - // Extract the contents of the ZIP file - $zip = new \ZipArchive(); - if ($zip->open($file->getPathname()) === true) { - $zip->extractTo($directory); - $zip->close(); - $shpFile = $this->findShpFile($directory); - if (! $shpFile) { - return response()->json(['error' => 'Shapefile (.shp) not found in the ZIP file'], 400); - } - $geojsonFilename = Str::replaceLast('.shp', '.geojson', basename($shpFile)); - $geojsonPath = $tempDir . DIRECTORY_SEPARATOR . $geojsonFilename; - $process = new Process(['ogr2ogr', '-f', 'GeoJSON', $geojsonPath, $shpFile]); - $process->run(); - if (! $process->isSuccessful()) { - Log::error('Error converting Shapefile to GeoJSON: ' . $process->getErrorOutput()); - return response()->json(['error' => 'Failed to convert Shapefile to GeoJSON', 'message' => $process->getErrorOutput()], 500); + try { + $body = $request->all(); + $site_id = $request->input('uuid'); + $file = $request->file('file'); + if ($file->getClientOriginalExtension() !== 'zip') { + return response()->json(['error' => 'Only ZIP files are allowed'], 400); } + $tempDir = sys_get_temp_dir(); + $directory = $tempDir . DIRECTORY_SEPARATOR . uniqid('shapefile_'); + mkdir($directory, 0755, true); + // Extract the contents of the ZIP file + $zip = new \ZipArchive(); + if ($zip->open($file->getPathname()) === true) { + $zip->extractTo($directory); + $zip->close(); + $shpFile = $this->findShpFile($directory); + if (! $shpFile) { + return response()->json(['error' => 'Shapefile (.shp) not found in the ZIP file'], 400); + } + $geojsonFilename = Str::replaceLast('.shp', '.geojson', basename($shpFile)); + $geojsonPath = $tempDir . DIRECTORY_SEPARATOR . $geojsonFilename; + $process = new Process(['ogr2ogr', '-f', 'GeoJSON', $geojsonPath, $shpFile]); + $process->run(); + if (! $process->isSuccessful()) { + Log::error('Error converting Shapefile to GeoJSON: ' . $process->getErrorOutput()); + + return response()->json(['error' => 'Failed to convert Shapefile to GeoJSON', 'message' => $process->getErrorOutput()], 500); + } - $polygonLoadedList = isset($body['polygon_loaded']) && filter_var($body['polygon_loaded'], FILTER_VALIDATE_BOOLEAN); - $submitPolygonsLoaded = isset($body['submit_polygon_loaded']) && filter_var($body['submit_polygon_loaded'], FILTER_VALIDATE_BOOLEAN); + $polygonLoadedList = isset($body['polygon_loaded']) && filter_var($body['polygon_loaded'], FILTER_VALIDATE_BOOLEAN); + $submitPolygonsLoaded = $request->input('submit_polygon_loaded') === true || $request->input('submit_polygon_loaded') === 'true'; + if (! $polygonLoadedList && ! $submitPolygonsLoaded) { + $uuid = $this->insertGeojsonToDB($geojsonFilename, $site_id, 'site', $body['primary_uuid'] ?? null); + } - if (! $polygonLoadedList && ! $submitPolygonsLoaded) { - $uuid = $this->insertGeojsonToDB($geojsonFilename, $site_id, 'site', $body['primary_uuid'] ?? null); - } + if ($polygonLoadedList) { + $filePath = $tempDir . DIRECTORY_SEPARATOR . $geojsonFilename; + $geojsonContent = file_get_contents($filePath); + $polygonLoaded = $this->GetAllPolygonsLoaded($geojsonContent, $site_id); - if ($polygonLoadedList) { - $filePath = $tempDir . DIRECTORY_SEPARATOR . $geojsonFilename; - $geojsonContent = file_get_contents($filePath); - $polygonLoaded = $this->GetAllPolygonsLoaded($geojsonContent, $site_id); + return response()->json($polygonLoaded->original, 200); + } - return response()->json($polygonLoaded->original, 200); - } + if ($submitPolygonsLoaded) { + $uuid = $this->insertGeojsonToDB($geojsonFilename, $site_id, 'site', $body['primary_uuid'] ?? null, $body['submit_polygon_loaded']); + } - if ($submitPolygonsLoaded) { - $uuid = $this->insertGeojsonToDB($geojsonFilename, $site_id, 'site', $body['primary_uuid'] ?? null, $body['submit_polygon_loaded']); - } + if (isset($uuid['error'])) { + return response()->json(['error' => 'Geometry not inserted into DB', 'message' => $uuid['error']], 500); + } + App::make(SiteService::class)->setSiteToRestorationInProgress($site_id); - if (isset($uuid['error'])) { - return response()->json(['error' => 'Geometry not inserted into DB', 'message' => $uuid['error']], 500); + return response()->json(['message' => 'Shape file processed and inserted successfully', 'uuid' => $uuid], 200); + } else { + return response()->json(['error' => 'Failed to open the ZIP file'], 400); } - App::make(SiteService::class)->setSiteToRestorationInProgress($site_id); - - return response()->json(['message' => 'Shape file processed and inserted successfully', 'uuid' => $uuid], 200); - } else { - return response()->json(['error' => 'Failed to open the ZIP file'], 400); + } catch (Exception $e) { + return response()->json(['error' => $e->getMessage()], HttpResponse::HTTP_INTERNAL_SERVER_ERROR); } + } public function checkSelfIntersection(Request $request) @@ -893,6 +899,28 @@ public function validateEstimatedArea(Request $request) ); } + public function validateEstimatedAreaProject(Request $request) + { + $uuid = $request->input('uuid'); + + return $this->handlePolygonValidation( + $uuid, + EstimatedArea::getAreaDataProject($uuid), + PolygonService::ESTIMATED_AREA_CRITERIA_ID + ); + } + + public function validateEstimatedAreaSite(Request $request) + { + $uuid = $request->input('uuid'); + + return $this->handlePolygonValidation( + $uuid, + EstimatedArea::getAreaDataSite($uuid), + PolygonService::ESTIMATED_AREA_CRITERIA_ID + ); + } + public function validateCoordinateSystem(Request $request) { $uuid = $request->input('uuid'); @@ -1005,6 +1033,89 @@ public function getAllPolygonsAsGeoJSONDownload(Request $request) } } + public function downloadAllActivePolygonsByFramework(Request $request) + { + ini_set('max_execution_time', '-1'); + ini_set('memory_limit', '-1'); + $framework = $request->query('framework'); + + try { + $sitesFromFramework = Site::where('framework_key', $framework)->pluck('uuid'); + + $activePolygonIds = SitePolygon::wherein('site_id', $sitesFromFramework)->active()->pluck('poly_id'); + Log::info('count of active polygons: ', ['count' => count($activePolygonIds)]); + $features = []; + foreach ($activePolygonIds as $polygonUuid) { + + $polygonGeometry = PolygonGeometry::where('uuid', $polygonUuid) + ->select(DB::raw('ST_AsGeoJSON(geom) AS geojsonGeom')) + ->first(); + + if (! $polygonGeometry) { + Log::warning('No geometry found for Polygon UUID:', ['uuid' => $polygonUuid]); + + continue; + } + $sitePolygon = SitePolygon::where('poly_id', $polygonUuid)->first(); + $properties = $sitePolygon ? $sitePolygon->only(['poly_name', 'plantstart', 'plantend', 'practice', 'target_sys', 'distr', 'num_trees', 'site_id', 'uuid']) : []; + $feature = [ + 'type' => 'Feature', + 'geometry' => json_decode($polygonGeometry->geojsonGeom), + 'properties' => $properties, + ]; + $features[] = $feature; + } + $featureCollection = [ + 'type' => 'FeatureCollection', + 'features' => $features, + ]; + + return response()->json($featureCollection); + } catch (\Exception $e) { + return response()->json(['message' => 'Failed to generate GeoJSON.', 'error' => $e->getMessage()], 500); + } + } + + public function downloadGeojsonAllActivePolygons() + { + ini_set('max_execution_time', '-1'); + ini_set('memory_limit', '-1'); + + try { + $activePolygonIds = SitePolygon::active()->pluck('poly_id'); + + $features = []; + foreach ($activePolygonIds as $polygonUuid) { + + $polygonGeometry = PolygonGeometry::where('uuid', $polygonUuid) + ->select(DB::raw('ST_AsGeoJSON(geom) AS geojsonGeom')) + ->first(); + + if (! $polygonGeometry) { + Log::warning('No geometry found for Polygon UUID:', ['uuid' => $polygonUuid]); + + continue; + } + $sitePolygon = SitePolygon::where('poly_id', $polygonUuid)->first(); + $properties = $sitePolygon ? $sitePolygon->only(['poly_name', 'plantstart', 'plantend', 'practice', 'target_sys', 'distr', 'num_trees', 'site_id', 'uuid']) : []; + $feature = [ + 'type' => 'Feature', + 'geometry' => json_decode($polygonGeometry->geojsonGeom), + 'properties' => $properties, + ]; + $features[] = $feature; + } + $featureCollection = [ + 'type' => 'FeatureCollection', + 'features' => $features, + ]; + + return response()->json($featureCollection); + } catch (\Exception $e) { + return response()->json(['message' => 'Failed to generate GeoJSON.', 'error' => $e->getMessage()], 500); + } + } + public function GetAllPolygonsLoaded($geojson, $uuid) { $polygonsUuids = SitePolygon::where('site_id', $uuid) diff --git a/app/Http/Controllers/V2/Terrafund/TerrafundEditGeometryController.php b/app/Http/Controllers/V2/Terrafund/TerrafundEditGeometryController.php index 64895d8d4..b686c1123 100644 --- a/app/Http/Controllers/V2/Terrafund/TerrafundEditGeometryController.php +++ b/app/Http/Controllers/V2/Terrafund/TerrafundEditGeometryController.php @@ -256,6 +256,9 @@ public function createSitePolygonNewVersion(string $uuid, Request $request) $user = Auth::user(); $newPolygonVersion = $sitePolygon->createCopy($user, null, false, $validatedData); + if (! $newPolygonVersion) { + return response()->json(['error' => 'An error occurred while creating a new version of the site polygon'], 500); + } $newPolygonVersion->changeStatusOnEdit(); return response()->json(['message' => 'Site polygon version created successfully'], 201); diff --git a/app/Http/Controllers/V2/User/AdminUserController.php b/app/Http/Controllers/V2/User/AdminUserController.php index bf386a3f6..fc951a962 100644 --- a/app/Http/Controllers/V2/User/AdminUserController.php +++ b/app/Http/Controllers/V2/User/AdminUserController.php @@ -53,7 +53,7 @@ public function index(Request $request): UsersCollection } if ($request->query('search')) { - $ids = User::search(trim($request->query('search')))->get()->pluck('id')->toArray(); + $ids = User::searchUsers(trim($request->query('search')))->pluck('id')->toArray(); if (empty($ids)) { return new UsersCollection([]); diff --git a/app/Http/Controllers/V2/UserLocaleController.php b/app/Http/Controllers/V2/UserLocaleController.php new file mode 100644 index 000000000..744554269 --- /dev/null +++ b/app/Http/Controllers/V2/UserLocaleController.php @@ -0,0 +1,22 @@ +locale = $request->get('locale'); + $user->saveOrFail(); + + return response()->json([ + 'message' => 'User locale updated successfully', + ]); + } +} diff --git a/app/Http/Requests/V2/Geometry/StoreGeometryRequest.php b/app/Http/Requests/V2/Geometry/StoreGeometryRequest.php index 0aa1f7249..3d35041bf 100644 --- a/app/Http/Requests/V2/Geometry/StoreGeometryRequest.php +++ b/app/Http/Requests/V2/Geometry/StoreGeometryRequest.php @@ -73,21 +73,15 @@ public function validateGeometries(): void 'features' => 'required|array|size:1', 'features.0.properties.site_id' => 'required|string', ])->validate(); - // This is guaranteed to be Point given the rules specified in rules() } else { - // Require that all geometries in the collection are valid points, include estimated area, and that the - // collection has exactly one unique site id. - $siteIds = collect(data_get($geometry, 'features.*.properties.site_id')) - ->unique()->filter()->toArray(); - Validator::make(['geometry' => $geometry, 'site_ids' => $siteIds], [ + // Require that all geometries in the collection are valid points, include estimated area + Validator::make(['geometry' => $geometry], [ 'geometry.features.*.geometry.type' => 'required|string|in:Point', 'geometry.features.*.geometry.coordinates' => 'required|array|size:2', // Minimum is 1m^2 (0.0001 hectares) 'geometry.features.*.properties.est_area' => 'required|numeric|min:0.0001', - // All points require a site id set, and they must all be the same site (enforced via site_ids below) 'geometry.features.*.properties.site_id' => 'required|string', - 'site_ids' => 'required|array|size:1', ])->validate(); } } diff --git a/app/Http/Requests/V2/Reminder/SendReminderRequest.php b/app/Http/Requests/V2/Reminder/SendReminderRequest.php new file mode 100644 index 000000000..8b775bd0d --- /dev/null +++ b/app/Http/Requests/V2/Reminder/SendReminderRequest.php @@ -0,0 +1,20 @@ + ['sometimes', 'string', 'nullable'], + ]; + } +} diff --git a/app/Http/Requests/V2/UserLocaleRequest.php b/app/Http/Requests/V2/UserLocaleRequest.php new file mode 100644 index 000000000..fe795f1cd --- /dev/null +++ b/app/Http/Requests/V2/UserLocaleRequest.php @@ -0,0 +1,20 @@ + 'required|in:en-US,es-MX,fr-FR,pt-BR', + ]; + } +} diff --git a/app/Http/Resources/V2/Files/Gallery/GalleryResource.php b/app/Http/Resources/V2/Files/Gallery/GalleryResource.php index ccba8d1cb..bcf9b7976 100644 --- a/app/Http/Resources/V2/Files/Gallery/GalleryResource.php +++ b/app/Http/Resources/V2/Files/Gallery/GalleryResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources\V2\Files\Gallery; +use App\Http\Resources\V2\User\UserLiteResource; use App\Models\V2\Nurseries\Nursery; use App\Models\V2\Nurseries\NurseryReport; use App\Models\V2\Projects\Project; @@ -10,6 +11,7 @@ use App\Models\V2\Sites\Site; use App\Models\V2\Sites\SiteMonitoring; use App\Models\V2\Sites\SiteReport; +use App\Models\V2\User; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Resources\Json\JsonResource; @@ -27,10 +29,12 @@ public function toArray($request) 'uuid' => $this->uuid, 'file_url' => $this->getFullUrl(), 'thumb_url' => $this->getFullUrl('thumbnail'), + 'name' => $this->name, 'file_name' => $this->file_name, 'created_date' => $this->created_at, 'model_name' => $this->getModelName(), 'is_public' => (bool) $this->is_public, + 'is_cover' => (bool) $this->is_cover, 'location' => [ 'lat' => (float) $this->lat ?? null, 'lng' => (float) $this->lng ?? null, @@ -38,6 +42,9 @@ public function toArray($request) 'mime_type' => $this->mime_type, 'file_size' => $this->size, 'collection_name' => $this->collection_name, + 'photographer' => $this->photographer, + 'description' => $this->description, + 'created_by' => new UserLiteResource(User::find($this->created_by)), ]; } diff --git a/app/Http/Resources/V2/Projects/ProjectResource.php b/app/Http/Resources/V2/Projects/ProjectResource.php index 4afc9309b..8d0bde966 100644 --- a/app/Http/Resources/V2/Projects/ProjectResource.php +++ b/app/Http/Resources/V2/Projects/ProjectResource.php @@ -91,8 +91,17 @@ public function toArray($request) 'migrated' => ! empty($this->old_model), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, + 'trees_restored_ppc' => + $this->getTreesGrowingThroughAnr($this->sites) + (($this->trees_planted_count + $this->seeds_planted_count) * ($this->survival_rate / 100)), ]; return $this->appendFilesToResource($data); } + + public function getTreesGrowingThroughAnr($sites) + { + return $sites->sum(function ($site) { + return $site->reports->sum('num_trees_regenerating'); + }); + } } diff --git a/app/Http/Resources/V2/User/MeResource.php b/app/Http/Resources/V2/User/MeResource.php index 93861f303..d1025649b 100644 --- a/app/Http/Resources/V2/User/MeResource.php +++ b/app/Http/Resources/V2/User/MeResource.php @@ -16,6 +16,7 @@ public function toArray($request) 'email_address_verified_at' => $this->email_address_verified_at, 'email_address' => $this->email_address, 'role' => $this->primary_role->name, + 'locale' => $this->locale, 'organisation' => new MyOrganisationLiteResource($this->my_primary_organisation), 'frameworks' => $this->my_frameworks->map(function ($framework) { return [ diff --git a/app/Jobs/NotifyInterestJob.php b/app/Jobs/NotifyInterestJob.php index b10c3a35d..b3069090f 100644 --- a/app/Jobs/NotifyInterestJob.php +++ b/app/Jobs/NotifyInterestJob.php @@ -24,9 +24,12 @@ class NotifyInterestJob implements ShouldQueue private $interest = null; - public function __construct(InterestModel $interest) + private $user = null; + + public function __construct(InterestModel $interest, $user) { $this->interest = $interest; + $this->user = $user; } public function handle() @@ -60,7 +63,7 @@ private function notifyUsers(String $model, String $name, Int $id, Int $organisa ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new InterestShownMail($model, $name, $id)); + Mail::to($user->email_address)->send(new InterestShownMail($model, $name, $id, $user)); } $pushService->sendPush( $user, diff --git a/app/Jobs/NotifyProgressUpdateCreatedJob.php b/app/Jobs/NotifyProgressUpdateCreatedJob.php index 26e0b1bda..3ac9b992d 100644 --- a/app/Jobs/NotifyProgressUpdateCreatedJob.php +++ b/app/Jobs/NotifyProgressUpdateCreatedJob.php @@ -44,7 +44,7 @@ private function notifyUsers(Int $organisationId, Int $progressUpdateId, String foreach ($users as $user) { if ($user->is_subscribed) { Mail::to($user->email_address)->send( - new ProgressUpdateCreatedMail($progressUpdateId, $pitchName) + new ProgressUpdateCreatedMail($progressUpdateId, $pitchName, $user) ); } /* diff --git a/app/Jobs/NotifyProjectUpdatedJob.php b/app/Jobs/NotifyProjectUpdatedJob.php index 900ddca2d..e6d685f11 100644 --- a/app/Jobs/NotifyProjectUpdatedJob.php +++ b/app/Jobs/NotifyProjectUpdatedJob.php @@ -86,7 +86,7 @@ private function notifyMatchedUsers(array $organisationIds, String $model, Int $ ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new ProjectUpdatedMail('Matched', $model, $id)); + Mail::to($user->email_address)->send(new ProjectUpdatedMail('Matched', $model, $id, $user)); } $pushService->sendPush( $user, @@ -117,7 +117,7 @@ private function notifyInterestedUsers(array $organisationIds, String $model, In ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new ProjectUpdatedMail('Interest', $model, $id)); + Mail::to($user->email_address)->send(new ProjectUpdatedMail('Interest', $model, $id, $user)); } $pushService->sendPush( $user, diff --git a/app/Jobs/NotifySatelliteMapCreatedJob.php b/app/Jobs/NotifySatelliteMapCreatedJob.php index 39060ffd4..bf40c178b 100644 --- a/app/Jobs/NotifySatelliteMapCreatedJob.php +++ b/app/Jobs/NotifySatelliteMapCreatedJob.php @@ -48,7 +48,7 @@ private function notifyUsers(Int $organisationId, Int $satelliteMapId, String $n ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new SatelliteMapCreatedMail($satelliteMapId, $name)); + Mail::to($user->email_address)->send(new SatelliteMapCreatedMail($satelliteMapId, $name, $user)); } /* $pushService->sendPush( diff --git a/app/Jobs/NotifyTargetAcceptedJob.php b/app/Jobs/NotifyTargetAcceptedJob.php index c22b4ee9a..789b7a07e 100644 --- a/app/Jobs/NotifyTargetAcceptedJob.php +++ b/app/Jobs/NotifyTargetAcceptedJob.php @@ -47,7 +47,7 @@ private function notifyUsers(Int $organisationId, Int $monitoringId, Int $target ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new TargetAcceptedMail($monitoringId, $name)); + Mail::to($user->email_address)->send(new TargetAcceptedMail($monitoringId, $name, $user)); } /* $pushService->sendPush( diff --git a/app/Jobs/NotifyTargetCreatedJob.php b/app/Jobs/NotifyTargetCreatedJob.php index a181cbc3d..19342505b 100644 --- a/app/Jobs/NotifyTargetCreatedJob.php +++ b/app/Jobs/NotifyTargetCreatedJob.php @@ -57,7 +57,7 @@ private function notifyUsers(Int $organisationId, Int $targetId, String $name) ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new TargetCreatedMail($targetId, $name)); + Mail::to($user->email_address)->send(new TargetCreatedMail($targetId, $name, $user)); } /* $pushService->sendPush( diff --git a/app/Jobs/NotifyTargetUpdatedJob.php b/app/Jobs/NotifyTargetUpdatedJob.php index 35a2a7291..0d4fe799f 100644 --- a/app/Jobs/NotifyTargetUpdatedJob.php +++ b/app/Jobs/NotifyTargetUpdatedJob.php @@ -57,7 +57,7 @@ private function notifyUsers(Int $organisationId, Int $targetId, String $name) ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new TargetUpdatedMail($targetId, $name)); + Mail::to($user->email_address)->send(new TargetUpdatedMail($targetId, $name, $user)); } /* $pushService->sendPush( diff --git a/app/Jobs/NotifyUnmatchJob.php b/app/Jobs/NotifyUnmatchJob.php index 05fcc96d6..6fa505702 100644 --- a/app/Jobs/NotifyUnmatchJob.php +++ b/app/Jobs/NotifyUnmatchJob.php @@ -66,7 +66,7 @@ private function notifyUsers(Int $organisationId, String $name) ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new UnmatchMail('User', $name)); + Mail::to($user->email_address)->send(new UnmatchMail('User', $name, '', $user)); } $pushService->sendPush( $user, @@ -90,7 +90,7 @@ private function notifyAdmins(String $firstName, String $secondName) $admins = User::admin()->accepted()->verified()->get(); foreach ($admins as $admin) { if ($admin->is_subscribed) { - Mail::to($admin->email_address)->send(new UnmatchMail('Admin', $firstName, $secondName)); + Mail::to($admin->email_address)->send(new UnmatchMail('Admin', $firstName, $secondName, $admin)); } $notification = new NotificationModel([ 'user_id' => $admin->id, diff --git a/app/Jobs/NotifyUpcomingProgressUpdateJob.php b/app/Jobs/NotifyUpcomingProgressUpdateJob.php index 538e296cf..a25f25e4e 100644 --- a/app/Jobs/NotifyUpcomingProgressUpdateJob.php +++ b/app/Jobs/NotifyUpcomingProgressUpdateJob.php @@ -44,7 +44,7 @@ private function notifyUsers(Int $organisationId, Int $monitoringId, String $pit foreach ($users as $user) { if ($user->is_subscribed) { Mail::to($user->email_address)->send( - new UpcomingProgressUpdateMail($monitoringId, $pitchName) + new UpcomingProgressUpdateMail($monitoringId, $pitchName, $user) ); } /* diff --git a/app/Jobs/NotifyUpdateVisibilityJob.php b/app/Jobs/NotifyUpdateVisibilityJob.php index d13eb6dd6..6764cc7ed 100644 --- a/app/Jobs/NotifyUpdateVisibilityJob.php +++ b/app/Jobs/NotifyUpdateVisibilityJob.php @@ -46,7 +46,7 @@ private function notifyUsers(String $model, Int $id, Int $organisationId) ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new UpdateVisibility($model, $id)); + Mail::to($user->email_address)->send(new UpdateVisibility($model, $id, $user)); } $pushService->sendPush( $user, diff --git a/app/Jobs/NotifyVersionApprovedJob.php b/app/Jobs/NotifyVersionApprovedJob.php index 8decef235..0a6fd1910 100644 --- a/app/Jobs/NotifyVersionApprovedJob.php +++ b/app/Jobs/NotifyVersionApprovedJob.php @@ -78,7 +78,7 @@ private function notifyUsers(String $model, Int $id, Int $organisationId) ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new VersionApprovedMail($model, $id)); + Mail::to($user->email_address)->send(new VersionApprovedMail($model, $id, $user)); } $pushService->sendPush( $user, diff --git a/app/Jobs/NotifyVersionCreatedJob.php b/app/Jobs/NotifyVersionCreatedJob.php index f8db2a05f..6a5e88335 100644 --- a/app/Jobs/NotifyVersionCreatedJob.php +++ b/app/Jobs/NotifyVersionCreatedJob.php @@ -77,7 +77,7 @@ private function notifyAdmins(String $model, Int $id) $admins = User::admin()->accepted()->verified()->get(); foreach ($admins as $admin) { if ($admin->is_subscribed) { - Mail::to($admin->email_address)->send(new VersionCreatedMail($model, $id)); + Mail::to($admin->email_address)->send(new VersionCreatedMail($model, $id, $admin)); } $notification = new NotificationModel([ 'user_id' => $admin->id, diff --git a/app/Jobs/NotifyVersionRejectedJob.php b/app/Jobs/NotifyVersionRejectedJob.php index 1a7d05949..3c95e4ed8 100644 --- a/app/Jobs/NotifyVersionRejectedJob.php +++ b/app/Jobs/NotifyVersionRejectedJob.php @@ -83,7 +83,7 @@ private function notifyUsers(String $model, Int $id, Int $organisationId, String ->get(); foreach ($users as $user) { if ($user->is_subscribed) { - Mail::to($user->email_address)->send(new VersionRejectedMail($model, $id, $explanation)); + Mail::to($user->email_address)->send(new VersionRejectedMail($model, $id, $explanation, $user)); } $pushService->sendPush( $user, diff --git a/app/Jobs/ResetPasswordJob.php b/app/Jobs/ResetPasswordJob.php index 68d006148..b8fb18dac 100644 --- a/app/Jobs/ResetPasswordJob.php +++ b/app/Jobs/ResetPasswordJob.php @@ -40,6 +40,6 @@ public function handle() $passwordReset->user_id = $this->model->id; $passwordReset->token = Str::random(32); $passwordReset->saveOrFail(); - Mail::to($this->model->email_address)->send(new ResetPasswordMail($passwordReset->token, $this->callbackUrl)); + Mail::to($this->model->email_address)->send(new ResetPasswordMail($passwordReset->token, $this->callbackUrl, $this->model)); } } diff --git a/app/Jobs/UserVerificationJob.php b/app/Jobs/UserVerificationJob.php index fed6d86fb..abce95878 100644 --- a/app/Jobs/UserVerificationJob.php +++ b/app/Jobs/UserVerificationJob.php @@ -40,6 +40,6 @@ public function handle() $verification->user_id = $this->model->id; $verification->token = Str::random(32); $verification->saveOrFail(); - Mail::to($this->model->email_address)->send(new UserVerificationMail($verification->token, $this->callbackUrl)); + Mail::to($this->model->email_address)->send(new UserVerificationMail($verification->token, $this->callbackUrl, $this->model)); } } diff --git a/app/Jobs/V2/SendEntityStatusChangeEmailsJob.php b/app/Jobs/V2/SendEntityStatusChangeEmailsJob.php index 71968922a..d9aedb937 100644 --- a/app/Jobs/V2/SendEntityStatusChangeEmailsJob.php +++ b/app/Jobs/V2/SendEntityStatusChangeEmailsJob.php @@ -34,20 +34,20 @@ public function handle(): void return; } - $emailAddresses = $this->entity->project->users()->pluck('email_address'); - if (empty($emailAddresses)) { + $usersFromProject = $this->entity->project->users; + if (empty($usersFromProject)) { return; } // TODO: This is a temporary hack to avoid spamming folks that have a funky role right now. In the future, // they will have a different role, and we can simply skip sending this email to anybody with that role. $skipRecipients = collect(explode(',', getenv('ENTITY_UPDATE_DO_NOT_EMAIL'))); - foreach ($emailAddresses as $emailAddress) { - if ($skipRecipients->contains($emailAddress)) { + foreach ($usersFromProject as $user) { + if ($skipRecipients->contains($user['email_address'])) { continue; } - Mail::to($emailAddress)->send(new EntityStatusChangeMail($this->entity)); + Mail::to($user['email_address'])->send(new EntityStatusChangeMail($this->entity, $user)); } } } diff --git a/app/Jobs/V2/SendProjectManagerJob.php b/app/Jobs/V2/SendProjectManagerJob.php new file mode 100644 index 000000000..d28e654aa --- /dev/null +++ b/app/Jobs/V2/SendProjectManagerJob.php @@ -0,0 +1,40 @@ +entity = $entity; + } + + public function handle(): void + { + $usersManagers = $this->entity->project->managers ?? $this->entity->managers; + + if (empty($usersManagers)) { + return; + } + + foreach ($usersManagers as $manager) { + Mail::to($manager['email_address'])->send(new ProjectManagerMail($this->entity, $manager)); + } + } +} diff --git a/app/Jobs/V2/SendReportReminderEmailsJob.php b/app/Jobs/V2/SendReportReminderEmailsJob.php new file mode 100644 index 000000000..cd0e40908 --- /dev/null +++ b/app/Jobs/V2/SendReportReminderEmailsJob.php @@ -0,0 +1,43 @@ +entity = $entity; + $this->feedback = $feedback; + } + + public function handle(): void + { + + $users = $this->entity->project->users; + if (empty($users)) { + return; + } + + foreach ($users as $user) { + Mail::to($user->email_address)->send(new ReportReminderMail($this->entity, $this->feedback, $user)); + } + } +} diff --git a/app/Listeners/v2/Application/ApplicationSubmittedConfirmationSendEmail.php b/app/Listeners/v2/Application/ApplicationSubmittedConfirmationSendEmail.php index d9e97fca4..2bf897004 100644 --- a/app/Listeners/v2/Application/ApplicationSubmittedConfirmationSendEmail.php +++ b/app/Listeners/v2/Application/ApplicationSubmittedConfirmationSendEmail.php @@ -27,6 +27,6 @@ public function handle(ApplicationSubmittedEvent $event) $form = $formSubmission->form; $user = $event->user; - Mail::to($user->email_address)->send(new ApplicationSubmittedConfirmation(data_get($form, 'submission_message', 'Thank you for sending your application.'))); + Mail::to($user->email_address)->send(new ApplicationSubmittedConfirmation(data_get($form, 'submission_message', 'Thank you for sending your application.'), $user)); } } diff --git a/app/Listeners/v2/Organisation/OrganisationApprovedSendEmail.php b/app/Listeners/v2/Organisation/OrganisationApprovedSendEmail.php index a47032d8e..4c84f8c1d 100644 --- a/app/Listeners/v2/Organisation/OrganisationApprovedSendEmail.php +++ b/app/Listeners/v2/Organisation/OrganisationApprovedSendEmail.php @@ -27,6 +27,6 @@ public function handle(OrganisationApprovedEvent $event) $organisation = $event->organisation; $user = User::where('organisation_id', $organisation->id)->firstOrFail(); - Mail::to($user->email_address)->send(new OrganisationApproved($organisation)); + Mail::to($user->email_address)->send(new OrganisationApproved($user)); } } diff --git a/app/Listeners/v2/Organisation/OrganisationRejectedSendEmail.php b/app/Listeners/v2/Organisation/OrganisationRejectedSendEmail.php index c90eb5bc1..56e792a02 100644 --- a/app/Listeners/v2/Organisation/OrganisationRejectedSendEmail.php +++ b/app/Listeners/v2/Organisation/OrganisationRejectedSendEmail.php @@ -27,6 +27,6 @@ public function handle(OrganisationRejectedEvent $event) $organisation = $event->organisation; $user = User::where('organisation_id', $organisation->id)->firstOrFail(); - Mail::to($user->email_address)->send(new OrganisationRejected($organisation)); + Mail::to($user->email_address)->send(new OrganisationRejected($organisation, $user)); } } diff --git a/app/Listeners/v2/Organisation/OrganisationSubmittedConfirmationSendEmail.php b/app/Listeners/v2/Organisation/OrganisationSubmittedConfirmationSendEmail.php index 4da0a73a2..8af4baff8 100644 --- a/app/Listeners/v2/Organisation/OrganisationSubmittedConfirmationSendEmail.php +++ b/app/Listeners/v2/Organisation/OrganisationSubmittedConfirmationSendEmail.php @@ -27,6 +27,6 @@ public function handle(OrganisationSubmittedEvent $event) $organisation = $event->organisation; $user = User::where('organisation_id', $organisation->id)->firstOrFail(); - Mail::to($user->email_address)->send(new OrganisationSubmitConfirmation($organisation)); + Mail::to($user->email_address)->send(new OrganisationSubmitConfirmation($user)); } } diff --git a/app/Listeners/v2/Organisation/OrganisationUserApprovedSendEmail.php b/app/Listeners/v2/Organisation/OrganisationUserApprovedSendEmail.php index 4d61ed457..944ba9e49 100644 --- a/app/Listeners/v2/Organisation/OrganisationUserApprovedSendEmail.php +++ b/app/Listeners/v2/Organisation/OrganisationUserApprovedSendEmail.php @@ -26,6 +26,6 @@ public function handle(OrganisationUserRequestApprovedEvent $event) $organisation = $event->organisation; $user = $event->user; - Mail::to($user->email_address)->send(new OrganisationUserApproved($organisation)); + Mail::to($user->email_address)->send(new OrganisationUserApproved($organisation, $user)); } } diff --git a/app/Listeners/v2/Organisation/OrganisationUserJoinRequestSendEmail.php b/app/Listeners/v2/Organisation/OrganisationUserJoinRequestSendEmail.php index 10d8932b1..faf9ddfe0 100644 --- a/app/Listeners/v2/Organisation/OrganisationUserJoinRequestSendEmail.php +++ b/app/Listeners/v2/Organisation/OrganisationUserJoinRequestSendEmail.php @@ -24,8 +24,9 @@ public function __construct() public function handle(OrganisationUserJoinRequestEvent $event) { $organisation = $event->organisation; - $emailAddressList = $organisation->owners()->pluck('email_address')->toArray(); - - Mail::to($emailAddressList)->send(new OrganisationUserJoinRequested($organisation)); + $emailAddressList = $organisation->owners; + foreach ($emailAddressList as $user) { + Mail::to($user->email_address)->send(new OrganisationUserJoinRequested($user)); + } } } diff --git a/app/Listeners/v2/Organisation/OrganisationUserRejectedSendEmail.php b/app/Listeners/v2/Organisation/OrganisationUserRejectedSendEmail.php index a7a8dc9d2..3261ab694 100644 --- a/app/Listeners/v2/Organisation/OrganisationUserRejectedSendEmail.php +++ b/app/Listeners/v2/Organisation/OrganisationUserRejectedSendEmail.php @@ -26,6 +26,6 @@ public function handle(OrganisationUserRequestRejectedEvent $event) $organisation = $event->organisation; $user = $event->user; - Mail::to($user->email_address)->send(new OrganisationUserRejected($organisation)); + Mail::to($user->email_address)->send(new OrganisationUserRejected($organisation, $user)); } } diff --git a/app/Mail/ApplicationSubmittedConfirmation.php b/app/Mail/ApplicationSubmittedConfirmation.php index 3a9bc9bbf..5be948ddc 100644 --- a/app/Mail/ApplicationSubmittedConfirmation.php +++ b/app/Mail/ApplicationSubmittedConfirmation.php @@ -2,12 +2,14 @@ namespace App\Mail; -class ApplicationSubmittedConfirmation extends Mail +class ApplicationSubmittedConfirmation extends I18nMail { - public function __construct(string $submissionMessage) + public function __construct(string $submissionMessage, $user) { - $this->subject = 'Your Application Has Been Submitted'; - $this->title = 'Your Application Has Been Submitted!'; + parent::__construct($user); + + $this->setSubjectKey('application-submitted-confirmation.subject') + ->setTitleKey('application-submitted-confirmation.title'); $this->body = $submissionMessage; } diff --git a/app/Mail/EntityStatusChange.php b/app/Mail/EntityStatusChange.php index da312f111..361c2bf12 100644 --- a/app/Mail/EntityStatusChange.php +++ b/app/Mail/EntityStatusChange.php @@ -5,21 +5,53 @@ use App\Models\V2\EntityModel; use App\Models\V2\ReportModel; use App\StateMachines\EntityStatusStateMachine; -use Illuminate\Support\Collection; -class EntityStatusChange extends Mail +class EntityStatusChange extends I18nMail { private EntityModel $entity; - public function __construct(EntityModel $entity) + public function __construct(EntityModel $entity, $user) { + parent::__construct($user); $this->entity = $entity; - $this->subject = $this->getSubject(); - $this->title = $this->subject; - $this->body = $this->getBodyParagraphs()->join('

'); + if ($this->getEntityStatus() == EntityStatusStateMachine::APPROVED) { + $this->setSubjectKey('entity-status-change.subject-approved') + ->setTitleKey('entity-status-change.subject-approved'); + } + if ($this->getEntityStatus() == EntityStatusStateMachine::NEEDS_MORE_INFORMATION) { + $this->setSubjectKey('entity-status-change.subject-needs-more-information') + ->setTitleKey('entity-status-change.subject-needs-more-information'); + } + + if ($this->entity instanceof ReportModel) { + if ($this->getEntityStatus() == EntityStatusStateMachine::APPROVED) { + $this->setBodyKey('entity-status-change.body-report-approved'); + } + if ($this->getEntityStatus() == EntityStatusStateMachine::NEEDS_MORE_INFORMATION) { + $this->setBodyKey('entity-status-change.body-report-needs-more-information'); + } + } else { + if ($this->getEntityStatus() == EntityStatusStateMachine::APPROVED) { + $this->setBodyKey('entity-status-change.body-entity-approved'); + } + if ($this->getEntityStatus() == EntityStatusStateMachine::NEEDS_MORE_INFORMATION) { + $this->setBodyKey('entity-status-change.body-entity-needs-more-information'); + } + } + $params = [ + '{entityTypeName}' => $this->getEntityTypeName(), + '{lowerEntityTypeName}' => strtolower($this->getEntityTypeName()), + '{entityName}' => $this->entity->name, + '{feedback}' => $this->getFeedback() ?? '(No feedback)', + ]; + if ($this->entity->parentEntity) { + $params['{parentEntityName}'] = $this->entity->parentEntity()->pluck('name')->first(); + } + + $this->setParams($params) + ->setCta('entity-status-change.cta'); $this->link = $this->entity->getViewLinkPath(); - $this->cta = 'View ' . $this->getEntityTypeName(); $this->transactional = true; } @@ -46,17 +78,6 @@ private function getEntityStatus(): ?string return null; } - private function getSubject(): string - { - return match ($this->getEntityStatus()) { - EntityStatusStateMachine::APPROVED => - 'Your ' . $this->getEntityTypeName() . ' Has Been Approved', - EntityStatusStateMachine::NEEDS_MORE_INFORMATION => - 'There is More Information Requested About Your ' . $this->getEntityTypeName(), - default => '', - }; - } - private function getFeedback(): ?string { if ($this->entity->update_request_status == EntityStatusStateMachine::APPROVED || @@ -77,36 +98,4 @@ private function getFeedback(): ?string return str_replace("\n", '
', $feedback); } - - private function getBodyParagraphs(): Collection - { - $paragraphs = collect(); - if ($this->entity instanceof ReportModel) { - $paragraphs->push('Thank you for submitting your ' . - $this->entity->parentEntity()->pluck('name')->first() . - ' report.'); - } else { - $paragraphs->push('Thank you for submitting your ' . - strtolower($this->getEntityTypeName()) . - ' information for ' . - $this->entity->name . - '.'); - } - - $paragraphs->push(match ($this->getEntityStatus()) { - EntityStatusStateMachine::APPROVED => [ - 'The information has been reviewed by your project manager and has been approved.', - $this->getFeedback(), - ], - EntityStatusStateMachine::NEEDS_MORE_INFORMATION => [ - 'The information has been reviewed by your project manager and they would like to see the following updates:', - $this->getFeedback() ?? '(No feedback)', - ], - default => null - }); - - $paragraphs->push('If you have any additional questions please reach out to your project manager or to info@terramatch.org'); - - return $paragraphs->flatten()->filter(); - } } diff --git a/app/Mail/FormSubmissionApproved.php b/app/Mail/FormSubmissionApproved.php index 8b198778a..0522be760 100644 --- a/app/Mail/FormSubmissionApproved.php +++ b/app/Mail/FormSubmissionApproved.php @@ -2,18 +2,15 @@ namespace App\Mail; -class FormSubmissionApproved extends Mail +class FormSubmissionApproved extends I18nMail { - public function __construct(String $feedback = null) + public function __construct(String $feedback = null, $user) { - $this->subject = 'Application Approved'; - $this->title = 'Your application has been approved'; - $this->body = - 'Your application has been approved.'; - if (! is_null($feedback)) { - $this->body = 'Your application has been approved. Please see comments below:

' . - e($feedback); - } + parent::__construct($user); + $this->setSubjectKey('form-submission-approved.subject') + ->setTitleKey('form-submission-approved.title') + ->setBodyKey(! is_null($feedback) ? 'form-submission-approved.body-feedback' : 'form-submission-approved.body') + ->setParams(['{feedback}' => e($feedback)]); $this->transactional = true; } } diff --git a/app/Mail/FormSubmissionFeedbackReceived.php b/app/Mail/FormSubmissionFeedbackReceived.php index bbd1edaf7..b97740393 100644 --- a/app/Mail/FormSubmissionFeedbackReceived.php +++ b/app/Mail/FormSubmissionFeedbackReceived.php @@ -2,18 +2,16 @@ namespace App\Mail; -class FormSubmissionFeedbackReceived extends Mail +class FormSubmissionFeedbackReceived extends I18nMail { - public function __construct(String $feedback = null) + public function __construct(String $feedback = null, $user) { - $this->subject = 'You have received feedback on your application'; - $this->title = 'You have received feedback on your application'; - $this->body = - 'Your application requires more information.'; - if (! is_null($feedback)) { - $this->body = 'Your application requires more information. Please see comments below:

' . - e($feedback); - } + parent::__construct($user); + $this->setSubjectKey('form-submission-feedback-received.subject') + ->setTitleKey('form-submission-feedback-received.title') + ->setBodyKey(! is_null($feedback) ? 'form-submission-feedback-received.body-feedback' : 'form-submission-feedback-received.body') + ->setParams(['{feedback}' => e($feedback)]); + $this->transactional = true; } } diff --git a/app/Mail/FormSubmissionFinalStageApproved.php b/app/Mail/FormSubmissionFinalStageApproved.php index affb5e13b..2a43954aa 100644 --- a/app/Mail/FormSubmissionFinalStageApproved.php +++ b/app/Mail/FormSubmissionFinalStageApproved.php @@ -2,22 +2,16 @@ namespace App\Mail; -class FormSubmissionFinalStageApproved extends Mail +class FormSubmissionFinalStageApproved extends I18nMail { - public function __construct(String $feedback = null) + public function __construct(String $feedback = null, $user) { - $this->subject = 'Application Approved'; - $this->title = 'Your application has been approved'; - if (! is_null($feedback)) { - $this->body = - 'Your application has successfully passed all stages of our evaluation process and has been officially approved. Please see the comments below:

' . - e($feedback) . - '

If you have any immediate queries, please do not hesitate to reach out to our dedicated support team.'; - } else { - $this->body = - 'Your application has successfully passed all stages of our evaluation process and has been officially approved. - If you have any immediate queries, please do not hesitate to reach out to our support team.'; - } + parent::__construct($user); + $this->setSubjectKey('form-submission-final-stage-approved.subject') + ->setTitleKey('form-submission-final-stage-approved.title') + ->setBodyKey(! is_null($feedback) ? 'form-submission-final-stage-approved.body-feedback' : 'form-submission-final-stage-approved.body') + ->setParams(['{feedback}' => e($feedback)]); + $this->transactional = true; } } diff --git a/app/Mail/FormSubmissionRejected.php b/app/Mail/FormSubmissionRejected.php index db36d23c1..3ed6b5373 100644 --- a/app/Mail/FormSubmissionRejected.php +++ b/app/Mail/FormSubmissionRejected.php @@ -2,18 +2,16 @@ namespace App\Mail; -class FormSubmissionRejected extends Mail +class FormSubmissionRejected extends I18nMail { - public function __construct(String $feedback = null) + public function __construct(String $feedback = null, $user) { - $this->subject = 'Application Status Update'; - $this->title = 'THANK YOU FOR YOUR APPLICATION'; - $this->body = 'After careful review, our team has decided your application will not move forward.'; - if (! is_null($feedback)) { - $this->body .= - ' Please see the comments below for more details or any follow-up resources.

' . - e($feedback); - } + parent::__construct($user); + $this->setSubjectKey('form-submission-rejected.subject') + ->setTitleKey('form-submission-rejected.title') + ->setBodyKey(! is_null($feedback) ? 'form-submission-rejected.body-feedback' : 'form-submission-rejected.body') + ->setParams(['{feedback}' => e($feedback)]); + $this->transactional = true; } } diff --git a/app/Mail/FormSubmissionSubmitted.php b/app/Mail/FormSubmissionSubmitted.php index 1c1e7d9c2..b698289a0 100644 --- a/app/Mail/FormSubmissionSubmitted.php +++ b/app/Mail/FormSubmissionSubmitted.php @@ -2,14 +2,15 @@ namespace App\Mail; -class FormSubmissionSubmitted extends Mail +class FormSubmissionSubmitted extends I18nMail { - public function __construct() + public function __construct($user) { - $this->subject = 'You have submitted an application'; - $this->title = 'You have submitted an application'; - $this->body = - 'Your application has been submitted'; + parent::__construct($user); + $this->setSubjectKey('form-submission-submitted.subject') + ->setTitleKey('form-submission-submitted.title') + ->setBodyKey('form-submission-submitted.body'); + $this->transactional = true; } } diff --git a/app/Mail/I18nMail.php b/app/Mail/I18nMail.php new file mode 100644 index 000000000..75c659b1b --- /dev/null +++ b/app/Mail/I18nMail.php @@ -0,0 +1,94 @@ +userLocale = is_null($user) ? 'en-US' : $user->locale ?? $user['locale'] ?? 'en-US'; + } + + public function build() + { + if (isset($this->subjectKey)) { + $this->subject = $this->getValueTranslated($this->subjectKey); + } + if (isset($this->titleKey)) { + $this->title = $this->getValueTranslated($this->titleKey); + } + if (isset($this->bodyKey)) { + $this->body = $this->getValueTranslated($this->bodyKey); + } + if (isset($this->ctaKey)) { + $this->cta = $this->getValueTranslated($this->ctaKey); + } + + parent::build(); + } + + public function setSubjectKey(string $key): I18nMail + { + $this->subjectKey = $key; + + return $this; + } + + public function setTitleKey(string $key): I18nMail + { + $this->titleKey = $key; + + return $this; + } + + public function setBodyKey(string $key): I18nMail + { + $this->bodyKey = $key; + + return $this; + } + + public function setCta(string $key): I18nMail + { + $this->ctaKey = $key; + + return $this; + } + + public function setParams(array $params = []): I18nMail + { + $this->params = $params; + + return $this; + } + + public function getValueTranslated($valueKey) + { + App::setLocale($this->userLocale); + $localizationKey = LocalizationKey::where('key', $valueKey)->first(); + if (is_null($localizationKey)) { + return $valueKey; + } + + if (! empty($this->params)) { + return str_replace(array_keys($this->params), array_values($this->params), $localizationKey->translated_value); + } + + return $localizationKey->translated_value; + } +} diff --git a/app/Mail/InterestShown.php b/app/Mail/InterestShown.php index b83e9136d..fdf419d6e 100644 --- a/app/Mail/InterestShown.php +++ b/app/Mail/InterestShown.php @@ -4,10 +4,11 @@ use Exception; -class InterestShown extends Mail +class InterestShown extends I18nMail { - public function __construct(String $model, String $name, Int $id) + public function __construct(String $model, String $name, Int $id, $user) { + parent::__construct($user); switch ($model) { case 'Offer': $link = '/funding/' . $id; @@ -20,12 +21,12 @@ public function __construct(String $model, String $name, Int $id) default: throw new Exception(); } - $this->subject = 'Someone Has Shown Interest In One Of Your Projects'; - $this->title = 'Someone Has Shown Interest In One Of Your Projects'; - $this->body = - e($name) . ' has shown interest in one of your projects.

' . - 'Follow this link to view their project.'; + $this->setSubjectKey('interest-shown.subject') + ->setTitleKey('interest-shown.title') + ->setBodyKey('interest-shown.body') + ->setCta('interest-shown.cta') + ->setParams(['{name}' => e($name)]); + $this->link = $link; - $this->cta = 'View Project'; } } diff --git a/app/Mail/MatchMail.php b/app/Mail/MatchMail.php index d4f20b336..fb31e10b2 100644 --- a/app/Mail/MatchMail.php +++ b/app/Mail/MatchMail.php @@ -3,11 +3,13 @@ namespace App\Mail; use Exception; +use Illuminate\Support\Facades\Auth; -class MatchMail extends Mail +class MatchMail extends I18nMail { public function __construct(String $model, String $firstName = "", String $secondName = "") { + parent::__construct(null); switch ($model) { case "Admin": $isAdmin = true; @@ -21,34 +23,29 @@ public function __construct(String $model, String $firstName = "", String $secon throw new Exception(); } if ($isAdmin) { - $this->subject = 'Match Detected'; - $this->title = "Match Detected"; - $this->body = - e($firstName) . " and " . e($secondName) . " have matched.

" . - "Follow this link to view the match."; + $this->setSubjectKey('match-mail.subject-admin') + ->setTitleKey('match-mail.title-admin') + ->setBodyKey('match-mail.body-admin') + ->setCta('match-mail.cta-admin') + ->setParams([ + '{firstName}' => e($firstName), + '{secondName}' => e($secondName) + ]); $this->link = "/admin/matches"; - $this->cta = "View Match"; } else { if ($isFunder) { - $this->subject = 'Someone Has Matched With One Of Your Funding Offers'; - $this->title = "Someone Has Matched With One Of Your Funding Offers"; - $this->body = - "Congratulations! " . e($firstName) . " has matched with one of your funding offers.

" . - "Follow the link below to view their contact details.

" . - "If you have decided to move forward together, we encourage you to monitor your project on TerraMatch. Our monitoring system allows you to set mutually agreed targets, easily report on project progress using our templates, and access WRI's state-of-the-art satellite monitoring so that you can track progress over the long term.

" . - "Check out the monitoring section at TerraMatch.org."; + $this->setSubjectKey('match-mail.subject-funder') + ->setTitleKey('match-mail.title-funder') + ->setBodyKey('match-mail.body-funder') + ->setParams(['{firstName}' => e($firstName)]); } else { - $this->subject = 'Someone Has Matched With One Of Your Projects'; - $this->title = "Someone Has Matched With One Of Your Projects"; - $this->body = - "Congratulations! " . e($firstName) . " has matched with one of your projects.

" . - "Follow the link below to view their contact details.

" . - "If you have decided to move forward together, we encourage you to monitor your project on TerraMatch. Our monitoring system allows you to set mutually agreed targets, easily report on project progress using our templates, and access WRI's state-of-the-art satellite monitoring to show the funder that your trees are surviving. -

" . - "Check out the monitoring section at TerraMatch.org."; + $this->setSubjectKey('match-mail.subject-user') + ->setTitleKey('match-mail.title-user') + ->setBodyKey('match-mail.body-user') + ->setParams(['{firstName}' => e($firstName)]); } $this->link = "/connections"; - $this->cta = "View Contact Details"; + $this->setCta('match-mail.cta'); } } } diff --git a/app/Mail/OrganisationApproved.php b/app/Mail/OrganisationApproved.php index 7f2d89d0b..800393b87 100644 --- a/app/Mail/OrganisationApproved.php +++ b/app/Mail/OrganisationApproved.php @@ -2,14 +2,15 @@ namespace App\Mail; -class OrganisationApproved extends Mail +class OrganisationApproved extends I18nMail { - public function __construct() + public function __construct($user) { - $this->subject = 'Your organization has been accepted into TerraMatch.'; - $this->title = 'YOUR ORGANIZATION HAS BEEN ACCEPTED INTO TERRAMATCH.'; - $this->body = 'Please login to submit an application or report on a monitored project. If you have any questions, please reach out to info@terramatch.org'; - $this->cta = 'LOGIN'; + parent::__construct($user); + $this->setSubjectKey('organisation-approved.subject') + ->setTitleKey('organisation-approved.title') + ->setBodyKey('organisation-approved.body') + ->setCta('organisation-approved.cta'); $this->link = '/auth/login'; } } diff --git a/app/Mail/OrganisationRejected.php b/app/Mail/OrganisationRejected.php index c9e6f551d..61c4bdf76 100644 --- a/app/Mail/OrganisationRejected.php +++ b/app/Mail/OrganisationRejected.php @@ -2,16 +2,13 @@ namespace App\Mail; -class OrganisationRejected extends Mail +class OrganisationRejected extends I18nMail { - public function __construct() + public function __construct($user) { - $this->subject = 'Your organization has been rejected from joining TerraMatch.'; - $this->title = 'Your organization has been rejected from joining TerraMatch.'; - $this->body = 'This could be due to the fact that your organization is already on TerraMatch, - your organization will not benefit from the services that TerraMatch provides - or we do not have enough information to understand what your organization does. - Please login to TerraMatch to view a more detail description about why your - organization request has been rejected.'; + parent::__construct($user); + $this->setSubjectKey('organisation-rejected.subject') + ->setTitleKey('organisation-rejected.title') + ->setBodyKey('organisation-rejected.body'); } } diff --git a/app/Mail/OrganisationSubmitConfirmation.php b/app/Mail/OrganisationSubmitConfirmation.php index 319c85ff5..275853a0c 100644 --- a/app/Mail/OrganisationSubmitConfirmation.php +++ b/app/Mail/OrganisationSubmitConfirmation.php @@ -2,16 +2,13 @@ namespace App\Mail; -class OrganisationSubmitConfirmation extends Mail +class OrganisationSubmitConfirmation extends I18nMail { - public function __construct() + public function __construct($user) { - $this->subject = 'Organization request submitted to TerraMatch.'; - $this->title = 'ORGANIZATION REQUEST SUBMITTED TO TERRAMATCH.'; - $this->body = 'Your organization has been submitted and is in review with WRI. You can continue to use the platform to whilst your ' . - 'application is in review. If you have any questions, feel free to message us at info@terramatch.org.

' . - '

--
' . - 'Votre organisation a été soumise et est en cours d\'examen par le WRI. Vous pouvez continuer à utiliser la plateforme pendant que ' . - 'votre demande est en cours d\'examen. Si vous avez des questions, n\'hésitez pas à nous envoyer un message à info@terramatch.org.'; + parent::__construct($user); + $this->setSubjectKey('organisation-submit-confirmation.subject') + ->setTitleKey('organisation-submit-confirmation.title') + ->setBodyKey('organisation-submit-confirmation.body'); } } diff --git a/app/Mail/OrganisationUserApproved.php b/app/Mail/OrganisationUserApproved.php index aeac90c7d..c49f5ca06 100644 --- a/app/Mail/OrganisationUserApproved.php +++ b/app/Mail/OrganisationUserApproved.php @@ -4,12 +4,14 @@ use App\Models\V2\Organisation; -class OrganisationUserApproved extends Mail +class OrganisationUserApproved extends I18nMail { - public function __construct(Organisation $organisation) + public function __construct(Organisation $organisation, $user) { - $this->subject = 'You have been accepted to join ' . $organisation->name . ' on TerraMatch'; - $this->title = 'You have been accepted to join ' . $organisation->name . ' on TerraMatch'; - $this->body = 'You have been accepted to join ' . $organisation->name . ' on TerraMatch. Log-in to view or update your organization’s information.'; + parent::__construct($user); + $this->setSubjectKey('organisation-user-approved.subject') + ->setTitleKey('organisation-user-approved.title') + ->setBodyKey('organisation-user-approved.body') + ->setParams(['{organisationName}' => $organisation->name]); } } diff --git a/app/Mail/OrganisationUserJoinRequested.php b/app/Mail/OrganisationUserJoinRequested.php index 1a68f378b..50d412c85 100644 --- a/app/Mail/OrganisationUserJoinRequested.php +++ b/app/Mail/OrganisationUserJoinRequested.php @@ -2,14 +2,13 @@ namespace App\Mail; -use App\Models\V2\Organisation; - -class OrganisationUserJoinRequested extends Mail +class OrganisationUserJoinRequested extends I18nMail { - public function __construct(Organisation $organisation) + public function __construct($user) { - $this->subject = 'A user has requested to join your organization'; - $this->title = 'A user has requested to join your organization'; - $this->body = 'A user has requested to join your organization. Please go to the ‘Meet the Team’ page to review this request.'; + parent::__construct($user); + $this->setSubjectKey('organisation-user-join-requested.subject') + ->setTitleKey('organisation-user-join-requested.title') + ->setBodyKey('organisation-user-join-requested.body'); } } diff --git a/app/Mail/OrganisationUserRejected.php b/app/Mail/OrganisationUserRejected.php index d3dc0ef0b..59ce8896f 100644 --- a/app/Mail/OrganisationUserRejected.php +++ b/app/Mail/OrganisationUserRejected.php @@ -4,14 +4,14 @@ use App\Models\V2\Organisation; -class OrganisationUserRejected extends Mail +class OrganisationUserRejected extends I18nMail { - public function __construct(Organisation $organisation) + public function __construct(Organisation $organisation, $user) { - $this->subject = 'Your request to join ' . $organisation->name . ' on TerraMatch has been rejected'; - $this->title = 'Your request to join ' . $organisation->name . ' on TerraMatch has been rejected'; - $this->body = 'Your request to join ' . $organisation->name . ' on TerraMatch has been rejected.

' . - 'Please set-up a new organizational profile on TerraMatch if you wish to join the platform. ' . - 'Please reach out the help center here if you need more information: https://terramatchsupport.zendesk.com/hc/en-us/requests/new'; + parent::__construct($user); + $this->setSubjectKey('organisation-user-rejected.subject') + ->setTitleKey('organisation-user-rejected.title') + ->setBodyKey('organisation-user-rejected.body') + ->setParams(['{organisationName}' => $organisation->name]); } } diff --git a/app/Mail/ProgressUpdateCreated.php b/app/Mail/ProgressUpdateCreated.php index 7e2492978..8307ae04f 100644 --- a/app/Mail/ProgressUpdateCreated.php +++ b/app/Mail/ProgressUpdateCreated.php @@ -2,17 +2,16 @@ namespace App\Mail; -class ProgressUpdateCreated extends Mail +class ProgressUpdateCreated extends I18nMail { - public function __construct(Int $progressUpdateId, String $pitchName) + public function __construct(Int $progressUpdateId, String $pitchName, $user) { - $this->subject = 'Report Received'; - $this->banner = 'progress_update_created'; - $this->title = 'Report Received'; - $this->body = - ' A new progress update report has been submitted for ' . e($pitchName) . '.

' . - 'Click below to view the report.'; + parent::__construct($user); + $this->setSubjectKey('progress-update-created.subject') + ->setTitleKey('progress-update-created.title') + ->setBodyKey('progress-update-created.body') + ->setParams(['{pitchName}' => $pitchName]) + ->setCta('progress-update-created.cta'); $this->link = '/monitoring/report/' . $progressUpdateId; - $this->cta = 'View Report'; } } diff --git a/app/Mail/ProjectInviteReceived.php b/app/Mail/ProjectInviteReceived.php index 701df715c..0e578bb67 100644 --- a/app/Mail/ProjectInviteReceived.php +++ b/app/Mail/ProjectInviteReceived.php @@ -2,18 +2,18 @@ namespace App\Mail; -class ProjectInviteReceived extends Mail +class ProjectInviteReceived extends I18nMail { public function __construct(String $name, String $token, String $callbackUrl = null) { - $this->subject = 'Project Invite'; - $this->title = 'Project Invite'; - $this->body = - 'You have been sent an invite to join ' . e($name) . '.

' . - 'Click below to accept the invite.

'; + parent::__construct(null); + $this->setSubjectKey('project-invite-received.subject') + ->setTitleKey('project-invite-received.title') + ->setBodyKey('project-invite-received.body') + ->setParams(['{name}' => e($name)]) + ->setCta('project-invite-received.cta'); $this->link = $callbackUrl ? $callbackUrl . 'terrafund/programme/invite/accept?token=' . $token : '/terrafund/programme/invite/accept?token=' . $token; - $this->cta = 'Accept invite'; } } diff --git a/app/Mail/ProjectManager.php b/app/Mail/ProjectManager.php new file mode 100644 index 000000000..c317eabc6 --- /dev/null +++ b/app/Mail/ProjectManager.php @@ -0,0 +1,75 @@ +entity = $entity; + + if (get_class($entity) === Project::class || get_class($entity) === ProjectReport::class) { + $this->setSubjectKey('project-manager-project.subject') + ->setTitleKey('project-manager-project.title') + ->setBodyKey('project-manager-project.body') + ->setParams([ + '{projectName}' => $this->entity->project->name ?? $this->entity->name, + '{entityTypeName}' => 'Project', + ]) + ->setCta('project-manager-project.cta'); + $this->link = $this->getViewLinkEntity( + $this->entity->project->shortName ?? $this->entity->shortName, + $this->entity->project->uuid ?? $this->entity->uuid + ); + } + + if (get_class($entity) === Site::class || get_class($entity) === SiteReport::class) { + $this->setSubjectKey('project-manager-site.subject') + ->setTitleKey('project-manager-site.title') + ->setBodyKey('project-manager-site.body') + ->setParams([ + '{projectName}' => $this->entity->project->name, + '{entityName}' => $this->entity->site->name ?? $this->entity->name, + '{entityTypeName}' => 'Site', + ]) + ->setCta('project-manager-site.cta'); + $this->link = $this->getViewLinkEntity( + $this->entity->site->shortName ?? $this->entity->shortName, + $this->entity->site->uuid ?? $this->entity->uuid + ); + } + + if (get_class($entity) === Nursery::class || get_class($entity) === NurseryReport::class) { + $this->setSubjectKey('project-manager-nursery.subject') + ->setTitleKey('project-manager-nursery.title') + ->setBodyKey('project-manager-nursery.body') + ->setParams([ + '{projectName}' => $this->entity->project->name, + '{entityName}' => $this->entity->nursery->name ?? $this->entity->name, + '{entityTypeName}' => 'Nursery', + ]) + ->setCta('project-manager-nursery.cta'); + $this->link = $this->getViewLinkEntity( + $this->entity->nursery->shortName ?? $this->entity->shortName, + $this->entity->nursery->uuid ?? $this->entity->uuid + ); + } + } + + public function getViewLinkEntity($entity, $uuid) + { + return '/admin#/' . Str::camel($entity) . '/' . $uuid . '/show'; + } +} diff --git a/app/Mail/ProjectUpdated.php b/app/Mail/ProjectUpdated.php index 2808a9363..520553264 100644 --- a/app/Mail/ProjectUpdated.php +++ b/app/Mail/ProjectUpdated.php @@ -6,19 +6,20 @@ use App\Models\Pitch as PitchModel; use Exception; -class ProjectUpdated extends Mail +class ProjectUpdated extends I18nMail { - public function __construct(string $type, string $model, int $id) + public function __construct(string $type, string $model, int $id, $user) { + parent::__construct($user); switch ($type) { case 'Match': - $this->subject = "A Project You've Matched With Has Changed"; - $this->title = "A Project You've Matched With Has Changed"; + $this->setSubjectKey('project-updated.subject-match') + ->setTitleKey('project-updated.title-match'); break; case 'Interest': - $this->subject = "A Project You're Interested In Has Changed"; - $this->title = "A Project You're Interested In Has Changed"; + $this->setSubjectKey('project-updated.subject-interest') + ->setTitleKey('project-updated.title-interest'); break; default: @@ -51,11 +52,9 @@ public function __construct(string $type, string $model, int $id) default: throw new Exception(); } - $this->body = - e($name) . ' has changed.

' . - "You may want to review the changes to ensure you're still interested.

" . - 'Follow this link to view the project.'; + $this->setBodyKey('project-updated.body') + ->setParams(['{name}' => e($name)]) + ->setCta('project-updated.cta'); $this->link = $link; - $this->cta = 'View Project'; } } diff --git a/app/Mail/ReportReminder.php b/app/Mail/ReportReminder.php new file mode 100644 index 000000000..36bc3eb8f --- /dev/null +++ b/app/Mail/ReportReminder.php @@ -0,0 +1,32 @@ +entity = $entity; + $feedback = empty($feedback) ? '(No feedback)' : $feedback; + + $this->setSubjectKey('report-reminder.subject') + ->setTitleKey('report-reminder.title') + ->setBodyKey('report-reminder.body') + ->setParams([ + '{entityTypeName}' => $this->getEntityTypeName($entity), + '{entityStatus}' => str_replace('-', ' ', $entity->status), + '{feedback}' => $feedback, + ]); + } + + private function getEntityTypeName($entity): string + { + return ucwords(str_replace('-', ' ', Str::kebab(explode_pop('\\', get_class($entity))))); + } +} diff --git a/app/Mail/ResetPassword.php b/app/Mail/ResetPassword.php index c676e4aad..db09a4b0e 100644 --- a/app/Mail/ResetPassword.php +++ b/app/Mail/ResetPassword.php @@ -2,20 +2,18 @@ namespace App\Mail; -class ResetPassword extends Mail +class ResetPassword extends I18nMail { - public function __construct(String $token, string $callbackUrl = null) + public function __construct(String $token, string $callbackUrl = null, $user) { - $this->subject = 'RESET YOUR PASSWORD'; - $this->title = 'RESET YOUR PASSWORD'; - $this->body = - "You've requested a password reset.

" . - "Follow this link to reset your password. It's valid for 2 hours.

" . - 'If you have any questions, feel free to message us at info@terramatch.org.'; + parent::__construct($user); + $this->setSubjectKey('reset-password.subject') + ->setTitleKey('reset-password.title') + ->setBodyKey('reset-password.body') + ->setCta('reset-password.cta'); $this->link = $callbackUrl ? $callbackUrl . urlencode($token) : '/passwordReset?token=' . urlencode($token); - $this->cta = 'Reset Password'; $this->transactional = true; } } diff --git a/app/Mail/SatelliteMapCreated.php b/app/Mail/SatelliteMapCreated.php index c48845065..2f11e67ae 100644 --- a/app/Mail/SatelliteMapCreated.php +++ b/app/Mail/SatelliteMapCreated.php @@ -2,16 +2,16 @@ namespace App\Mail; -class SatelliteMapCreated extends Mail +class SatelliteMapCreated extends I18nMail { - public function __construct(Int $satelliteMapId, String $name) + public function __construct(Int $satelliteMapId, String $name, $user) { - $this->subject = 'Remote Sensing Map Received'; - $this->title = 'Remote Sensing Map Received'; - $this->body = - 'WRI has submitted an updated remote sensing map for ' . e($name) . '.

' . - 'Click below to view the map.'; + parent::__construct($user); + $this->setSubjectKey('satellite-map-created.subject') + ->setTitleKey('satellite-map-created.title') + ->setBodyKey('satellite-map-created.body') + ->setParams(['{name}' => e($name)]) + ->setCta('satellite-map-created.cta'); $this->link = '/monitoring/dashboard/?satelliteId=' . $satelliteMapId; - $this->cta = 'View Monitored Project'; } } diff --git a/app/Mail/TargetAccepted.php b/app/Mail/TargetAccepted.php index 1745eaefd..c0f31df47 100644 --- a/app/Mail/TargetAccepted.php +++ b/app/Mail/TargetAccepted.php @@ -2,17 +2,17 @@ namespace App\Mail; -class TargetAccepted extends Mail +class TargetAccepted extends I18nMail { - public function __construct(Int $monitoringId, String $name) + public function __construct(Int $monitoringId, String $name, $user) { - $this->subject = 'Monitoring Targets Approved'; + parent::__construct($user); + $this->setSubjectKey('target-accepted.subject') + ->setTitleKey('target-accepted.title') + ->setBodyKey('target-accepted.body') + ->setParams(['{name}' => e($name)]) + ->setCta('target-accepted.cta'); $this->banner = 'target_accepted'; - $this->title = 'Monitoring Targets Approved'; - $this->body = - 'Monitoring targets have been approved for ' . e($name) . '.

' . - 'Click below to view the newly unlocked project dashboard.'; $this->link = '/monitoring/dashboard/?monitoringId=' . $monitoringId; - $this->cta = 'View Monitored Project'; } } diff --git a/app/Mail/TargetCreated.php b/app/Mail/TargetCreated.php index da0b6967e..c0470b0dd 100644 --- a/app/Mail/TargetCreated.php +++ b/app/Mail/TargetCreated.php @@ -2,17 +2,16 @@ namespace App\Mail; -class TargetCreated extends Mail +class TargetCreated extends I18nMail { - public function __construct(Int $targetId, String $name) + public function __construct(Int $targetId, String $name, $user) { - $this->subject = 'Monitoring Targets Set'; - $this->title = 'Monitoring Targets Set'; - $this->body = - 'You have been sent monitoring targets to approve for ' . e($name) . '.

' . - 'Click below to view these targets.

' . - 'Note: you may need to update your funding status on TerraMatch to view.'; + parent::__construct($user); + $this->setSubjectKey('target-created.subject') + ->setTitleKey('target-created.title') + ->setBodyKey('target-created.body') + ->setParams(['{name}' => e($name)]) + ->setCta('target-created.cta'); $this->link = '/monitoring/review/?targetId=' . $targetId; - $this->cta = 'View Monitoring Terms'; } } diff --git a/app/Mail/TargetUpdated.php b/app/Mail/TargetUpdated.php index 47b36fc37..1bc6d44a3 100644 --- a/app/Mail/TargetUpdated.php +++ b/app/Mail/TargetUpdated.php @@ -2,16 +2,16 @@ namespace App\Mail; -class TargetUpdated extends Mail +class TargetUpdated extends I18nMail { - public function __construct(Int $targetId, String $name) + public function __construct(Int $targetId, String $name, $user) { - $this->subject = 'Monitoring Targets Need Review'; - $this->title = 'Monitoring Targets Need Review'; - $this->body = - 'Monitoring targets for ' . e($name) . ' have been edited and need reviewing.

' . - 'Click below to review the edited targets'; + parent::__construct($user); + $this->setSubjectKey('target-updated.subject') + ->setTitleKey('target-updated.title') + ->setBodyKey('target-updated.body') + ->setParams(['{name}' => e($name)]) + ->setCta('target-updated.cta'); $this->link = '/monitoring/review/?targetId=' . $targetId; - $this->cta = 'View Monitoring Terms'; } } diff --git a/app/Mail/TerrafundProgrammeSubmissionReceived.php b/app/Mail/TerrafundProgrammeSubmissionReceived.php index fe752df2a..9d078343e 100644 --- a/app/Mail/TerrafundProgrammeSubmissionReceived.php +++ b/app/Mail/TerrafundProgrammeSubmissionReceived.php @@ -2,18 +2,17 @@ namespace App\Mail; -class TerrafundProgrammeSubmissionReceived extends Mail +class TerrafundProgrammeSubmissionReceived extends I18nMail { public function __construct(int $id, String $name) { - $this->subject = 'Terrafund Programme Report Submitted'; - $this->title = 'Terrafund Programme Report Submitted'; - $this->body = - 'A new report has been submitted!

' . - e($name) . ' has a new report submited.

' . - 'Click below to view and edit the report.

'; + parent::__construct(null); + $this->setSubjectKey('terrafund-programme-submission-received.subject') + ->setTitleKey('terrafund-programme-submission-received.title') + ->setBodyKey('terrafund-programme-submission-received.body') + ->setParams(['{name}' => e($name)]) + ->setCta('terrafund-programme-submission-received.cta'); $this->link = '/admin/terrafundProgrammes/preview/?programmeId=' . $id; - $this->cta = 'View Report'; $this->transactional = true; } } diff --git a/app/Mail/TerrafundReportReminder.php b/app/Mail/TerrafundReportReminder.php index 73385ac8c..e58e9d2ce 100644 --- a/app/Mail/TerrafundReportReminder.php +++ b/app/Mail/TerrafundReportReminder.php @@ -2,18 +2,16 @@ namespace App\Mail; -class TerrafundReportReminder extends Mail +class TerrafundReportReminder extends I18nMail { - public function __construct(int $id) + public function __construct(int $id, $user) { - $this->subject = 'Terrafund Report Reminder'; - $this->title = 'YOU HAVE A REPORT DUE!'; - $this->body = 'Your next report is due on July 31. It should reflect any progress made between January 1, 2023 and June 30, 2022.

' . - 'If you have any questions, feel free to message us at info@terramatch.org.' . - '

---

' . - 'Votre prochain rapport doit être remis le 31 juillet. Il doit refléter tous les progrès réalisés entre le 1er janvier 2023 et le 30 juin 2023. '; + parent::__construct($user); + $this->setSubjectKey('terrafund-report-reminder.subject') + ->setTitleKey('terrafund-report-reminder.title') + ->setBodyKey('terrafund-report-reminder.body') + ->setCta('terrafund-report-reminder.cta'); $this->link = '/terrafund/programmeOverview/' . $id; - $this->cta = 'View Project'; $this->transactional = true; } diff --git a/app/Mail/TerrafundSiteAndNurseryReminder.php b/app/Mail/TerrafundSiteAndNurseryReminder.php index 17b2e6223..bc9f54f3d 100644 --- a/app/Mail/TerrafundSiteAndNurseryReminder.php +++ b/app/Mail/TerrafundSiteAndNurseryReminder.php @@ -2,19 +2,17 @@ namespace App\Mail; -class TerrafundSiteAndNurseryReminder extends Mail +class TerrafundSiteAndNurseryReminder extends I18nMail { - public function __construct(int $id) + public function __construct(int $id, $user) { - $this->subject = 'Terrafund Site & Nursery Reminder'; - $this->title = 'Terrafund Site & Nursery Reminder'; - - $this->body = - 'You haven\'t created any sites or nurseries for your project, reports are due in a month.

' . - 'Click below to create.

'; + parent::__construct($user); + $this->setSubjectKey('terrafund-site-and-nursery-reminder.subject') + ->setTitleKey('terrafund-site-and-nursery-reminder.title') + ->setBodyKey('terrafund-site-and-nursery-reminder.body') + ->setCta('terrafund-site-and-nursery-reminder.cta'); $this->link = '/terrafund/programmeOverview/' . $id; - $this->cta = 'Create a site or nursery'; $this->transactional = true; } diff --git a/app/Mail/Unmatch.php b/app/Mail/Unmatch.php index bb025e9c8..0e59a493c 100644 --- a/app/Mail/Unmatch.php +++ b/app/Mail/Unmatch.php @@ -4,10 +4,11 @@ use Exception; -class Unmatch extends Mail +class Unmatch extends I18nMail { - public function __construct(String $model, String $firstName = '', String $secondName = '') + public function __construct(String $model, String $firstName = '', String $secondName = '', $user) { + parent::__construct($user); switch ($model) { case 'Admin': $isAdmin = true; @@ -21,13 +22,15 @@ public function __construct(String $model, String $firstName = '', String $secon throw new Exception(); } if ($isAdmin) { - $this->subject = 'Unmatch Detected'; - $this->title = 'Unmatch Detected'; - $this->body = e($firstName) . ' and ' . e($secondName) . ' have unmatched.'; + $this->setSubjectKey('unmatch.subject-admin') + ->setTitleKey('unmatch.title-admin') + ->setBodyKey('unmatch.body-admin') + ->setParams(['{firstName}' => e($firstName), '{secondName}' => e($secondName)]); } else { - $this->subject = 'Someone Has Unmatched With One Of Your Projects'; - $this->title = 'Someone Has Unmatched With One Of Your Projects'; - $this->body = e($firstName) . ' has unmatched with one of your projects.'; + $this->setSubjectKey('unmatch.subject-user') + ->setTitleKey('unmatch.title-user') + ->setBodyKey('unmatch.body-user') + ->setParams(['{firstName}' => e($firstName)]); } } } diff --git a/app/Mail/UpcomingProgressUpdate.php b/app/Mail/UpcomingProgressUpdate.php index c82236f5f..4710f2163 100644 --- a/app/Mail/UpcomingProgressUpdate.php +++ b/app/Mail/UpcomingProgressUpdate.php @@ -2,16 +2,16 @@ namespace App\Mail; -class UpcomingProgressUpdate extends Mail +class UpcomingProgressUpdate extends I18nMail { - public function __construct(Int $monitoringId, String $pitchName) + public function __construct(Int $monitoringId, String $pitchName, $user) { - $this->subject = 'Report Due'; - $this->title = 'Report Due'; - $this->body = - ' You are due to submit a progress update report for ' . e($pitchName) . ' in 30 days.

' . - 'Click below to to create your report.'; + parent::__construct($user); + $this->setSubjectKey('upcoming-progress-update.subject') + ->setTitleKey('upcoming-progress-update.title') + ->setBodyKey('upcoming-progress-update.body') + ->setCta('upcoming-progress-update.cta') + ->setParams(['{pitchName}' => e($pitchName)]); $this->link = '/report/setup/' . $monitoringId; - $this->cta = 'Create Report'; } } diff --git a/app/Mail/UpdateVisibility.php b/app/Mail/UpdateVisibility.php index 45b9402be..d524cb1a6 100644 --- a/app/Mail/UpdateVisibility.php +++ b/app/Mail/UpdateVisibility.php @@ -4,10 +4,11 @@ use Exception; -class UpdateVisibility extends Mail +class UpdateVisibility extends I18nMail { - public function __construct(String $model, Int $id) + public function __construct(String $model, Int $id, $user) { + parent::__construct($user); switch ($model) { case 'Offer': $link = '/funding/' . $id; @@ -20,12 +21,10 @@ public function __construct(String $model, Int $id) default: throw new Exception(); } - $this->subject = "Update Your Project's Funding Status"; - $this->title = "Update Your Project's Funding Status"; - $this->body = - "It's been three days since someone matched with one of your projects. " . - 'Do you need to update its funding status?'; + $this->setSubjectKey('update-visibility.subject') + ->setTitleKey('update-visibility.title') + ->setBodyKey('update-visibility.body') + ->setCta('update-visibility.cta'); $this->link = $link; - $this->cta = 'Update Funding Status'; } } diff --git a/app/Mail/UserInvited.php b/app/Mail/UserInvited.php index 7e8672404..feb5fd4fd 100644 --- a/app/Mail/UserInvited.php +++ b/app/Mail/UserInvited.php @@ -4,32 +4,30 @@ use Exception; -class UserInvited extends Mail +class UserInvited extends I18nMail { public function __construct(String $emailAddress, String $type, String $callbackUrl = null) { + parent::__construct(null); switch ($type) { case 'Admin': - $prefix = "You've been invited to the administration."; + $this->setBodyKey('user-invited.body-admin'); break; case 'User': - $prefix = "You've been invited to an organisation."; + $this->setBodyKey('user-invited.body-user'); break; default: throw new Exception(); } - $this->subject = 'Create Your Account'; - $this->title = 'Create Your Account'; - $this->body = - $prefix . '

' . - 'Follow this link to create your account.'; + $this->setSubjectKey('user-invited.subject') + ->setTitleKey('user-invited.title') + ->setCta('user-invited.cta'); $this->link = $callbackUrl ? $callbackUrl . 'invite?emailAddress=' . urlencode($emailAddress) . '&type=' . urlencode(strtolower($type)) : '/invite?emailAddress=' . urlencode($emailAddress) . '&type=' . urlencode(strtolower($type)); - $this->cta = 'Create Account'; $this->transactional = true; } } diff --git a/app/Mail/UserVerification.php b/app/Mail/UserVerification.php index bb91f8eea..10e07f140 100644 --- a/app/Mail/UserVerification.php +++ b/app/Mail/UserVerification.php @@ -2,23 +2,18 @@ namespace App\Mail; -class UserVerification extends Mail +class UserVerification extends I18nMail { - public function __construct(String $token, string $callbackUrl = null) + public function __construct(String $token, string $callbackUrl = null, $user) { - $this->subject = 'Verify Your Email Address'; - $this->title = 'VERIFY YOUR EMAIL ADDRESS'; - $this->body = 'Follow the below link to verify your email address. It\'s valid for 48 hours. If the link does not work, log on ' . - 'to TerraMatch and resubmit a verfication request.
' . - 'If you continue to have problems accessing your account, feel free to message us at info@terramatch.org.' . - '

-----

' . - 'Suivez le lien ci-dessous pour vérifier votre adresse e-mail. Ce lien est valable pendant 48 heures. Si le lien ne fonctionne pas, ' . - 'connectez-vous à TerraMatch et soumettez à nouveau une demande de vérification.
' . - 'Si vous continuez à avoir des problèmes pour accéder à votre compte, n\'hésitez pas à nous envoyer un message à l\'adresse info@terramatch.org.'; + parent::__construct($user); + $this->setSubjectKey('user-verification.subject') + ->setTitleKey('user-verification.title') + ->setBodyKey('user-verification.body') + ->setCta('user-verification.cta'); $this->link = $callbackUrl ? $callbackUrl . urlencode($token) : '/verify?token=' . urlencode($token); - $this->cta = 'VERIFY EMAIL ADDRESS'; $this->transactional = true; } } diff --git a/app/Mail/V2ProjectInviteReceived.php b/app/Mail/V2ProjectInviteReceived.php index cd5a5273a..19aed7887 100644 --- a/app/Mail/V2ProjectInviteReceived.php +++ b/app/Mail/V2ProjectInviteReceived.php @@ -2,17 +2,15 @@ namespace App\Mail; -class V2ProjectInviteReceived extends Mail +class V2ProjectInviteReceived extends I18nMail { - public function __construct(String $name, String $nameOrganisation, String $callbackUrl) + public function __construct(String $name, String $nameOrganisation, String $callbackUrl, $user) { - $this->subject = 'You have been invited to join TerraMatch'; - $this->title = 'You have been invited to join TerraMatch'; - $this->body = - $nameOrganisation .' has invited you to join TerraMatch as a monitoring - partner to '. e($name) .'. Set an account password today to see the project’s - progress and access their latest reports.

- Reset your password Here.

'; + parent::__construct($user); + $this->setSubjectKey('project-invite-received.subject') + ->setTitleKey('project-invite-received.title') + ->setBodyKey('project-invite-received.body') + ->setParams(['{name}' => e($name), '{nameOrganisation}' => $nameOrganisation, '{callbackUrl}' => $callbackUrl]); $this->link = $callbackUrl ? $callbackUrl . '' : ''; diff --git a/app/Mail/V2ProjectMonitoringNotification.php b/app/Mail/V2ProjectMonitoringNotification.php index 62a44c30f..a2d4ae352 100644 --- a/app/Mail/V2ProjectMonitoringNotification.php +++ b/app/Mail/V2ProjectMonitoringNotification.php @@ -1,18 +1,20 @@ subject = 'You have been added as a monitoring partner.'; - $this->title = 'You have been added as a monitoring partner.'; - $this->body = - 'You have been added to'. e($name) .'as a monitoring partner on TerraMatch. Login into your account - today to see the project progress and relevant reports.

- Login Here.

'; + parent::__construct($user); + $this->setSubjectKey('v2-project-monitoring-notification.subject') + ->setTitleKey('v2-project-monitoring-notification.title') + ->setBodyKey('v2-project-monitoring-notification.body') + ->setParams(['{name}' => $name, '{callbackUrl}' => $callbackUrl]); + $this->transactional = false; $this->monitoring = true; + } } diff --git a/app/Mail/VersionApproved.php b/app/Mail/VersionApproved.php index 5f40247ce..b3cc30c29 100644 --- a/app/Mail/VersionApproved.php +++ b/app/Mail/VersionApproved.php @@ -6,10 +6,11 @@ use App\Models\Pitch as PitchModel; use Exception; -class VersionApproved extends Mail +class VersionApproved extends I18nMail { - public function __construct(String $model, Int $id) + public function __construct(String $model, Int $id, $user) { + parent::__construct($user); switch ($model) { case 'Organisation': $link = '/profile'; @@ -34,12 +35,11 @@ public function __construct(String $model, Int $id) if (is_null($version)) { throw new Exception(); } - $this->subject = 'Your Changes Have Been Approved'; - $this->title = 'Your Changes Have Been Approved'; - $this->body = - 'Your changes to ' . e($version->name) . ' have been approved.

' . - 'Follow this link to view the changes.'; + $this->setSubjectKey('version-approved.subject') + ->setTitleKey('version-approved.title') + ->setBodyKey('version-approved.body') + ->setParams(['{versionName}' => e($version->name)]) + ->setCta('version-approved.cta'); $this->link = $link; - $this->cta = 'View Changes'; } } diff --git a/app/Mail/VersionCreated.php b/app/Mail/VersionCreated.php index e42564a75..8e7c534b0 100644 --- a/app/Mail/VersionCreated.php +++ b/app/Mail/VersionCreated.php @@ -6,10 +6,11 @@ use App\Models\Pitch as PitchModel; use Exception; -class VersionCreated extends Mail +class VersionCreated extends I18nMail { - public function __construct(String $model, Int $id) + public function __construct(String $model, Int $id, $user) { + parent::__construct($user); switch ($model) { case 'Organisation': $link = '/admin/organization/preview/' . $id; @@ -34,11 +35,11 @@ public function __construct(String $model, Int $id) if (is_null($version)) { throw new Exception(); } - $this->subject = 'CHANGES REQUIRING YOUR APPROVAL '; - $this->title = 'Changes Requiring Your Approval'; - $this->body = - 'Changes have been made to ' . e($version->name) . '. Follow this link to review the changes.'; + $this->setSubjectKey('version-created.subject') + ->setTitleKey('version-created.title') + ->setBodyKey('version-created.body') + ->setParams(['{versionName}' => e($version->name)]) + ->setCta('version-created.cta'); $this->link = $link; - $this->cta = 'Review Changes'; } } diff --git a/app/Mail/VersionRejected.php b/app/Mail/VersionRejected.php index d364be6d8..9c51b287c 100644 --- a/app/Mail/VersionRejected.php +++ b/app/Mail/VersionRejected.php @@ -6,10 +6,11 @@ use App\Models\Pitch as PitchModel; use Exception; -class VersionRejected extends Mail +class VersionRejected extends I18nMail { - public function __construct(String $model, Int $id, String $explanation) + public function __construct(String $model, Int $id, String $explanation, $user) { + parent::__construct($user); switch ($model) { case 'Organisation': $link = '/profile'; @@ -34,11 +35,11 @@ public function __construct(String $model, Int $id, String $explanation) if (is_null($version)) { throw new Exception(); } - $this->subject = 'Your Changes Have Been Rejected'; - $this->title = 'Your Changes Have Been Rejected'; - $this->body = 'Your changes to ' . e($version->name) . ' have been rejected. ' . e($explanation) . '. Follow this link to view the changes.

' . - 'If you have any questions, feel free to message us at info@terramatch.org.'; + $this->setSubjectKey('version-rejected.subject') + ->setTitleKey('version-rejected.title') + ->setBodyKey('version-rejected.body') + ->setParams(['{versionName}' => e($version->name), '{explanation}' => e($explanation)]) + ->setCta('version-rejected.cta'); $this->link = $link; - $this->cta = 'View Changes'; } } diff --git a/app/Models/DTOs/AuditStatusDTO.php b/app/Models/DTOs/AuditStatusDTO.php index 33397feab..3009b36a8 100644 --- a/app/Models/DTOs/AuditStatusDTO.php +++ b/app/Models/DTOs/AuditStatusDTO.php @@ -52,13 +52,14 @@ public function __construct( public static function fromAudits($audit) { $user = self::getUserFromAudit($audit->user_id); + $comment = data_get($audit->new_values, 'status').': '.data_get($audit->new_values, 'feedback'); return new AuditStatusDTO( $audit->id, $audit->status, $user ? $user->first_name : null, $user ? $user->last_name : null, - $audit->new_values ? data_get($audit->new_values, 'feedback') : null, + $audit->new_values ? str_replace('-', ' ', $comment) : null, $audit->type, $audit->request_removed, $audit->created_at, diff --git a/app/Models/Traits/SaveAuditStatusTrait.php b/app/Models/Traits/SaveAuditStatusTrait.php index c45b133e3..7fb3d26a3 100644 --- a/app/Models/Traits/SaveAuditStatusTrait.php +++ b/app/Models/Traits/SaveAuditStatusTrait.php @@ -50,6 +50,16 @@ public function saveAuditStatusAdminMoreInfo($data, $entity) $this->saveAuditStatus(get_class($entity), $entity->id, $entity->status, $comment, 'change-request-updated', true); } + public function saveAuditStatusAdminRestorationInProgress($entity) + { + $this->saveAuditStatus(get_class($entity), $entity->id, $entity->status, 'Restoration In Progress', 'change-request-updated', true); + } + + public function saveAuditStatusAdminSendReminder($entity, $feedback) + { + $this->saveAuditStatus(get_class($entity), $entity->id, $entity->status, 'Feedback: '.$feedback, 'reminder-sent', true); + } + private function getApproveComment($data) { return 'Approve: '.data_get($data, 'feedback'); @@ -57,7 +67,7 @@ private function getApproveComment($data) private function getMoreInfoComment($data, $entity) { - $feedbackFields = data_get($data, 'feedback_fields'); + $feedbackFields = data_get($data, 'feedback_fields', []); $feedbackFieldLabels = []; foreach ($feedbackFields as $formQuestionUUID) { $question = FormQuestion::isUuid($formQuestionUUID)->first(); diff --git a/app/Models/V2/Forms/Application.php b/app/Models/V2/Forms/Application.php index 6ab424746..310d6ad60 100644 --- a/app/Models/V2/Forms/Application.php +++ b/app/Models/V2/Forms/Application.php @@ -13,14 +13,12 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; -use Laravel\Scout\Searchable; class Application extends Model { use HasFactory; use HasUuid; use SoftDeletes; - use Searchable; protected $fillable = [ 'organisation_uuid', @@ -67,6 +65,13 @@ public function toSearchableArray() ]; } + public static function searchApplications($query) + { + return self::select('applications.*') + ->join('organisations', 'applications.organisation_uuid', '=', 'organisations.uuid') + ->where('organisations.name', 'like', "%$query%"); + } + public function getRouteKeyName() { return 'uuid'; diff --git a/app/Models/V2/LocalizationKey.php b/app/Models/V2/LocalizationKey.php new file mode 100644 index 000000000..d4d6992fe --- /dev/null +++ b/app/Models/V2/LocalizationKey.php @@ -0,0 +1,32 @@ +belongsTo(I18nItem::class, 'value_id', 'id'); + } + + public function getTranslatedValueAttribute(): ?string + { + return $this->getTranslation('i18nValue', 'value'); + } +} diff --git a/app/Models/V2/Nurseries/Nursery.php b/app/Models/V2/Nurseries/Nursery.php index 847c9e829..3fea19790 100644 --- a/app/Models/V2/Nurseries/Nursery.php +++ b/app/Models/V2/Nurseries/Nursery.php @@ -26,7 +26,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Laravel\Scout\Searchable; use OwenIt\Auditing\Auditable; use OwenIt\Auditing\Contracts\Auditable as AuditableContract; use Spatie\MediaLibrary\InteractsWithMedia; @@ -38,7 +37,6 @@ class Nursery extends Model implements MediaModel, AuditableContract, EntityMode use HasFactory; use HasUuid; use SoftDeletes; - use Searchable; use HasLinkedFields; use UsesLinkedFields; use InteractsWithMedia; @@ -117,6 +115,16 @@ public function toSearchableArray() ]; } + public static function searchNurseries($query) + { + return self::select('v2_nurseries.*') + ->join('v2_projects', 'v2_nurseries.project_id', '=', 'v2_projects.id') + ->join('organisations', 'v2_projects.organisation_id', '=', 'organisations.id') + ->where('v2_nurseries.name', 'like', "%$query%") + ->orWhere('v2_projects.name', 'like', "%$query%") + ->orWhere('organisations.name', 'like', "%$query%"); + } + /** RELATIONS */ public function framework(): BelongsTo { diff --git a/app/Models/V2/Organisation.php b/app/Models/V2/Organisation.php index e38b28615..a6c8dfe12 100644 --- a/app/Models/V2/Organisation.php +++ b/app/Models/V2/Organisation.php @@ -19,7 +19,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Laravel\Scout\Searchable; use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\Tags\HasTags; @@ -32,7 +31,6 @@ class Organisation extends Model implements MediaModel use HasUuid; use HasStatus; use HasTypes; - use Searchable; use InteractsWithMedia; use HasV2MediaCollections; use SoftDeletes; @@ -220,6 +218,12 @@ public function toSearchableArray() ]; } + public static function searchOrganisations($query) + { + return self::select('organisations.*') + ->where('organisations.name', 'like', "%$query%"); + } + public function treeSpecies(): MorphMany { return $this->morphMany(TreeSpecies::class, 'speciesable') diff --git a/app/Models/V2/ProjectPitch.php b/app/Models/V2/ProjectPitch.php index e7df3ffa1..a2d13a492 100644 --- a/app/Models/V2/ProjectPitch.php +++ b/app/Models/V2/ProjectPitch.php @@ -13,7 +13,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Laravel\Scout\Searchable; use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\Tags\HasTags; @@ -24,7 +23,6 @@ class ProjectPitch extends Model implements MediaModel use HasUuid; use HasStatus; use SoftDeletes; - use Searchable; use InteractsWithMedia; use HasV2MediaCollections; use HasTags; @@ -115,6 +113,14 @@ public function toSearchableArray() ]; } + public static function searchProjectPitches($query) + { + return self::select('project_pitches.*') + ->join('organisations', 'project_pitches.organisation_id', '=', 'organisations.uuid') + ->where('project_pitches.project_name', 'like', "%$query%") + ->orwhere('organisations.name', 'like', "%$query%"); + } + public function getRouteKeyName() { return 'uuid'; diff --git a/app/Models/V2/Projects/Project.php b/app/Models/V2/Projects/Project.php index 9793d7180..3f6e68fb3 100644 --- a/app/Models/V2/Projects/Project.php +++ b/app/Models/V2/Projects/Project.php @@ -39,7 +39,6 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\DB; -use Laravel\Scout\Searchable; use OwenIt\Auditing\Auditable; use OwenIt\Auditing\Contracts\Auditable as AuditableContract; use Spatie\MediaLibrary\InteractsWithMedia; @@ -50,7 +49,6 @@ class Project extends Model implements MediaModel, AuditableContract, EntityMode use HasFactory; use HasUuid; use SoftDeletes; - use Searchable; use HasFrameworkKey; use HasLinkedFields; use UsesLinkedFields; @@ -510,6 +508,12 @@ public function toSearchableArray() ]; } + public static function searchProjects($query) + { + return self::select('v2_projects.*') + ->where('v2_projects.name', 'like', "%$query%"); + } + public function auditStatuses(): MorphMany { return $this->morphMany(AuditStatus::class, 'auditable'); diff --git a/app/Models/V2/ScheduledJobs/ReportReminderJob.php b/app/Models/V2/ScheduledJobs/ReportReminderJob.php index 0df608b02..6476c7fe7 100644 --- a/app/Models/V2/ScheduledJobs/ReportReminderJob.php +++ b/app/Models/V2/ScheduledJobs/ReportReminderJob.php @@ -7,6 +7,7 @@ use App\Models\V2\Projects\Project; use Carbon\Carbon; use Illuminate\Support\Facades\Mail; + use Parental\HasParent; /** @@ -38,8 +39,10 @@ protected function performJob(): void $query->whereHas('sites')->orWhereHas('nurseries'); })->chunkById(100, function ($projects) { $projects->each(function ($project) { - Mail::to($project->users->pluck('email_address'))->queue(new TerrafundReportReminder($project->id)); $project->users->each(function ($user) use ($project) { + Mail::to($user->email_address) + ->queue(new TerrafundReportReminder($project->id, $user)); + NotifyReportReminderJob::dispatch($user, $project, $this->framework_key); }); }); diff --git a/app/Models/V2/ScheduledJobs/ScheduledJob.php b/app/Models/V2/ScheduledJobs/ScheduledJob.php index 063ba9641..20b6d72ca 100644 --- a/app/Models/V2/ScheduledJobs/ScheduledJob.php +++ b/app/Models/V2/ScheduledJobs/ScheduledJob.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Log; + use Parental\HasChildren; /** diff --git a/app/Models/V2/ScheduledJobs/SiteAndNurseryReminderJob.php b/app/Models/V2/ScheduledJobs/SiteAndNurseryReminderJob.php index eebbd025e..f86e8aa79 100644 --- a/app/Models/V2/ScheduledJobs/SiteAndNurseryReminderJob.php +++ b/app/Models/V2/ScheduledJobs/SiteAndNurseryReminderJob.php @@ -37,9 +37,10 @@ protected function performJob(): void ->whereDoesntHave('nurseries') ->chunkById(100, function ($projects) { $projects->each(function ($project) { - if ($project->users->count() > 0) { - Mail::to($project->users->pluck('email_address'))->queue(new TerrafundSiteAndNurseryReminder($project->id)); - } + $project->users->each(function ($user) use ($project) { + Mail::to($user->email_address) + ->queue(new TerrafundSiteAndNurseryReminder($project->id, $user)); + }); }); }); } diff --git a/app/Models/V2/ScheduledJobs/TaskDueJob.php b/app/Models/V2/ScheduledJobs/TaskDueJob.php index 48454ada2..dffc0e390 100644 --- a/app/Models/V2/ScheduledJobs/TaskDueJob.php +++ b/app/Models/V2/ScheduledJobs/TaskDueJob.php @@ -10,6 +10,7 @@ use App\StateMachines\TaskStatusStateMachine; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; + use Parental\HasParent; /** diff --git a/app/Models/V2/Sites/Site.php b/app/Models/V2/Sites/Site.php index ed07e135b..21330c7f6 100644 --- a/app/Models/V2/Sites/Site.php +++ b/app/Models/V2/Sites/Site.php @@ -40,7 +40,6 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; -use Laravel\Scout\Searchable; use OwenIt\Auditing\Auditable; use OwenIt\Auditing\Contracts\Auditable as AuditableContract; use Spatie\MediaLibrary\InteractsWithMedia; @@ -55,7 +54,6 @@ class Site extends Model implements MediaModel, AuditableContract, EntityModel, use HasFactory; use HasUuid; use SoftDeletes; - use Searchable; use HasLinkedFields; use UsesLinkedFields; use InteractsWithMedia; @@ -200,6 +198,16 @@ public function toSearchableArray() ]; } + public static function searchSites($query) + { + return self::select('v2_sites.*') + ->join('v2_projects', 'v2_sites.project_id', '=', 'v2_projects.id') + ->join('organisations', 'v2_projects.organisation_id', '=', 'organisations.id') + ->where('v2_sites.name', 'like', "%$query%") + ->orWhere('v2_projects.name', 'like', "%$query%") + ->orWhere('organisations.name', 'like', "%$query%"); + } + /** * Overrides the method from HasEntityStatusScopesAndTransitions */ diff --git a/app/Models/V2/Sites/SitePolygon.php b/app/Models/V2/Sites/SitePolygon.php index 18e298cfd..9f60a7715 100644 --- a/app/Models/V2/Sites/SitePolygon.php +++ b/app/Models/V2/Sites/SitePolygon.php @@ -16,6 +16,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Znck\Eloquent\Relations\BelongsToThrough; use Znck\Eloquent\Traits\BelongsToThrough as BelongsToThroughTrait; @@ -119,13 +120,32 @@ public function createCopy(User $user, ?string $poly_id = null, ?bool $submit_po { $geometry = $this->polygonGeometry()->first(); SitePolygon::where('primary_uuid', $this->primary_uuid)->update(['is_active' => false]); + + $newSitePolygon = $this->replicate(); + $currentSite = Site::where('uuid', $newSitePolygon->site_id)->exists(); + if (! $currentSite) { + if (isset($properties['site_id'])) { + $siteExists = Site::where('uuid', $properties['site_id'])->exists(); + if (! $siteExists) { + Log::info('Provided Site UUID not found = ' . $properties['site_id'] . ' for SitePolygon UUID = ' . $newSitePolygon->uuid); + + return null; + } + Log::info($this->primary_uuid.'Changing sites from ' . $newSitePolygon->site_id . ' to ' . $properties['site_id']); + $newSitePolygon->site_id = $properties['site_id']; + } else { + Log::info('Site UUID not found = ' . $newSitePolygon->site_id . ' for SitePolygon UUID = ' . $newSitePolygon->uuid); + + return null; + } + } + if (! $poly_id) { $copyGeometry = PolygonGeometry::create([ 'geom' => $geometry->geom, 'created_by' => $user->id, ]); } - $newSitePolygon = $this->replicate(); $newSitePolygon->primary_uuid = $this->primary_uuid; $newSitePolygon->plantstart = $properties['plantstart'] ?? $this->plantstart; $newSitePolygon->plantend = $properties['plantend'] ?? $this->plantend; diff --git a/app/Models/V2/Sites/SiteReport.php b/app/Models/V2/Sites/SiteReport.php index 8b028c5e0..a3689b26e 100644 --- a/app/Models/V2/Sites/SiteReport.php +++ b/app/Models/V2/Sites/SiteReport.php @@ -383,6 +383,7 @@ public static function searchReports($query) ->join('organisations', 'v2_projects.organisation_id', '=', 'organisations.id') ->where('v2_projects.name', 'like', "%$query%") ->orWhere('organisations.name', 'like', "%$query%") + ->orWhere('v2_sites.name', 'like', "%$query%") ->get(); } } diff --git a/app/Models/V2/User.php b/app/Models/V2/User.php index db13d9b80..43d2f52dd 100644 --- a/app/Models/V2/User.php +++ b/app/Models/V2/User.php @@ -29,7 +29,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; -use Laravel\Scout\Searchable; use Spatie\Permission\Models\Role; use Spatie\Permission\Traits\HasRoles; use Tymon\JWTAuth\Contracts\JWTSubject; @@ -45,7 +44,6 @@ class User extends Authenticatable implements JWTSubject use InvitedAcceptedAndVerifiedScopesTrait; use HasFactory; use HasUuid; - use Searchable; use SoftDeletes; use HasRoles; @@ -75,6 +73,7 @@ class User extends Authenticatable implements JWTSubject 'api_key', 'country', 'program', + 'locale', ]; protected $casts = [ @@ -121,6 +120,16 @@ public function toSearchableArray() ]; } + public static function searchUsers($query) + { + return self::select('users.*') + ->leftJoin('organisations', 'users.organisation_id', '=', 'organisations.id') + ->where('organisations.name', 'like', "%$query%") + ->orWhere('users.first_name', 'like', "%$query%") + ->orWhere('users.last_name', 'like', "%$query%") + ->orWhere('users.email_address', 'like', "%$query%"); + } + public function getJWTIdentifier(): string { return $this->getKey(); diff --git a/app/Models/V2/Workdays/Workday.php b/app/Models/V2/Workdays/Workday.php index 6cdfc231b..e33d8de37 100644 --- a/app/Models/V2/Workdays/Workday.php +++ b/app/Models/V2/Workdays/Workday.php @@ -123,7 +123,7 @@ public static function syncRelation(EntityModel $entity, string $property, $data // Make sure the incoming data is clean, and meets our expectations of one row per type/subtype/name combo. // The FE is not supposed to send us data with duplicates, but there has been a bug in the past that caused // this problem, so this extra check is just covering our bases. - $syncData = collect($workdayData['demographics'])->reduce(function ($syncData, $row) { + $syncData = isset($workdayData['demographics']) ? collect($workdayData['demographics'])->reduce(function ($syncData, $row) { $type = data_get($row, 'type'); $subtype = data_get($row, 'subtype'); $name = data_get($row, 'name'); @@ -144,7 +144,7 @@ public static function syncRelation(EntityModel $entity, string $property, $data $syncData[] = $row; return $syncData; - }, []); + }, []) : []; $demographics = $workday->demographics; $represented = collect(); diff --git a/app/Services/PolygonService.php b/app/Services/PolygonService.php index f4f66497f..78661aa85 100755 --- a/app/Services/PolygonService.php +++ b/app/Services/PolygonService.php @@ -15,10 +15,12 @@ use App\Validators\SitePolygonValidator; use DateTime; use Exception; +use Illuminate\Http\Response; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use InvalidArgumentException; class PolygonService @@ -129,50 +131,66 @@ public function processEntity($entity) public function createGeojsonModels($geojson, $sitePolygonProperties = [], ?string $primary_uuid = null, ?bool $submit_polygon_loaded = false): array { - if (data_get($geojson, 'features.0.geometry.type') == 'Point') { - return $this->transformAndStorePoints($geojson, $sitePolygonProperties); - } + try { + if (data_get($geojson, 'features.0.geometry.type') == 'Point') { + return $this->transformAndStorePoints($geojson, $sitePolygonProperties); + } - $uuids = []; - foreach ($geojson['features'] as $feature) { - if ($feature['geometry']['type'] === 'Polygon') { - $data = $this->insertSinglePolygon($feature['geometry']); - $uuids[] = $data['uuid']; - $sitePolygonProperties['area'] = $data['area']; - if ($submit_polygon_loaded) { - $this->insertPolygon($data['uuid'], $sitePolygonProperties, $feature['properties'], $feature['properties']['uuid'], $submit_polygon_loaded); - } else { - $this->insertPolygon($data['uuid'], $sitePolygonProperties, $feature['properties'], $primary_uuid); - } - } elseif ($feature['geometry']['type'] === 'MultiPolygon') { - foreach ($feature['geometry']['coordinates'] as $polygon) { - $singlePolygon = ['type' => 'Polygon', 'coordinates' => $polygon]; - $data = $this->insertSinglePolygon($singlePolygon); + $uuids = []; + foreach ($geojson['features'] as $feature) { + if ($feature['geometry']['type'] === 'Polygon') { + $data = $this->insertSinglePolygon($feature['geometry']); $uuids[] = $data['uuid']; - if ($submit_polygon_loaded) { + $sitePolygonProperties['area'] = $data['area']; + if ($submit_polygon_loaded && isset($feature['properties']['uuid'])) { $this->insertPolygon($data['uuid'], $sitePolygonProperties, $feature['properties'], $feature['properties']['uuid'], $submit_polygon_loaded); } else { $this->insertPolygon($data['uuid'], $sitePolygonProperties, $feature['properties'], $primary_uuid); } + } elseif ($feature['geometry']['type'] === 'MultiPolygon') { + foreach ($feature['geometry']['coordinates'] as $polygon) { + $singlePolygon = ['type' => 'Polygon', 'coordinates' => $polygon]; + $data = $this->insertSinglePolygon($singlePolygon); + $uuids[] = $data['uuid']; + if ($submit_polygon_loaded && isset($feature['properties']['uuid'])) { + $this->insertPolygon($data['uuid'], $sitePolygonProperties, $feature['properties'], $feature['properties']['uuid'], $submit_polygon_loaded); + } else { + $this->insertPolygon($data['uuid'], $sitePolygonProperties, $feature['properties'], $primary_uuid); + } + } } } - } - return $uuids; + return $uuids; + } catch (\Exception $e) { + return response()->json(['error at create geojson models' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR); + } } private function insertPolygon($uuid, $sitePolygonProperties, $featureProperties, ?string $primary_uuid, ?bool $submit_polygon_loaded = false) { - if (isset($featureProperties['site_id']) && isset($sitePolygonProperties['site_id'])) { - $featureProperties['site_id'] = $sitePolygonProperties['site_id']; - } - if($primary_uuid) { - $this->insertSitePolygonVersion($uuid, $primary_uuid, $submit_polygon_loaded, $featureProperties); - } else { - $this->insertSitePolygon( - $uuid, - array_merge($sitePolygonProperties, $featureProperties), - ); + try { + if (isset($featureProperties['site_id']) && isset($sitePolygonProperties['site_id']) && $sitePolygonProperties['site_id'] !== null) { + $featureProperties['site_id'] = $sitePolygonProperties['site_id']; + } + if($primary_uuid) { + $result = $this->insertSitePolygonVersion($uuid, $primary_uuid, $submit_polygon_loaded, $featureProperties); + if ($result === false) { + $this->insertSitePolygon( + $uuid, + array_merge($sitePolygonProperties, $featureProperties) + ); + } + } else { + $this->insertSitePolygon( + $uuid, + array_merge($sitePolygonProperties, $featureProperties), + ); + } + } catch (\Exception $e) { + Log::error('Error inserting polygon', ['uuid' => $uuid, 'primary_uuid' => $primary_uuid, 'submit_polygon_loaded' => $submit_polygon_loaded, 'error' => $e->getMessage()]); + + return response()->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR); } } @@ -272,6 +290,11 @@ protected function insertSitePolygon(string $polygonUuid, array $properties) ], )); $site = $sitePolygon->site()->first(); + if (! $site) { + Log::error('Site not found', ['site polygon uuid' => $sitePolygon->uuid, 'site id' => $sitePolygon->site_id]); + + return response()->json(['error' => 'Site not found'], Response::HTTP_INTERNAL_SERVER_ERROR); + } $site->restorationInProgress(); $project = $sitePolygon->project()->first(); $geometryHelper = new GeometryHelper(); @@ -286,21 +309,38 @@ protected function insertSitePolygon(string $polygonUuid, array $properties) protected function insertSitePolygonVersion(string $polygonUuid, string $primary_uuid, ?bool $submit_polygon_loaded = false, ?array $properties) { try { - $sitePolygon = SitePolygon::isUuid($primary_uuid)->first(); + $sitePolygon = SitePolygon::isUuid($primary_uuid)->active()->first(); if (! $sitePolygon) { - return response()->json(['error' => 'Site polygon not found'], 404); + return false; + } + $user = Auth::check() ? Auth::user() : null; + + if ($user) { + $user = User::isUuid($user->uuid)->first(); + } else { + $user = User::find(1); } - $user = User::isUuid(Auth::user()->uuid)->first(); $newSitePolygon = $sitePolygon->createCopy($user, $polygonUuid, $submit_polygon_loaded, $properties); + if (! $newSitePolygon) { + return false; + } $site = $newSitePolygon->site()->first(); + if (! $site) { + Log::error('Site not found', ['site polygon uuid' => $newSitePolygon->uuid, 'site id' => $newSitePolygon->site_id]); + + return false; + + } $site->restorationInProgress(); $project = $newSitePolygon->project()->first(); $geometryHelper = new GeometryHelper(); $geometryHelper->updateProjectCentroid($project->uuid); - return null; + return true; } catch (\Exception $e) { - return $e->getMessage(); + Log::error('Error inserting site polygon version', ['polygon uuid' => $polygonUuid, 'error' => $e->getMessage()]); + + return response()->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR); } } diff --git a/app/Validators/Extensions/Polygons/EstimatedArea.php b/app/Validators/Extensions/Polygons/EstimatedArea.php index 324d125fd..45ebfcfcd 100644 --- a/app/Validators/Extensions/Polygons/EstimatedArea.php +++ b/app/Validators/Extensions/Polygons/EstimatedArea.php @@ -5,6 +5,8 @@ use App\Models\V2\Projects\Project; use App\Models\V2\Sites\SitePolygon; use App\Validators\Extensions\Extension; +use Exception; +use Illuminate\Support\Facades\Log; class EstimatedArea extends Extension { @@ -29,7 +31,29 @@ public static function getAreaData(string $polygonUuid): array if ($sitePolygon == null) { return ['valid' => false, 'error' => 'Site polygon not found for the given polygon ID', 'status' => 404]; } + $siteData = self::generateAreaDataSite($sitePolygon); + $projectData = self::generateAreaDataProject($sitePolygon); + $valid = $siteData['valid'] || $projectData['valid']; + // Construct the result array + return [ + 'valid' => $valid, + 'total_area_site' => $siteData['total_area_site'] ?? null, + 'total_area_project' => $projectData['total_area_project'] ?? null, + 'extra_info' => [ + 'sum_area_site' => $siteData['extra_info']['sum_area_site'] ?? null, + 'sum_area_project' => $projectData['extra_info']['sum_area_project'] ?? null, + 'percentage_site' => $siteData['extra_info']['percentage_site'] ?? null, + 'percentage_project' => $projectData['extra_info']['percentage_project'] ?? null, + 'total_area_site' => $siteData['extra_info']['total_area_site'] ?? null, + 'total_area_project' => $projectData['extra_info']['total_area_project'] ?? null, + ], + 'insertion_success' => true, // Assuming 'insertion_success' is always true for now + ]; + } + + public static function generateAreaDataProject($sitePolygon): array + { $project = $sitePolygon->project; if ($project == null) { return [ @@ -40,8 +64,16 @@ public static function getAreaData(string $polygonUuid): array ]; } - if (empty($project->total_hectares_restored_goal)) { - return ['valid' => false, 'error' => 'Total hectares restored goal not set for the project', 'status' => 500]; + if (empty($project->total_hectares_restored_goal) || ! $project->total_hectares_restored_goal) { + return [ + 'valid' => false, + 'total_area_project' => $project->total_hectares_restored_goal, + 'extra_info' => [ + 'sum_area_project' => null, + 'percentage_project' => null, + 'total_area_project' => null, + ], + ]; } $sumEstArea = $project->sitePolygons()->sum('calc_area'); @@ -52,8 +84,8 @@ public static function getAreaData(string $polygonUuid): array $sumEstArea = round($sumEstArea); $percentage = round($percentage); $extra_info = [ - 'sum_area' => $sumEstArea, - 'percentage' => $percentage, + 'sum_area_project' => $sumEstArea, + 'percentage_project' => $percentage, 'total_area_project' => $project->total_hectares_restored_goal, ]; @@ -65,6 +97,70 @@ public static function getAreaData(string $polygonUuid): array ]; } + public static function getAreaDataProject(string $polygonUuid): array + { + $sitePolygon = SitePolygon::forPolygonGeometry($polygonUuid)->first(); + if ($sitePolygon == null) { + return ['valid' => false, 'error' => 'Site polygon not found for the given polygon ID', 'status' => 404]; + } + + return self::generateAreaDataProject($sitePolygon); + } + + public static function generateAreaDataSite($sitePolygon): array + { + $site = $sitePolygon->site; + $sumEstArea = $site->sitePolygons()->sum('calc_area'); + $lowerBound = self::LOWER_BOUND_MULTIPLIER * $site->hectares_to_restore_goal; + $upperBound = self::UPPER_BOUND_MULTIPLIER * $site->hectares_to_restore_goal; + $valid = $sumEstArea >= $lowerBound && $sumEstArea <= $upperBound; + if (! $site->hectares_to_restore_goal) { + return [ + 'valid' => false, + 'total_area_site' => $site->hectares_to_restore_goal, + 'extra_info' => [ + 'sum_area' => null, + 'percentage' => null, + 'total_area_site' => null, + ], + ]; + } + $percentage = ($sumEstArea / $site->hectares_to_restore_goal) * 100; + $sumEstArea = round($sumEstArea); + $percentage = round($percentage); + $extra_info = [ + 'sum_area_site' => $sumEstArea, + 'percentage_site' => $percentage, + 'total_area_site' => $site->hectares_to_restore_goal, + ]; + + return [ + 'valid' => $valid, + 'sum_area_site' => $sumEstArea, + 'total_area_site' => $site->hectares_to_restore_goal, + 'extra_info' => $extra_info, + ]; + } + + public static function getAreaDataSite(string $polygonUuid): array + { + $sitePolygon = SitePolygon::forPolygonGeometry($polygonUuid)->first(); + if ($sitePolygon == null) { + return ['valid' => false, 'error' => 'Site polygon not found for the given polygon ID', 'status' => 404]; + } + + try { + return self::generateAreaDataSite($sitePolygon); + } catch (Exception $e) { + Log::error($e->getMessage()); + + return [ + 'valid' => false, + 'error' => 'Error while getting site data', + ]; + } + } + public static function getAreaDataWithSiteID(string $siteUuid): array { $sitePolygon = SitePolygon::where('site_id', $siteUuid)->first(); diff --git a/database/factories/V2/UserFactory.php b/database/factories/V2/UserFactory.php index f058e66e9..ab3020261 100644 --- a/database/factories/V2/UserFactory.php +++ b/database/factories/V2/UserFactory.php @@ -28,6 +28,7 @@ public function definition() 'email_address_verified_at' => now(), 'password' => Hash::make('password'), 'job_role' => 'Manager', + 'locale' => 'en-US', 'phone_number' => $this->faker->phoneNumber(), 'whatsapp_phone' => $this->faker->phoneNumber(), 'organisation_id' => Organisation::factory(['status' => Organisation::STATUS_APPROVED])->create()->id, diff --git a/database/migrations/2024_07_30_160948_alter_user_table_add_locale_column.php b/database/migrations/2024_07_30_160948_alter_user_table_add_locale_column.php new file mode 100644 index 000000000..bda02d363 --- /dev/null +++ b/database/migrations/2024_07_30_160948_alter_user_table_add_locale_column.php @@ -0,0 +1,27 @@ +string('locale')->default('en-US'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('locale'); + }); + } +}; diff --git a/database/migrations/2024_08_07_144251_add_localization_keys_table.php b/database/migrations/2024_08_07_144251_add_localization_keys_table.php new file mode 100644 index 000000000..78c37332e --- /dev/null +++ b/database/migrations/2024_08_07_144251_add_localization_keys_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('key'); + $table->text('value'); + $table->integer('value_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('localization_keys'); + } +}; diff --git a/database/migrations/2024_09_09_192124_add_fields_to_media_table.php b/database/migrations/2024_09_09_192124_add_fields_to_media_table.php new file mode 100644 index 000000000..a9354dd31 --- /dev/null +++ b/database/migrations/2024_09_09_192124_add_fields_to_media_table.php @@ -0,0 +1,35 @@ +boolean('is_cover')->default(false)->after('is_public'); + $table->string('photographer', 100)->nullable()->after('order_column'); + $table->string('description', 500)->nullable()->after('photographer'); + $table->string('tag')->nullable()->after('description'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('media', function (Blueprint $table) { + $table->dropColumn(['is_cover', 'photographer', 'description', 'tag']); + }); + } +} diff --git a/database/migrations/2024_09_17_101030_alter_audit_statuses_table_type_new_column_reminder.php b/database/migrations/2024_09_17_101030_alter_audit_statuses_table_type_new_column_reminder.php new file mode 100644 index 000000000..f474d58b5 --- /dev/null +++ b/database/migrations/2024_09_17_101030_alter_audit_statuses_table_type_new_column_reminder.php @@ -0,0 +1,21 @@ +createLocalizationKey('application-submitted-confirmation.subject', 'Your Application Has Been Submitted'); + $this->createLocalizationKey('application-submitted-confirmation.title', 'Your Application Has Been Submitted!'); + + // form-submission-approved + $this->createLocalizationKey('form-submission-approved.subject', 'Application Approved'); + $this->createLocalizationKey('form-submission-approved.title', 'Your application has been approved'); + $this->createLocalizationKey('form-submission-approved.body', 'Your application has been approved.'); + $this->createLocalizationKey('form-submission-approved.body-feedback', 'Your application has been approved. Please see comments below:

'); + + // form-submission-rejected + $this->createLocalizationKey('form-submission-rejected.subject', 'Application Status Update'); + $this->createLocalizationKey('form-submission-rejected.title', 'THANK YOU FOR YOUR APPLICATION'); + $this->createLocalizationKey('form-submission-rejected.body', 'After careful review, our team has decided your application will not move forward.'); + $this->createLocalizationKey('form-submission-rejected.body-feedback', 'After careful review, our team has decided your application will not move forward. Please see the comments below for more details or any follow-up resources.

{feedback}'); + + // entity-status-change + $this->createLocalizationKey('entity-status-change.subject-approved', 'Your {entityTypeName} Has Been Approved'); + $this->createLocalizationKey('entity-status-change.subject-needs-more-information', 'There is More Information Requested About Your {entityTypeName}'); + $this->createLocalizationKey('entity-status-change.body-report-approved', 'Thank you for submitting your {parentEntityName} report.' . + '

The information has been reviewed by your project manager and has been approved.

{feedback}' . + '

If you have any additional questions please reach out to your project manager or to info@terramatch.org

'); + $this->createLocalizationKey('entity-status-change.body-report-needs-more-information', 'Thank you for submitting your {parentEntityName} report.' . + '

The information has been reviewed by your project manager and they would like to see the following updates:

{feedback}' . + '

If you have any additional questions please reach out to your project manager or to info@terramatch.org

'); + $this->createLocalizationKey('entity-status-change.body-entity-approved', 'Thank you for submitting your {lowerEntityTypeName} information for {entityName}.' . + '

The information has been reviewed by your project manager and has been approved.

{feedback}' . + '

If you have any additional questions please reach out to your project manager or to info@terramatch.org

'); + $this->createLocalizationKey('entity-status-change.body-entity-needs-more-information', 'Thank you for submitting your {lowerEntityTypeName} information for {entityName}.' . + '

The information has been reviewed by your project manager and they would like to see the following updates:

{feedback}' . + '

If you have any additional questions please reach out to your project manager or to info@terramatch.org

'); + $this->createLocalizationKey('entity-status-change.cta', 'View {entityTypeName}'); + // + + // form-submission-feedback-received + $this->createLocalizationKey('form-submission-feedback-received.subject', 'You have received feedback on your application'); + $this->createLocalizationKey('form-submission-feedback-received.title', 'You have received feedback on your application'); + $this->createLocalizationKey('form-submission-feedback-received.body', 'Your application requires more information.'); + $this->createLocalizationKey('form-submission-feedback-received.body-feedback', 'Your application requires more information. Please see comments below:

{feedback}'); + + // form-submission-final-stage-approved + $this->createLocalizationKey('form-submission-final-stage-approved.subject', 'Application Approved'); + $this->createLocalizationKey('form-submission-final-stage-approved.title', 'Your application has been approved'); + $this->createLocalizationKey('form-submission-final-stage-approved.body', 'Your application has successfully passed all stages of our evaluation process and has been officially approved. + If you have any immediate queries, please do not hesitate to reach out to our support team.'); + $this->createLocalizationKey('form-submission-final-stage-approved.body-feedback', 'Your application has successfully passed all stages of our evaluation process and has been officially approved. Please see the comments below:

{feedback}' . + '

If you have any immediate queries, please do not hesitate to reach out to our dedicated support team.'); + + // form-submission-submitted + $this->createLocalizationKey('form-submission-submitted.subject', 'You have submitted an application'); + $this->createLocalizationKey('form-submission-submitted.title', 'You have submitted an application'); + $this->createLocalizationKey('form-submission-submitted.body', 'Your application has been submitted'); + + // interest-shown + $this->createLocalizationKey('interest-shown.subject', 'Someone Has Shown Interest In One Of Your Projects'); + $this->createLocalizationKey('interest-shown.title', 'Someone Has Shown Interest In One Of Your Projects'); + $this->createLocalizationKey('interest-shown.body', '{name} has shown interest in one of your projects.

' . + 'Follow this link to view their project.'); + $this->createLocalizationKey('interest-shown.cta', 'View Project'); + + // match-mail + $this->createLocalizationKey('match-mail.subject-admin', 'Match Detected'); + $this->createLocalizationKey('match-mail.title-admin', 'Match Detected'); + $this->createLocalizationKey('match-mail.body-admin', '{firstName} and {secondName} have matched.

Follow this link to view the match.'); + $this->createLocalizationKey('match-mail.cta-admin', 'View Match'); + + $this->createLocalizationKey('match-mail.subject-funder', 'Someone Has Matched With One Of Your Funding Offers'); + $this->createLocalizationKey('match-mail.title-funder', 'Someone Has Matched With One Of Your Funding Offers'); + $this->createLocalizationKey('match-mail.body-funder', 'Congratulations! {firstName} has matched with one of your funding offers.

' . + 'Follow the link below to view their contact details.

' . + 'If you have decided to move forward together, we encourage you to monitor your project on TerraMatch. Our monitoring system allows you to set mutually agreed targets, easily report on project progress using our templates, and access WRI\'s state-of-the-art satellite monitoring so that you can track progress over the long term.

' . + 'Check out the monitoring section at TerraMatch.org.'); + $this->createLocalizationKey('match-mail.subject-user', 'Someone Has Matched With One Of Your Projects'); + $this->createLocalizationKey('match-mail.title-user', 'Someone Has Matched With One Of Your Projects'); + $this->createLocalizationKey('match-mail.body-user', 'Congratulations! {firstName} has matched with one of your projects.

' . + 'Follow the link below to view their contact details.

' . + 'If you have decided to move forward together, we encourage you to monitor your project on TerraMatch. Our monitoring system allows you to set mutually agreed targets, easily report on project progress using our templates, and access WRI\'s state-of-the-art satellite monitoring to show the funder that your trees are surviving.

' . + 'Check out the monitoring section at TerraMatch.org.'); + $this->createLocalizationKey('match-mail.cta', 'View Contact Details'); + + // organisation-approved + $this->createLocalizationKey('organisation-approved.subject', 'Your organization has been accepted into TerraMatch.'); + $this->createLocalizationKey('organisation-approved.title', 'YOUR ORGANIZATION HAS BEEN ACCEPTED INTO TERRAMATCH.'); + $this->createLocalizationKey('organisation-approved.body', 'Please login to submit an application or report on a monitored project. If you have any questions, please reach out to info@terramatch.org'); + $this->createLocalizationKey('organisation-approved.cta', 'LOGIN'); + + // organisation-rejected + $this->createLocalizationKey('organisation-rejected.subject', 'Your organization has been rejected from joining TerraMatch.'); + $this->createLocalizationKey('organisation-rejected.title', 'Your organization has been rejected from joining TerraMatch.'); + $this->createLocalizationKey('organisation-rejected.body', 'This could be due to the fact that your organization is already on TerraMatch, + your organization will not benefit from the services that TerraMatch provides + or we do not have enough information to understand what your organization does. + Please login to TerraMatch to view a more detail description about why your + organization request has been rejected.'); + + // organisation-submit-confirmation + $this->createLocalizationKey('organisation-submit-confirmation.subject', 'Organization request submitted to TerraMatch.'); + $this->createLocalizationKey('organisation-submit-confirmation.title', 'ORGANIZATION REQUEST SUBMITTED TO TERRAMATCH.'); + $this->createLocalizationKey('organisation-submit-confirmation.body', 'Your organization has been submitted and is in review with WRI. You can continue to use the platform to whilst your ' . + 'application is in review. If you have any questions, feel free to message us at info@terramatch.org.

' . + '

--
' . + 'Votre organisation a été soumise et est en cours d\'examen par le WRI. Vous pouvez continuer à utiliser la plateforme pendant que ' . + 'votre demande est en cours d\'examen. Si vous avez des questions, n\'hésitez pas à nous envoyer un message à info@terramatch.org.'); + + // organisation-user-approved + $this->createLocalizationKey('organisation-user-approved.subject', 'You have been accepted to join {organisationName} on TerraMatch'); + $this->createLocalizationKey('organisation-user-approved.title', 'You have been accepted to join {organisationName} on TerraMatch'); + $this->createLocalizationKey('organisation-user-approved.body', 'You have been accepted to join {organisationName} on TerraMatch. Log-in to view or update your organization’s information.'); + + // organisation-user-join-requested + $this->createLocalizationKey('organisation-user-join-requested.subject', 'A user has requested to join your organization'); + $this->createLocalizationKey('organisation-user-join-requested.title', 'A user has requested to join your organization'); + $this->createLocalizationKey('organisation-user-join-requested.body', 'A user has requested to join your organization. Please go to the ‘Meet the Team’ page to review this request.'); + + // organisation-user-rejected + $this->createLocalizationKey('organisation-user-rejected.subject', 'Your request to join {organisationName} on TerraMatch has been rejected'); + $this->createLocalizationKey('organisation-user-rejected.title', 'Your request to join {organisationName} on TerraMatch has been rejected'); + $this->createLocalizationKey('organisation-user-rejected.body', 'Your request to join {organisationName} on TerraMatch has been rejected.

' . + 'Please set-up a new organizational profile on TerraMatch if you wish to join the platform. ' . + 'Please reach out the help center here if you need more information: https://terramatchsupport.zendesk.com/hc/en-us/requests/new'); + + // progress-update-created + $this->createLocalizationKey('progress-update-created.subject', 'Report Received'); + $this->createLocalizationKey('progress-update-created.title', 'Report Received'); + $this->createLocalizationKey('progress-update-created.body', 'A new progress update report has been submitted for {pitchName}.

' . + 'Click below to view the report.'); + $this->createLocalizationKey('progress-update-created.cta', 'View Report'); + + // project-invite-received + $this->createLocalizationKey('project-invite-received.subject', 'Project Invite'); + $this->createLocalizationKey('project-invite-received.title', 'Project Invite'); + $this->createLocalizationKey('project-invite-received.body', 'You have been sent an invite to join {name}.

' . + 'Click below to accept the invite.

'); + $this->createLocalizationKey('project-invite-received.cta', 'Accept invite'); + + // project-updated + $this->createLocalizationKey('project-updated.subject-match', 'A Project You\'ve Matched With Has Changed'); + $this->createLocalizationKey('project-updated.title-match', 'A Project You\'ve Matched With Has Changed'); + $this->createLocalizationKey('project-updated.subject-interest', 'A Project You\'re Interested In Has Changed'); + $this->createLocalizationKey('project-updated.title-interest', 'A Project You\'re Interested In Has Changed'); + $this->createLocalizationKey('project-updated.body', '{name} has changed.

' . + 'You may want to review the changes to ensure you\'re still interested.

' . + 'Follow this link to view the project.'); + $this->createLocalizationKey('project-updated.cta', 'View Project'); + + // reset-password + $this->createLocalizationKey('reset-password.subject', 'RESET YOUR PASSWORD'); + $this->createLocalizationKey('reset-password.title', 'RESET YOUR PASSWORD'); + $this->createLocalizationKey('reset-password.body', 'You\'ve requested a password reset.

' . + 'Follow this link to reset your password. It\'s valid for 2 hours.

' . + 'If you have any questions, feel free to message us at info@terramatch.org.'); + $this->createLocalizationKey('reset-password.cta', 'Reset Password'); + + // satellite-map-created + $this->createLocalizationKey('satellite-map-created.subject', 'Remote Sensing Map Received'); + $this->createLocalizationKey('satellite-map-created.title', 'Remote Sensing Map Received'); + $this->createLocalizationKey('satellite-map-created.body', 'WRI has submitted an updated remote sensing map for {name}.

' . + 'Click below to view the map.'); + $this->createLocalizationKey('satellite-map-created.cta', 'View Monitored Project'); + + // target-accepted + $this->createLocalizationKey('target-accepted.subject', 'Monitoring Targets Approved'); + $this->createLocalizationKey('target-accepted.title', 'Monitoring Targets Approved'); + $this->createLocalizationKey('target-accepted.body', 'Monitoring targets have been approved for {name}.

' . + 'Click below to view the newly unlocked project dashboard.'); + $this->createLocalizationKey('target-accepted.cta', 'View Monitored Project'); + + // target-created + $this->createLocalizationKey('target-created.subject', 'Monitoring Targets Set'); + $this->createLocalizationKey('target-created.title', 'Monitoring Targets Set'); + $this->createLocalizationKey('target-created.body', 'You have been sent monitoring targets to approve for {name}.

' . + 'Click below to view these targets.

' . + 'Note: you may need to update your funding status on TerraMatch to view.'); + $this->createLocalizationKey('target-created.cta', 'View Monitoring Terms'); + + // target-updated + $this->createLocalizationKey('target-updated.subject', 'Monitoring Targets Need Review'); + $this->createLocalizationKey('target-updated.title', 'Monitoring Targets Need Review'); + $this->createLocalizationKey('target-updated.body', 'Monitoring targets for {name} have been edited and need reviewing.

' . + 'Click below to review the edited targets.'); + $this->createLocalizationKey('target-updated.cta', 'View Monitoring Terms'); + + // terrafund-programme-submission-received + $this->createLocalizationKey('terrafund-programme-submission-received.subject', 'Terrafund Programme Report Submitted'); + $this->createLocalizationKey('terrafund-programme-submission-received.title', 'Terrafund Programme Report Submitted'); + $this->createLocalizationKey('terrafund-programme-submission-received.body', 'A new report has been submitted!

' . + '{name} has a new report submited.

' . + 'Click below to view and edit the report.

'); + $this->createLocalizationKey('terrafund-programme-submission-received.cta', 'View Report'); + + // terrafund-report-reminder + $this->createLocalizationKey('terrafund-report-reminder.subject', 'Terrafund Report Reminder'); + $this->createLocalizationKey('terrafund-report-reminder.title', 'YOU HAVE A REPORT DUE!'); + $this->createLocalizationKey('terrafund-report-reminder.body', 'Your next report is due on July 31. It should reflect any progress made between January 1, 2023 and June 30, 2022.

' . + 'If you have any questions, feel free to message us at info@terramatch.org.' . + '

---

' . + 'Votre prochain rapport doit être remis le 31 juillet. Il doit refléter tous les progrès réalisés entre le 1er janvier 2023 et le 30 juin 2023. '); + $this->createLocalizationKey('terrafund-report-reminder.cta', 'View Project'); + + // terrafund-site-and-nursery-reminder + $this->createLocalizationKey('terrafund-site-and-nursery-reminder.subject', 'Terrafund Site & Nursery Reminder'); + $this->createLocalizationKey('terrafund-site-and-nursery-reminder.title', 'Terrafund Site & Nursery Reminder'); + $this->createLocalizationKey('terrafund-site-and-nursery-reminder.body', 'You haven\'t created any sites or nurseries for your project, reports are due in a month.

' . + 'Click below to create.

'); + $this->createLocalizationKey('terrafund-site-and-nursery-reminder.cta', 'Create a site or nursery'); + + // unmatch + $this->createLocalizationKey('unmatch.subject-admin', 'Unmatch Detected'); + $this->createLocalizationKey('unmatch.title-admin', 'Unmatch Detected'); + $this->createLocalizationKey('unmatch.body-admin', '{firstName} and {secondName} have unmatched.'); + + $this->createLocalizationKey('unmatch.subject-user', 'Someone Has Unmatched With One Of Your Projects'); + $this->createLocalizationKey('unmatch.title-user', 'Someone Has Unmatched With One Of Your Projects'); + $this->createLocalizationKey('unmatch.body-user', '{firstName} has unmatched with one of your projects.'); + + //upcoming-progress-update + $this->createLocalizationKey('upcoming-progress-update.subject', 'Report Due'); + $this->createLocalizationKey('upcoming-progress-update.title', 'Report Due'); + $this->createLocalizationKey('upcoming-progress-update.body', ' You are due to submit a progress update report for {pitchName} in 30 days.

' . + 'Click below to to create your report.'); + $this->createLocalizationKey('upcoming-progress-update.cta', 'Create Report'); + + //update-visibility + $this->createLocalizationKey('update-visibility.subject', 'Update Your Project\'s Funding Status'); + $this->createLocalizationKey('update-visibility.title', 'Update Your Project\'s Funding Status'); + $this->createLocalizationKey('update-visibility.body', 'It\'s been three days since someone matched with one of your projects. ' . + 'Do you need to update its funding status?'); + $this->createLocalizationKey('update-visibility.cta', 'Update Funding Status'); + + //user-invited + $this->createLocalizationKey('user-invited.subject', 'Create Your Account'); + $this->createLocalizationKey('user-invited.title', 'Create Your Account'); + $this->createLocalizationKey('user-invited.body-admin', 'You\'ve been invited to the administration.

' . + 'Follow this link to create your account.'); + $this->createLocalizationKey('user-invited.body-user', 'You\'ve been invited to an organisation.

' . + 'Follow this link to create your account.'); + $this->createLocalizationKey('user-invited.cta', 'Create Account'); + + //user-verification + $this->createLocalizationKey('user-verification.subject', 'Verify Your Email Address'); + $this->createLocalizationKey('user-verification.title', 'VERIFY YOUR EMAIL ADDRESS'); + $this->createLocalizationKey('user-verification.body', 'Follow the below link to verify your email address. It\'s valid for 48 hours. If the link does not work, log on ' . + 'to TerraMatch and resubmit a verfication request.
' . + 'If you continue to have problems accessing your account, feel free to message us at info@terramatch.org.' . + '

-----

' . + 'Suivez le lien ci-dessous pour vérifier votre adresse e-mail. Ce lien est valable pendant 48 heures. Si le lien ne fonctionne pas, ' . + 'connectez-vous à TerraMatch et soumettez à nouveau une demande de vérification.
' . + 'Si vous continuez à avoir des problèmes pour accéder à votre compte, n\'hésitez pas à nous envoyer un message à l\'adresse info@terramatch.org.'); + $this->createLocalizationKey('user-verification.cta', 'VERIFY EMAIL ADDRESS'); + + //v2-project-invite-received + $this->createLocalizationKey('v2-project-invite-received.subject', 'You have been invited to join TerraMatch'); + $this->createLocalizationKey('v2-project-invite-received.title', 'You have been invited to join TerraMatch'); + $this->createLocalizationKey('v2-project-invite-received.body', '{organisationName} has invited you to join TerraMatch as a monitoring + partner to {name}. Set an account password today to see the project’s + progress and access their latest reports.

+ Reset your password Here.

'); + + //v2-project-monitoring-notification + $this->createLocalizationKey('v2-project-monitoring-notification.subject', 'You have been added as a monitoring partner.'); + $this->createLocalizationKey('v2-project-monitoring-notification.title', 'You have been added as a monitoring partner.'); + $this->createLocalizationKey('v2-project-monitoring-notification.body', 'You have been added to {name} as a monitoring partner on TerraMatch. Login into your account + today to see the project progress and relevant reports.

+ Login Here.

'); + + // version-approved + $this->createLocalizationKey('version-approved.subject', 'Your Changes Have Been Approved'); + $this->createLocalizationKey('version-approved.title', 'Your Changes Have Been Approved'); + $this->createLocalizationKey('version-approved.body', 'Your changes to {versionName} have been approved.

' . + 'Follow this link to view the changes.'); + $this->createLocalizationKey('version-approved.cta', 'View Changes'); + + // version-created + $this->createLocalizationKey('version-created.subject', 'CHANGES REQUIRING YOUR APPROVAL '); + $this->createLocalizationKey('version-created.title', 'Changes Requiring Your Approval'); + $this->createLocalizationKey('version-created.body', 'Changes have been made to {versionName}. Follow this link to review the changes.'); + $this->createLocalizationKey('version-created.cta', 'Review Changes'); + + // version-rejected + $this->createLocalizationKey('version-rejected.subject', 'Your Changes Have Been Rejected'); + $this->createLocalizationKey('version-rejected.title', 'Your Changes Have Been Rejected'); + $this->createLocalizationKey('version-rejected.body', 'Your changes to {versionName} have been rejected. {explanation}. Follow this link to view the changes.

' . + 'If you have any questions, feel free to message us at info@terramatch.org.'); + $this->createLocalizationKey('version-rejected.cta', 'View Changes'); + + // report-reminder + $this->createLocalizationKey('report-reminder.subject', 'Reminder: Your {entityTypeName} Still Needs Your Input'); + $this->createLocalizationKey('report-reminder.title', 'Reminder: Your {entityTypeName} Still Needs Your Input'); + $this->createLocalizationKey('report-reminder.body', 'This is a reminder that your {entityTypeName} still has the status {entityStatus}. Below you will see a note from your project manager about the report.

+ If you have any questions, please reach out to your project manager or to info@terramatch.org.

{feedback}'); + + // //project-manager-project + $this->createLocalizationKey('project-manager-project.subject', 'Please Review Project Profile Update'); + $this->createLocalizationKey('project-manager-project.title', 'Please Review Project Profile Update'); + $this->createLocalizationKey('project-manager-project.body', 'The {projectName}. has submitted an update to their project that needs to be reviewed. '. + 'Please review the project and either accept the submission or request for more information.

'. + 'You are receiving this message because you are associated with this project as a Project Manager in TerraMatch. '. + 'If you wish to no longer recieve these messages or have any issues seeing or responding to the changes, please reach out to info@terramatch.org'); + $this->createLocalizationKey('project-manager-project.cta', 'View {entityTypeName}'); + + //project-manager-site + $this->createLocalizationKey('project-manager-site.subject', 'A Site Has Been Submitted for Your Review'); + $this->createLocalizationKey('project-manager-site.title', 'A Site Has Been Submitted for Your Review'); + $this->createLocalizationKey('project-manager-site.body', 'The project {projectName} has submitted the site {entityName} for your review. '. + 'Please review the site and either accept the submission or request for more information.

'. + 'You are receiving this message because you are associated with this project as a Project Manager in TerraMatch. '. + 'If you wish to no longer recieve these messages or have any issues seeing or responding to the changes, please reach out to info@terramatch.org'); + $this->createLocalizationKey('project-manager-site.cta', 'View {entityTypeName}'); + + //project-manager-nursery + $this->createLocalizationKey('project-manager-nursery.subject', 'A Nursery Has Been Submitted for Your Review'); + $this->createLocalizationKey('project-manager-nursery.title', 'A Nursery Has Been Submitted for Your Review'); + $this->createLocalizationKey('project-manager-nursery.body', 'The project {projectName} has submitted the nursery {entityName} for your review. '. + 'Please review the nursery and either accept the submission or request for more information.

'. + 'You are receiving this message because you are associated with this project as a Project Manager in TerraMatch. If you wish to no longer recieve these messages or have any issues seeing or responding to the changes, please reach out to info@terramatch.org'); + $this->createLocalizationKey('project-manager-nursery.cta', 'View {entityTypeName}'); + + } + + public function createLocalizationKey($key, $value): void + { + if (LocalizationKey::where('key', operator: $key)->exists()) { + return; + } + + $localizationKey = LocalizationKey::create([ + 'key' => $key, + 'value' => $value, + ]); + + $localizationKey->value_id = I18nHelper::generateI18nItem($localizationKey, 'value'); + $localizationKey->save(); + } +} diff --git a/openapi-src/V2/definitions/EntityReportReminder.yml b/openapi-src/V2/definitions/EntityReportReminder.yml new file mode 100644 index 000000000..0c1697a05 --- /dev/null +++ b/openapi-src/V2/definitions/EntityReportReminder.yml @@ -0,0 +1,5 @@ +title: EntityReportReminder +type: object +properties: + feedback: + type: string \ No newline at end of file diff --git a/openapi-src/V2/definitions/FileResource.yml b/openapi-src/V2/definitions/FileResource.yml new file mode 100644 index 000000000..40105ee5e --- /dev/null +++ b/openapi-src/V2/definitions/FileResource.yml @@ -0,0 +1,42 @@ +type: object +properties: + id: + type: integer + model_id: + type: integer + model_type: + type: string + collection_name: + type: string + name: + type: string + file_name: + type: string + mime_type: + type: string + disk: + type: string + size: + type: integer + manipulations: + type: object + custom_properties: + type: object + responsive_images: + type: object + order_column: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + description: + type: string + photographer: + type: string + is_public: + type: boolean + is_cover: + type: boolean \ No newline at end of file diff --git a/openapi-src/V2/definitions/MeResponse.yml b/openapi-src/V2/definitions/MeResponse.yml index 598fac22a..39615f3e6 100644 --- a/openapi-src/V2/definitions/MeResponse.yml +++ b/openapi-src/V2/definitions/MeResponse.yml @@ -13,6 +13,8 @@ properties: format: date-time role: type: string + locale: + type: string organisation: $ref: './_index.yml#/MyOrganisationLite' frameworks: diff --git a/openapi-src/V2/definitions/UpdateMediaRequest.yml b/openapi-src/V2/definitions/UpdateMediaRequest.yml new file mode 100644 index 000000000..a360fb278 --- /dev/null +++ b/openapi-src/V2/definitions/UpdateMediaRequest.yml @@ -0,0 +1,14 @@ +type: object +properties: + name: + type: string + description: Name of the media + description: + type: string + description: New description for the media + photographer: + type: string + description: Name of the photographer + is_public: + type: boolean + description: Whether the media is public or not \ No newline at end of file diff --git a/openapi-src/V2/definitions/V2FileGallery.yml b/openapi-src/V2/definitions/V2FileGallery.yml index 8791c4ba5..5c5ab5b6a 100644 --- a/openapi-src/V2/definitions/V2FileGallery.yml +++ b/openapi-src/V2/definitions/V2FileGallery.yml @@ -15,6 +15,8 @@ properties: type: string is_public: type: boolean + is_cover: + type: boolean location: type: object properties: diff --git a/openapi-src/V2/definitions/_index.yml b/openapi-src/V2/definitions/_index.yml index 6f3841e04..650d1e547 100644 --- a/openapi-src/V2/definitions/_index.yml +++ b/openapi-src/V2/definitions/_index.yml @@ -250,6 +250,8 @@ V2TaskActionRead: $ref: './V2TaskActionRead.yml' StatusUpdate: $ref: './StatusUpdate.yml' +EntityReportReminder: + $ref: './EntityReportReminder.yml' V2ProjectInviteRead: $ref: './V2ProjectInviteRead.yml' V2ProjectInviteCreate: @@ -370,3 +372,7 @@ MeResponse: $ref: './MeResponse.yml' MyOrganisationLite: $ref: './MyOrganisationLite.yml' +UpdateMediaRequest: + $ref: './UpdateMediaRequest.yml' +FileResource: + $ref: './FileResource.yml' \ No newline at end of file diff --git a/openapi-src/V2/paths/Entity/post-v2-admin-entity-uuid-reminder.yml b/openapi-src/V2/paths/Entity/post-v2-admin-entity-uuid-reminder.yml new file mode 100644 index 000000000..80f2835f2 --- /dev/null +++ b/openapi-src/V2/paths/Entity/post-v2-admin-entity-uuid-reminder.yml @@ -0,0 +1,23 @@ +summary: Send reminder to an entity report +operationId: post-v2-admin-entity-uuid-reminder +tags: + - V2 Project Reports + - V2 Site Reports + - V2 Nursery Reports +parameters: + - type: string + name: ENTITY + in: path + required: true + description: "allowed values are project-reports, site-reports, nursery-reports" + - type: string + name: UUID + in: path + required: true + - in: body + name: body + schema: + $ref: '../../definitions/_index.yml#/EntityReportReminder' +responses: + '200': + description: OK \ No newline at end of file diff --git a/openapi-src/V2/paths/Exports/post-v2-export-image.yml b/openapi-src/V2/paths/Exports/post-v2-export-image.yml new file mode 100644 index 000000000..e46579fdc --- /dev/null +++ b/openapi-src/V2/paths/Exports/post-v2-export-image.yml @@ -0,0 +1,30 @@ +operationId: exportImage +summary: Download an image from a provided URL +tags: + - V2 Exports +consumes: + - application/json +produces: + - image/jpeg +parameters: + - in: body + name: body + description: JSON object containing the image URL. + required: true + schema: + type: object + required: + - imageUrl + properties: + imageUrl: + type: string + description: The URL of the image to be downloaded. +responses: + '200': + description: OK + schema: + type: file + '400': + description: Invalid URL provided + '500': + description: Server error \ No newline at end of file diff --git a/openapi-src/V2/paths/Media/patch-v2-media-project-mediaUuid.yml b/openapi-src/V2/paths/Media/patch-v2-media-project-mediaUuid.yml new file mode 100644 index 000000000..b4dedb40d --- /dev/null +++ b/openapi-src/V2/paths/Media/patch-v2-media-project-mediaUuid.yml @@ -0,0 +1,51 @@ +tags: + - Media +summary: Updates the "is_cover" status of a media item for a project. +description: This endpoint allows you to update a specific media item's "is_cover" field to true. +parameters: + - name: project + in: path + required: true + type: string + description: The ID or UUID of the project. + - name: mediaUuid + in: path + required: true + type: string + description: The UUID of the media item. + - name: body + in: body + required: true + schema: + type: object + properties: + _method: + type: string + description: This allows PATCH requests via form submissions, used for Laravel. + example: "PATCH" +responses: + 200: + description: Cover image updated successfully. + schema: + type: object + properties: + message: + type: string + mediaUuid: + type: string + 404: + description: Media not found. + schema: + type: object + properties: + message: + type: string + 403: + description: Authorization error, unauthorized access. + schema: + type: object + properties: + message: + type: string + 500: + description: Server error. \ No newline at end of file diff --git a/openapi-src/V2/paths/Media/patch-v2-media-uuid.yml b/openapi-src/V2/paths/Media/patch-v2-media-uuid.yml new file mode 100644 index 000000000..0169877a8 --- /dev/null +++ b/openapi-src/V2/paths/Media/patch-v2-media-uuid.yml @@ -0,0 +1,31 @@ +tags: + - Media +summary: Update media attributes +description: Update description, photographer, is_public, and is_cover attributes of a media item +produces: + - application/json +parameters: + - name: uuid + in: path + description: UUID of the media to update + required: true + type: string + - name: body + in: body + description: Media attributes to update + required: true + schema: + $ref: '../../definitions/_index.yml#/UpdateMediaRequest' +responses: + 200: + description: Successful operation + schema: + $ref: '../../definitions/_index.yml#/FileResource' + 401: + description: Unauthorized + 403: + description: Forbidden + 404: + description: Media not found + 422: + description: Validation error \ No newline at end of file diff --git a/openapi-src/V2/paths/Users/post-v2-users-locale.yml b/openapi-src/V2/paths/Users/post-v2-users-locale.yml new file mode 100644 index 000000000..105c9d0b6 --- /dev/null +++ b/openapi-src/V2/paths/Users/post-v2-users-locale.yml @@ -0,0 +1,22 @@ +summary: Updates user locale +operationId: post-v2-users-locale +tags: + - V2 Locale +parameters: + - in: body + name: locale + required: true + description: locale used could be one of en-US,es-MX,fr-FR,pt-BR + schema: + type: object + properties: + locale: + type: string +responses: + '200': + description: OK + schema: + type: object + properties: + message: + type: string diff --git a/openapi-src/V2/paths/_index.yml b/openapi-src/V2/paths/_index.yml index f498986eb..335799e0b 100644 --- a/openapi-src/V2/paths/_index.yml +++ b/openapi-src/V2/paths/_index.yml @@ -99,6 +99,9 @@ /v2/admin/{ENTITY}/{UUID}/{STATUS}: put: $ref: './Entity/put-v2-admin-entity-uuid-status.yml' +/v2/admin/{ENTITY}/{UUID}/reminder: + post: + $ref: './Entity/post-v2-admin-entity-uuid-reminder.yml' /v2/update-requests/{UUID}: get: $ref: './UpdateRequests/get-v2-update-requests-uuid.yml' @@ -2515,6 +2518,15 @@ /v2/admin/audits/{ENTITY}/{UUID}: get: $ref: './Audits/get-v2-admin-audits-entity-uuid.yml' +/v2/export-image: + post: + $ref: './Exports/post-v2-export-image.yml' +/v2/media/{uuid}: + patch: + $ref: './Media/patch-v2-media-uuid.yml' +/v2/media/project/{project}/{mediaUuid}: + patch: + $ref: './Media/patch-v2-media-project-mediaUuid.yml' /v2/admin/{ENTITY}/export/{FRAMEWORK}: get: $ref: './Exports/get-v2-admin-entity-export-framework.yml' @@ -2733,9 +2745,12 @@ /v2/site-polygon/{uuid}/make-active: put: $ref: './Polygons/put-v2-site-polygon-uuid-make-active.yml' +/v2/users/locale: + patch: + $ref: './Users/post-v2-users-locale.yml' /v2/terrafund/clip-polygons/polygon/{uuid}: post: $ref: './Terrafund/post-v2-terrafund-clip-polygons-polygon-uuid.yml' /v2/terrafund/clip-polygons/site/{uuid}: post: - $ref: './Terrafund/post-v2-terrafund-clip-polygons-site-uuid.yml' \ No newline at end of file + $ref: './Terrafund/post-v2-terrafund-clip-polygons-site-uuid.yml' diff --git a/resources/docs/swagger-v2.yml b/resources/docs/swagger-v2.yml index cf21cbc48..37feb6faa 100644 --- a/resources/docs/swagger-v2.yml +++ b/resources/docs/swagger-v2.yml @@ -6584,6 +6584,8 @@ definitions: type: string is_public: type: boolean + is_cover: + type: boolean location: type: object properties: @@ -41201,6 +41203,12 @@ definitions: type: array items: type: string + EntityReportReminder: + title: EntityReportReminder + type: object + properties: + feedback: + type: string V2ProjectInviteRead: title: V2ProjectInviteRead type: object @@ -42723,6 +42731,8 @@ definitions: format: date-time role: type: string + locale: + type: string organisation: title: MyOrganisationLite type: object @@ -42772,6 +42782,64 @@ definitions: type: string updated_at: type: string + UpdateMediaRequest: + type: object + properties: + name: + type: string + description: Name of the media + description: + type: string + description: New description for the media + photographer: + type: string + description: Name of the photographer + is_public: + type: boolean + description: Whether the media is public or not + FileResource: + type: object + properties: + id: + type: integer + model_id: + type: integer + model_type: + type: string + collection_name: + type: string + name: + type: string + file_name: + type: string + mime_type: + type: string + disk: + type: string + size: + type: integer + manipulations: + type: object + custom_properties: + type: object + responsive_images: + type: object + order_column: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + description: + type: string + photographer: + type: string + is_public: + type: boolean + is_cover: + type: boolean paths: '/v2/tree-species/{entity}/{UUID}': get: @@ -55887,6 +55955,35 @@ paths: responses: '200': description: OK + '/v2/admin/{ENTITY}/{UUID}/reminder': + post: + summary: Send reminder to an entity report + operationId: post-v2-admin-entity-uuid-reminder + tags: + - V2 Project Reports + - V2 Site Reports + - V2 Nursery Reports + parameters: + - type: string + name: ENTITY + in: path + required: true + description: 'allowed values are project-reports, site-reports, nursery-reports' + - type: string + name: UUID + in: path + required: true + - in: body + name: body + schema: + title: EntityReportReminder + type: object + properties: + feedback: + type: string + responses: + '200': + description: OK '/v2/update-requests/{UUID}': get: summary: View a specific Update Request @@ -66822,6 +66919,8 @@ paths: type: string is_public: type: boolean + is_cover: + type: boolean location: type: object properties: @@ -87608,6 +87707,8 @@ paths: format: date-time role: type: string + locale: + type: string organisation: title: MyOrganisationLite type: object @@ -89326,6 +89427,178 @@ paths: type: integer unfiltered_total: type: integer + /v2/export-image: + post: + operationId: exportImage + summary: Download an image from a provided URL + tags: + - V2 Exports + consumes: + - application/json + produces: + - image/jpeg + parameters: + - in: body + name: body + description: JSON object containing the image URL. + required: true + schema: + type: object + required: + - imageUrl + properties: + imageUrl: + type: string + description: The URL of the image to be downloaded. + responses: + '200': + description: OK + schema: + type: file + '400': + description: Invalid URL provided + '500': + description: Server error + '/v2/media/{uuid}': + patch: + tags: + - Media + summary: Update media attributes + description: 'Update description, photographer, is_public, and is_cover attributes of a media item' + produces: + - application/json + parameters: + - name: uuid + in: path + description: UUID of the media to update + required: true + type: string + - name: body + in: body + description: Media attributes to update + required: true + schema: + type: object + properties: + name: + type: string + description: Name of the media + description: + type: string + description: New description for the media + photographer: + type: string + description: Name of the photographer + is_public: + type: boolean + description: Whether the media is public or not + responses: + '200': + description: Successful operation + schema: + type: object + properties: + id: + type: integer + model_id: + type: integer + model_type: + type: string + collection_name: + type: string + name: + type: string + file_name: + type: string + mime_type: + type: string + disk: + type: string + size: + type: integer + manipulations: + type: object + custom_properties: + type: object + responsive_images: + type: object + order_column: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + description: + type: string + photographer: + type: string + is_public: + type: boolean + is_cover: + type: boolean + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Media not found + '422': + description: Validation error + '/v2/media/project/{project}/{mediaUuid}': + patch: + tags: + - Media + summary: Updates the "is_cover" status of a media item for a project. + description: This endpoint allows you to update a specific media item's "is_cover" field to true. + parameters: + - name: project + in: path + required: true + type: string + description: The ID or UUID of the project. + - name: mediaUuid + in: path + required: true + type: string + description: The UUID of the media item. + - name: body + in: body + required: true + schema: + type: object + properties: + _method: + type: string + description: 'This allows PATCH requests via form submissions, used for Laravel.' + example: PATCH + responses: + '200': + description: Cover image updated successfully. + schema: + type: object + properties: + message: + type: string + mediaUuid: + type: string + '403': + description: 'Authorization error, unauthorized access.' + schema: + type: object + properties: + message: + type: string + '404': + description: Media not found. + schema: + type: object + properties: + message: + type: string + '500': + description: Server error. '/v2/admin/{ENTITY}/export/{FRAMEWORK}': get: operationId: get-v2-admin-entity-export-framework.yml @@ -92867,6 +93140,30 @@ paths: type: boolean version_name: type: string + /v2/users/locale: + patch: + summary: Updates user locale + operationId: post-v2-users-locale + tags: + - V2 Locale + parameters: + - in: body + name: locale + required: true + description: 'locale used could be one of en-US,es-MX,fr-FR,pt-BR' + schema: + type: object + properties: + locale: + type: string + responses: + '200': + description: OK + schema: + type: object + properties: + message: + type: string '/v2/terrafund/clip-polygons/polygon/{uuid}': post: summary: Clip Overlapping Polygons by Polygon diff --git a/routes/api_v2.php b/routes/api_v2.php index baac624cf..9292ab84b 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -35,6 +35,7 @@ use App\Http\Controllers\V2\Dashboard\ViewRestorationStrategyController; use App\Http\Controllers\V2\Dashboard\ViewTreeRestorationGoalController; use App\Http\Controllers\V2\Dashboard\VolunteersAndAverageSurvivalRateController; +use App\Http\Controllers\V2\Entities\AdminSendReminderController; use App\Http\Controllers\V2\Entities\AdminSoftDeleteEntityController; use App\Http\Controllers\V2\Entities\AdminStatusEntityController; use App\Http\Controllers\V2\Entities\EntityTypeController; @@ -47,6 +48,7 @@ use App\Http\Controllers\V2\Exports\ExportAllNurseryDataAsProjectDeveloperController; use App\Http\Controllers\V2\Exports\ExportAllProjectDataAsProjectDeveloperController; use App\Http\Controllers\V2\Exports\ExportAllSiteDataAsProjectDeveloperController; +use App\Http\Controllers\V2\Exports\ExportImageController; use App\Http\Controllers\V2\Exports\ExportProjectEntityAsProjectDeveloperController; use App\Http\Controllers\V2\Exports\ExportReportEntityAsProjectDeveloperController; use App\Http\Controllers\V2\Files\FilePropertiesController; @@ -212,6 +214,7 @@ use App\Http\Controllers\V2\User\CompleteActionController; use App\Http\Controllers\V2\User\IndexMyActionsController; use App\Http\Controllers\V2\User\UpdateMyBannersController; +use App\Http\Controllers\V2\UserLocaleController; use App\Http\Controllers\V2\Workdays\GetWorkdaysForEntityController; use App\Http\Middleware\ModelInterfaceBindingMiddleware; use App\Models\V2\AuditableModel; @@ -263,6 +266,8 @@ Route::delete('', [MediaController::class, 'bulkDelete']); Route::delete('/{uuid}', [MediaController::class, 'delete']); Route::delete('/{uuid}/{collection}', [MediaController::class, 'delete']); + Route::patch('/{uuid}', [MediaController::class, 'updateMedia']); + Route::patch('project/{project}/{mediaUuid}', [MediaController::class, 'updateIsCover']); }); /** ADMIN ONLY ROUTES */ @@ -326,6 +331,7 @@ ModelInterfaceBindingMiddleware::with(EntityModel::class, function () { Route::put('/{entity}/{status}', AdminStatusEntityController::class); + Route::post('/{entity}/reminder', AdminSendReminderController::class); Route::delete('/{entity}', AdminSoftDeleteEntityController::class); }); @@ -637,7 +643,8 @@ Route::post('/polygon/{uuid}', [TerrafundCreateGeometryController::class, 'processGeometry']); Route::get('/geojson/complete', [TerrafundCreateGeometryController::class, 'getPolygonAsGeoJSONDownload']); Route::get('/geojson/site', [TerrafundCreateGeometryController::class, 'getAllPolygonsAsGeoJSONDownload']); - + Route::get('/geojson/all-active', [TerrafundCreateGeometryController::class, 'downloadGeojsonAllActivePolygons']); + Route::get('/geojson/all-by-framework', [TerrafundCreateGeometryController::class, 'downloadAllActivePolygonsByFramework']); Route::get('/validation/self-intersection', [TerrafundCreateGeometryController::class, 'checkSelfIntersection']); Route::get('/validation/size-limit', [TerrafundCreateGeometryController::class, 'validatePolygonSize']); @@ -648,6 +655,8 @@ Route::get('/validation/criteria-data', [TerrafundCreateGeometryController::class, 'getCriteriaData']); Route::get('/validation/overlapping', [TerrafundCreateGeometryController::class, 'validateOverlapping']); Route::get('/validation/estimated-area', [TerrafundCreateGeometryController::class, 'validateEstimatedArea']); + Route::get('/validation/estimated-area-project', [TerrafundCreateGeometryController::class, 'validateEstimatedAreaProject']); + Route::get('/validation/estimated-area-site', [TerrafundCreateGeometryController::class, 'validateEstimatedAreaSite']); Route::get('/validation/table-data', [TerrafundCreateGeometryController::class, 'validateDataInDB']); Route::post('/validation/polygon', [TerrafundCreateGeometryController::class, 'getValidationPolygon']); Route::post('/validation/sitePolygons', [TerrafundCreateGeometryController::class, 'getSiteValidationPolygon']); @@ -687,6 +696,8 @@ function () { modelParameter: 'mediaModel' ); +Route::post('/export-image', ExportImageController::class); + Route::resource('files', FilePropertiesController::class); //Route::put('file/{uuid}', [FilePropertiesController::class, 'update']); //Route::delete('file/{uuid}', [FilePropertiesController::class, 'destroy']); @@ -741,3 +752,5 @@ function () { }); Route::get('/type-entity', EntityTypeController::class); + +Route::patch('/users/locale', UserLocaleController::class); diff --git a/tests/Unit/Models/V2/ScheduledJobs/ScheduledJobsTest.php b/tests/Unit/Models/V2/ScheduledJobs/ScheduledJobsTest.php index 77716a442..ab6c0eca6 100644 --- a/tests/Unit/Models/V2/ScheduledJobs/ScheduledJobsTest.php +++ b/tests/Unit/Models/V2/ScheduledJobs/ScheduledJobsTest.php @@ -82,7 +82,7 @@ public function test_report_reminder() public function test_site_and_nursery_reminder() { Mail::fake(); - $user = User::factory()->create(); + $user = User::factory()->create(['locale' => 'en-US']); $project = Project::factory()->terrafund()->create(); $user->projects()->sync([$project->id => ['is_monitoring' => true]]); SiteAndNurseryReminderJob::createSiteAndNurseryReminder(Carbon::now()->subDay(), 'terrafund'); diff --git a/tests/V2/Auth/AuthControllerTest.php b/tests/V2/Auth/AuthControllerTest.php index 0dc22a721..e40b26815 100644 --- a/tests/V2/Auth/AuthControllerTest.php +++ b/tests/V2/Auth/AuthControllerTest.php @@ -17,7 +17,7 @@ public function test_auth_me_action() { $primeOrg = Organisation::factory(['status' => Organisation::STATUS_APPROVED])->create(); $monitoringOrgs = Organisation::factory(['status' => Organisation::STATUS_APPROVED])->count(2)->create(); - $user = User::factory()->create(['organisation_id' => $primeOrg->id]); + $user = User::factory()->create(['organisation_id' => $primeOrg->id, 'locale' => 'en-US']); foreach ($monitoringOrgs as $monitoringOrg) { $monitoringOrg->partners()->attach($user, ['status' => 'approved']); } @@ -30,7 +30,8 @@ public function test_auth_me_action() public function test_resend_by_email_action(): void { - $user = User::factory()->create(); + $user = User::factory()->create(['locale' => 'en-US']); + $this->actingAs($user); $this->postJson('/api/v2/users/resend', [ 'email_address' => $user->email_address, diff --git a/tests/V2/Forms/SubmitFormSubmissionControllerTest.php b/tests/V2/Forms/SubmitFormSubmissionControllerTest.php index aa857f661..001461721 100644 --- a/tests/V2/Forms/SubmitFormSubmissionControllerTest.php +++ b/tests/V2/Forms/SubmitFormSubmissionControllerTest.php @@ -17,7 +17,8 @@ class SubmitFormSubmissionControllerTest extends TestCase public function test_invoke_action(): void { Mail::fake(); - $user = User::factory()->admin()->create(); + $user = User::factory()->admin()->create(['locale' => 'en-US']); + $this->actingAs($user); $projectPitch = ProjectPitch::factory()->create([ 'organisation_id' => $user->organisation->uuid, 'status' => ProjectPitch::STATUS_DRAFT, diff --git a/tests/V2/Geometry/GeometryControllerTest.php b/tests/V2/Geometry/GeometryControllerTest.php index f105bdb3c..d62d6b05a 100644 --- a/tests/V2/Geometry/GeometryControllerTest.php +++ b/tests/V2/Geometry/GeometryControllerTest.php @@ -49,11 +49,6 @@ public function test_geometry_payload_validation() $this->fakeGeojson([$this->fakePoint(), $this->fakePolygon()]), ]); - // Multiple site ids - $this->assertCreateError('site ids must contain 1 item', $service, [ - $this->fakeGeojson([$this->fakePoint(['site_id' => '123']), $this->fakePoint(['site_id' => '456'])]), - ]); - // Missing est area $this->assertCreateError('est_area field is required', $service, [ $this->fakeGeojson([$this->fakePoint(['site_id' => '123'])]), diff --git a/tests/V2/Nurseries/AdminStatusNurseryControllerTest.php b/tests/V2/Nurseries/AdminStatusNurseryControllerTest.php index 8ac610bc5..72c28dba6 100644 --- a/tests/V2/Nurseries/AdminStatusNurseryControllerTest.php +++ b/tests/V2/Nurseries/AdminStatusNurseryControllerTest.php @@ -33,7 +33,7 @@ public function test_invoke_action(): void $tfAdmin = User::factory()->terrafundAdmin()->create(); $ppcAdmin = User::factory()->ppcAdmin()->create(); - $payload = ['feedback' => 'testing more info']; + $payload = ['feedback' => 'testing more info', 'feedback_fields' => []]; $uri = '/api/v2/admin/nurseries/' . $nursery->uuid . '/moreinfo'; $this->actingAs($random) diff --git a/tests/V2/NurseryReports/AdminStatusNurseryReportControllerTest.php b/tests/V2/NurseryReports/AdminStatusNurseryReportControllerTest.php index 51d11c372..2d110f51c 100644 --- a/tests/V2/NurseryReports/AdminStatusNurseryReportControllerTest.php +++ b/tests/V2/NurseryReports/AdminStatusNurseryReportControllerTest.php @@ -39,7 +39,7 @@ public function test_invoke_action(): void $tfAdmin = User::factory()->terrafundAdmin()->create(); $ppcAdmin = User::factory()->ppcAdmin()->create(); - $payload = ['feedback' => 'testing more info']; + $payload = ['feedback' => 'testing more info', 'feedback_fields' => []]; $uri = '/api/v2/admin/nursery-reports/' . $report->uuid . '/moreinfo'; $this->actingAs($random) diff --git a/tests/V2/Organisation/AdminApproveOrganisationControllerTest.php b/tests/V2/Organisation/AdminApproveOrganisationControllerTest.php index ec72f52e4..949805e45 100644 --- a/tests/V2/Organisation/AdminApproveOrganisationControllerTest.php +++ b/tests/V2/Organisation/AdminApproveOrganisationControllerTest.php @@ -15,13 +15,14 @@ final class AdminApproveOrganisationControllerTest extends TestCase public function testInvokeAction(): void { - $admin = User::factory()->admin()->create(); - $user = User::factory()->create(); + $admin = User::factory()->admin()->create(['locale' => 'en-US']); + $user = User::factory()->create(['locale' => 'en-US']); $organisation = Organisation::factory(['status' => Organisation::STATUS_PENDING])->create(); $owner = User::factory()->create([ 'email_address' => 'test.account@testing.com', 'organisation_id' => $organisation->id, + 'locale' => 'en-US', ]); $payload = ['uuid' => $organisation->uuid]; diff --git a/tests/V2/Organisation/OrganisationSubmitControllerTest.php b/tests/V2/Organisation/OrganisationSubmitControllerTest.php index d745be808..2b2c92d2d 100644 --- a/tests/V2/Organisation/OrganisationSubmitControllerTest.php +++ b/tests/V2/Organisation/OrganisationSubmitControllerTest.php @@ -16,7 +16,8 @@ final class OrganisationSubmitControllerTest extends TestCase public function test_invoke_action(): void { $organisation = Organisation::factory(['status' => Organisation::STATUS_DRAFT])->create(); - $user = User::factory()->create(['organisation_id' => $organisation->id]); + $user = User::factory()->create(['organisation_id' => $organisation->id, 'locale' => 'en-US']); + $this->actingAs($user); $this->actingAs($user) ->putJson('/api/v2/organisations/submit/' . $organisation->uuid, []) @@ -30,7 +31,8 @@ public function test_invoke_action(): void public function test_validation(): void { $organisation = Organisation::factory(['status' => Organisation::STATUS_DRAFT, 'name' => null])->create(); - $user = User::factory()->create(['organisation_id' => $organisation->id]); + $user = User::factory()->create(['organisation_id' => $organisation->id, 'locale' => 'en-US']); + $this->actingAs($user); $this->actingAs($user) ->putJson('/api/v2/organisations/submit/' . $organisation->uuid, []) diff --git a/tests/V2/ProjectReports/AdminStatusProjectReportControllerTest.php b/tests/V2/ProjectReports/AdminStatusProjectReportControllerTest.php index b567a8894..4d8d17630 100644 --- a/tests/V2/ProjectReports/AdminStatusProjectReportControllerTest.php +++ b/tests/V2/ProjectReports/AdminStatusProjectReportControllerTest.php @@ -33,7 +33,7 @@ public function test_invoke_action(): void $tfAdmin = User::factory()->terrafundAdmin()->create(); $ppcAdmin = User::factory()->ppcAdmin()->create(); - $payload = ['feedback' => 'testing more info']; + $payload = ['feedback' => 'testing more info', 'feedback_fields' => []]; $uri = '/api/v2/admin/project-reports/' . $report->uuid . '/moreinfo'; $this->actingAs($random) diff --git a/tests/V2/Projects/AdminStatusProjectControllerTest.php b/tests/V2/Projects/AdminStatusProjectControllerTest.php index 776e21213..594abf3b7 100644 --- a/tests/V2/Projects/AdminStatusProjectControllerTest.php +++ b/tests/V2/Projects/AdminStatusProjectControllerTest.php @@ -27,7 +27,7 @@ public function test_invoke_action(): void $tfAdmin = User::factory()->terrafundAdmin()->create(); $ppcAdmin = User::factory()->ppcAdmin()->create(); - $payload = ['feedback' => 'testing more info']; + $payload = ['feedback' => 'testing more info', 'feedback_fields' => []]; $uri = '/api/v2/admin/projects/' . $project->uuid . '/moreinfo'; $this->actingAs($random) diff --git a/tests/V2/SiteReports/AdminStatusSiteReportControllerTest.php b/tests/V2/SiteReports/AdminStatusSiteReportControllerTest.php index 047021a89..88a1a69f1 100644 --- a/tests/V2/SiteReports/AdminStatusSiteReportControllerTest.php +++ b/tests/V2/SiteReports/AdminStatusSiteReportControllerTest.php @@ -40,7 +40,7 @@ public function test_invoke_action(): void $tfAdmin = User::factory()->terrafundAdmin()->create(); $ppcAdmin = User::factory()->ppcAdmin()->create(); - $payload = ['feedback' => 'testing more info']; + $payload = ['feedback' => 'testing more info', 'feedback_fields' => []]; $uri = '/api/v2/admin/site-reports/' . $report->uuid . '/moreinfo'; $this->actingAs($random) diff --git a/tests/V2/Sites/AdminStatusSiteControllerTest.php b/tests/V2/Sites/AdminStatusSiteControllerTest.php index 199ac7521..b5ff765f6 100644 --- a/tests/V2/Sites/AdminStatusSiteControllerTest.php +++ b/tests/V2/Sites/AdminStatusSiteControllerTest.php @@ -33,7 +33,7 @@ public function test_invoke_action(): void $tfAdmin = User::factory()->terrafundAdmin()->create(); $ppcAdmin = User::factory()->ppcAdmin()->create(); - $payload = ['feedback' => 'testing more info']; + $payload = ['feedback' => 'testing more info', 'feedback_fields' => []]; $uri = '/api/v2/admin/sites/' . $site->uuid . '/moreinfo'; $this->actingAs($random)