diff --git a/app/Exports/V2/EntityExport.php b/app/Exports/V2/EntityExport.php index db157c250..7481c9a8a 100644 --- a/app/Exports/V2/EntityExport.php +++ b/app/Exports/V2/EntityExport.php @@ -4,6 +4,7 @@ use App\Models\V2\Forms\Form; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Str; use Maatwebsite\Excel\Concerns\FromQuery; use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\WithMapping; @@ -69,14 +70,26 @@ protected function getAttachedMappedForEntity($entity): array $mapped = [ $entity->ppc_external_id ?? $entity->old_id ?? $entity->id ?? null, $entity->uuid, + ]; + + if (in_array($this->form->type, ['site', 'nursery', 'site-report', 'nursery-report'])) { + $frontEndUrl = config('app.front_end'); + // Our environment variable definitions are inconsistent. + if (! Str::endsWith($frontEndUrl, '/')) { + $frontEndUrl .= '/'; + } + $mapped[] = $frontEndUrl . 'admin#/' . Str::camel($entity->shortName) . '/' . $entity->uuid . '/show'; + } + + $mapped = array_merge($mapped, [ $organisation->readable_type ?? null, $organisation->name ?? null, $entity->project->name ?? null, $entity->status ?? null, $entity->due_at ?? null, - ]; + ]); - if (in_array($this->form->type, ['nursery', 'nursery-report','site', 'site-report', 'project-report'])) { + if (in_array($this->form->type, ['nursery', 'nursery-report', 'site', 'site-report', 'project-report'])) { $mapped[] = $entity->project->ppc_external_id ?? $entity->project->id ?? null; } @@ -84,6 +97,7 @@ protected function getAttachedMappedForEntity($entity): array $mapped[] = $entity->project->uuid ?? null; if($this->form->framework_key === 'ppc') { $mapped[] = $entity->seedlings_grown ?? null; + $mapped[] = $entity->seedlings_grown_to_date ?? null; } } if ($this->form->type === 'nursery-report') { @@ -94,8 +108,8 @@ protected function getAttachedMappedForEntity($entity): array if ($this->form->type === 'site-report') { $mapped[] = $entity->site->ppc_external_id ?? $entity->site->id ?? null; $mapped[] = $entity->site->name ?? null; - $sumTreeSPecies = $entity->treeSpecies()->sum('amount'); - $mapped[] = $sumTreeSPecies > 0 ? $sumTreeSPecies : null; + $sumTreeSpecies = $entity->treeSpecies()->sum('amount'); + $mapped[] = $sumTreeSpecies > 0 ? $sumTreeSpecies : null; $mapped[] = $entity->site->trees_planted_count ?? null; if($this->form->framework_key === 'ppc') { $sumSeeding = $entity->seedings()->sum('amount'); @@ -109,15 +123,19 @@ protected function getAttachedMappedForEntity($entity): array protected function getAttachedHeadingsForEntity(): array { - $initialHeadings = [ - 'id', - 'uuid', + $initialHeadings = ['id', 'uuid']; + + if (in_array($this->form->type, ['site', 'nursery', 'site-report', 'nursery-report'])) { + $initialHeadings[] = 'link_to_terramatch'; + } + + $initialHeadings = array_merge($initialHeadings, [ 'organization-readable_type', 'organization-name', 'project_name', 'status', 'due_date', - ]; + ]); if (in_array($this->form->type, ['nursery', 'nursery-report','site', 'site-report', 'project-report'])) { $initialHeadings[] = 'project-id'; @@ -126,6 +144,7 @@ protected function getAttachedHeadingsForEntity(): array if ($this->form->type === 'project-report') { $initialHeadings[] = 'project_uuid'; if($this->form->framework_key === 'ppc') { + $initialHeadings[] = 'total_seedlings_grown_report'; $initialHeadings[] = 'total_seedlings_grown'; } } diff --git a/app/Http/Controllers/V2/Nurseries/AdminIndexNurseriesController.php b/app/Http/Controllers/V2/Nurseries/AdminIndexNurseriesController.php index 05a6167b9..bdd340248 100644 --- a/app/Http/Controllers/V2/Nurseries/AdminIndexNurseriesController.php +++ b/app/Http/Controllers/V2/Nurseries/AdminIndexNurseriesController.php @@ -29,7 +29,7 @@ public function __invoke(Request $request): NurseriesCollection AllowedFilter::scope('country'), AllowedFilter::scope('organisation_uuid', 'organisationUuid'), AllowedFilter::scope('project_uuid', 'projectUuid'), - AllowedFilter::exact('framework', 'framework_key'), + AllowedFilter::exact('framework_key'), AllowedFilter::exact('status'), AllowedFilter::exact('update_request_status'), ]); diff --git a/app/Http/Requests/V2/Forms/StoreFormRequest.php b/app/Http/Requests/V2/Forms/StoreFormRequest.php index 586bdaf53..b5d2791fa 100644 --- a/app/Http/Requests/V2/Forms/StoreFormRequest.php +++ b/app/Http/Requests/V2/Forms/StoreFormRequest.php @@ -34,6 +34,7 @@ public function rules() 'form_sections.*.form_questions' => ['sometimes', 'array'], 'form_sections.*.form_questions.*.linked_field_key' => ['sometimes', 'nullable', 'string'], + 'form_sections.*.form_questions.*.collection' => ['sometimes', 'nullable', 'string'], 'form_sections.*.form_questions.*.label' => ['required', 'string'], 'form_sections.*.form_questions.*.input_type' => ['sometimes', 'nullable'], 'form_sections.*.form_questions.*.name' => ['sometimes', 'nullable'], @@ -58,6 +59,7 @@ public function rules() 'form_sections.*.form_questions.*.child_form_questions' => ['sometimes'], 'form_sections.*.form_questions.*.child_form_questions.*.linked_field_key' => ['sometimes', 'required', 'string'], + 'form_sections.*.form_questions.*.child_form_questions.*.collection' => ['sometimes', 'nullable', 'string'], 'form_sections.*.form_questions.*.child_form_questions.*.label' => ['sometimes', 'required', 'string'], 'form_sections.*.form_questions.*.child_form_questions.*.name' => ['sometimes', 'nullable'], 'form_sections.*.form_questions.*.child_form_questions.*.input_type' => ['sometimes', 'nullable'], diff --git a/app/Http/Requests/V2/Forms/UpdateFormRequest.php b/app/Http/Requests/V2/Forms/UpdateFormRequest.php index 6e02393a9..c7dea033a 100644 --- a/app/Http/Requests/V2/Forms/UpdateFormRequest.php +++ b/app/Http/Requests/V2/Forms/UpdateFormRequest.php @@ -34,6 +34,7 @@ public function rules() 'form_sections.*.form_questions' => ['sometimes', 'nullable', 'array'], 'form_sections.*.form_questions.*.linked_field_key' => ['sometimes', 'nullable', 'string'], + 'form_sections.*.form_questions.*.collection' => ['sometimes', 'nullable', 'string'], 'form_sections.*.form_questions.*.label' => ['sometimes', 'nullable', 'string'], 'form_sections.*.form_questions.*.uuid' => ['sometimes', 'nullable', 'string'], 'form_sections.*.form_questions.*.name' => ['sometimes', 'nullable'], @@ -63,6 +64,7 @@ public function rules() 'form_sections.*.form_questions.*.child_form_questions' => ['sometimes', 'nullable'], 'form_sections.*.form_questions.*.child_form_questions.*.uuid' => ['sometimes', 'nullable', 'string'], 'form_sections.*.form_questions.*.child_form_questions.*.linked_field_key' => ['sometimes', 'nullable', 'string'], + 'form_sections.*.form_questions.*.child_form_questions.*.collection' => ['sometimes', 'nullable', 'string'], 'form_sections.*.form_questions.*.child_form_questions.*.label' => ['sometimes', 'nullable', 'string'], 'form_sections.*.form_questions.*.child_form_questions.*.name' => ['sometimes', 'nullable'], 'form_sections.*.form_questions.*.child_form_questions.*.input_type' => ['sometimes', 'nullable'], diff --git a/app/Http/Resources/V2/Projects/ProjectResource.php b/app/Http/Resources/V2/Projects/ProjectResource.php index 4417e7d5c..eae518e46 100644 --- a/app/Http/Resources/V2/Projects/ProjectResource.php +++ b/app/Http/Resources/V2/Projects/ProjectResource.php @@ -52,6 +52,8 @@ public function toArray($request) 'seeds_planted_count' => $this->seeds_planted_count, 'regenerated_trees_count' => $this->regenerated_trees_count, 'workday_count' => $this->workday_count, + // Temporary until we have bulk import completed. + 'self_reported_workday_count' => $this->self_reported_workday_count, 'total_jobs_created' => $this->total_jobs_created, 'total_sites' => $this->total_sites, 'total_nurseries' => $this->total_nurseries, diff --git a/app/Http/Resources/V2/Sites/SiteResource.php b/app/Http/Resources/V2/Sites/SiteResource.php index 6daba0c71..88e2bfff3 100644 --- a/app/Http/Resources/V2/Sites/SiteResource.php +++ b/app/Http/Resources/V2/Sites/SiteResource.php @@ -33,7 +33,7 @@ public function toArray($request) 'survival_rate_planted' => $this->survival_rate_planted, 'direct_seeding_survival_rate' => $this->direct_seeding_survival_rate, 'a_nat_regeneration_trees_per_hectare' => $this->a_nat_regeneration_trees_per_hectare, - 'a_nat_regeneration' => $this->a_nat_regeneration, + 'a_nat_regeneration' => $this->a_nat_regeneration > 1 ? intval(round($this->a_nat_regeneration)) : $this->a_nat_regeneration, 'hectares_to_restore_goal' => $this->hectares_to_restore_goal, 'landscape_community_contribution' => $this->landscape_community_contribution, 'planting_pattern' => $this->planting_pattern, @@ -48,6 +48,8 @@ public function toArray($request) 'site_reports_total' => $this->site_reports_total, 'overdue_site_reports_total' => $this->overdue_site_reports_total, 'workday_count' => $this->workday_count, + // Temporary until we have bulk import completed. + 'self_reported_workday_count' => $this->self_reported_workday_count, 'trees_planted_count' => $this->trees_planted_count, 'regenerated_trees_count' => $this->regenerated_trees_count, 'migrated' => ! empty($this->old_model), diff --git a/app/Mail/FormSubmissionRejected.php b/app/Mail/FormSubmissionRejected.php index afcdb9580..db36d23c1 100644 --- a/app/Mail/FormSubmissionRejected.php +++ b/app/Mail/FormSubmissionRejected.php @@ -6,13 +6,13 @@ class FormSubmissionRejected extends Mail { public function __construct(String $feedback = null) { - $this->subject = 'Application Rejected'; - $this->title = 'Your application has been rejected'; - $this->body = - 'Your application has been rejected.'; + $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 = 'Your application has been rejected. Please see comments below:

' . - e($feedback); + $this->body .= + ' Please see the comments below for more details or any follow-up resources.

' . + e($feedback); } $this->transactional = true; } diff --git a/app/Models/V2/Projects/Project.php b/app/Models/V2/Projects/Project.php index 393e2aa7a..c946f5722 100644 --- a/app/Models/V2/Projects/Project.php +++ b/app/Models/V2/Projects/Project.php @@ -25,6 +25,8 @@ use App\Models\V2\Sites\SiteReport; use App\Models\V2\Tasks\Task; use App\Models\V2\TreeSpecies\TreeSpecies; +use App\Models\V2\Workdays\Workday; +use App\Models\V2\Workdays\WorkdayDemographic; use App\StateMachines\EntityStatusStateMachine; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -357,6 +359,21 @@ public function getRegeneratedTreesCountAttribute(): int } public function getWorkdayCountAttribute(): int + { + return WorkdayDemographic::whereIn( + 'workday_id', + Workday::where('workdayable_type', SiteReport::class) + ->whereIn('workdayable_id', $this->submittedSiteReports()->select('v2_site_reports.id')) + ->select('id') + )->orWhereIn( + 'workday_id', + Workday::where('workdayable_type', ProjectReport::class) + ->whereIn('workdayable_id', $this->reports()->hasBeenSubmitted()->select('id')) + ->select('id') + )->gender()->sum('amount') ?? 0; + } + + public function getSelfReportedWorkdayCountAttribute(): int { $sumQueries = [ DB::raw('sum(`workdays_paid`) as paid'), diff --git a/app/Models/V2/Projects/ProjectReport.php b/app/Models/V2/Projects/ProjectReport.php index a25f34874..cd2835fb7 100644 --- a/app/Models/V2/Projects/ProjectReport.php +++ b/app/Models/V2/Projects/ProjectReport.php @@ -13,8 +13,6 @@ use App\Models\Traits\HasWorkdays; use App\Models\Traits\UsesLinkedFields; use App\Models\V2\MediaModel; -use App\Models\V2\Nurseries\Nursery; -use App\Models\V2\Nurseries\NurseryReport; use App\Models\V2\Organisation; use App\Models\V2\Polygon; use App\Models\V2\ReportModel; @@ -290,30 +288,32 @@ public function getReportTitleAttribute(): string public function getSeedlingsGrownAttribute(): int { if ($this->framework_key == 'ppc') { - return $this->treeSpecies() - ->sum('amount'); + return $this->treeSpecies()->sum('amount'); } if ($this->framework_key == 'terrafund') { - if (empty($this->due_at)) { + if (empty($this->task_id)) { return 0; } - $month = $this->due_at->month; - $year = $this->due_at->year; - $nurseryIds = Nursery::where('project_id', data_get($this->project, 'id')) - ->isApproved() - ->pluck('id') - ->toArray(); - - if (count($nurseryIds) > 0) { - return NurseryReport::whereIn('nursery_id', $nurseryIds) - ->whereMonth('due_at', $month) - ->whereYear('due_at', $year) - ->sum('seedlings_young_trees'); - } + return $this->task->nurseryReports()->sum('seedlings_young_trees'); + } + + return 0; + } + + public function getSeedlingsGrownToDateAttribute(): int + { + if ($this->framework_key == 'ppc') { + return TreeSpecies::where('speciesable_type', ProjectReport::class) + ->whereIn( + 'speciesable_id', + $this->project->reports()->where('created_at', '<=', $this->created_at)->select('id') + ) + ->sum('amount'); } + // this attribute is currently only used for PPC report exports. return 0; } diff --git a/app/Models/V2/Sites/Site.php b/app/Models/V2/Sites/Site.php index a98a8b84e..96b69b828 100644 --- a/app/Models/V2/Sites/Site.php +++ b/app/Models/V2/Sites/Site.php @@ -20,6 +20,8 @@ use App\Models\V2\Seeding; use App\Models\V2\Stratas\Strata; use App\Models\V2\TreeSpecies\TreeSpecies; +use App\Models\V2\Workdays\Workday; +use App\Models\V2\Workdays\WorkdayDemographic; use App\StateMachines\ReportStatusStateMachine; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -301,6 +303,16 @@ public function getRegeneratedTreesCountAttribute(): int } public function getWorkdayCountAttribute(): int + { + return WorkdayDemographic::whereIn( + 'workday_id', + Workday::where('workdayable_type', SiteReport::class) + ->whereIn('workdayable_id', $this->reports()->hasBeenSubmitted()->select('id')) + ->select('id') + )->gender()->sum('amount') ?? 0; + } + + public function getSelfReportedWorkdayCountAttribute(): int { $totals = $this->reports()->hasBeenSubmitted()->get([ DB::raw('sum(`workdays_volunteer`) as volunteer'), diff --git a/database/factories/V2/Workdays/WorkdayFactory.php b/database/factories/V2/Workdays/WorkdayFactory.php index e6a19f119..ce4669559 100644 --- a/database/factories/V2/Workdays/WorkdayFactory.php +++ b/database/factories/V2/Workdays/WorkdayFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories\V2\Workdays; +use App\Models\V2\Projects\ProjectReport; use App\Models\V2\Sites\SiteReport; use App\Models\V2\Workdays\Workday; use Illuminate\Database\Eloquent\Factories\Factory; @@ -22,4 +23,15 @@ public function definition() 'collection' => $this->faker->randomElement(array_keys(Workday::SITE_COLLECTIONS)), ]; } + + public function projectReport(): Factory + { + return $this->state(function (array $attributes) { + return [ + 'workdayable_type' => ProjectReport::class, + 'workdayable_id' => ProjectReport::factory()->create(), + 'collection' => $this->faker->randomElement(array_keys(Workday::PROJECT_COLLECTION)), + ]; + }); + } } diff --git a/database/migrations/2024_05_14_151653_create_point_geometry_table.php b/database/migrations/2024_05_14_151653_create_point_geometry_table.php new file mode 100644 index 000000000..445d288ce --- /dev/null +++ b/database/migrations/2024_05_14_151653_create_point_geometry_table.php @@ -0,0 +1,33 @@ +id(); + $table->uuid('uuid')->unique(); + $table->geometry('geom')->nullable(); + $table->decimal('est_area', 15, 2)->nullable(); + $table->string('created_by')->nullable(); + $table->string('last_modified_by')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('point_geometry'); + } +}; diff --git a/database/migrations/2024_05_21_193901_float_regeneration.php b/database/migrations/2024_05_21_193901_float_regeneration.php new file mode 100644 index 000000000..76e566d94 --- /dev/null +++ b/database/migrations/2024_05_21_193901_float_regeneration.php @@ -0,0 +1,27 @@ +float('a_nat_regeneration')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('v2_sites', function (Blueprint $table) { + $table->unsignedInteger('a_nat_regeneration')->change(); + }); + } +}; diff --git a/tests/Unit/Models/V2/Projects/ProjectTest.php b/tests/Unit/Models/V2/Projects/ProjectTest.php index 27a3a1455..249157afc 100644 --- a/tests/Unit/Models/V2/Projects/ProjectTest.php +++ b/tests/Unit/Models/V2/Projects/ProjectTest.php @@ -7,6 +7,10 @@ use App\Models\V2\Projects\ProjectMonitoring; use App\Models\V2\Projects\ProjectReport; use App\Models\V2\Sites\Site; +use App\Models\V2\Sites\SiteReport; +use App\Models\V2\Workdays\Workday; +use App\Models\V2\Workdays\WorkdayDemographic; +use App\StateMachines\EntityStatusStateMachine; use Tests\TestCase; class ProjectTest extends TestCase @@ -107,6 +111,46 @@ public function test_it_deletes_its_own_project_monitorings(string $permission, } } + public function test_workday_count() + { + $project = Project::factory()->ppc()->create(); + + // The amounts are all prime so that it's easy to tell what got counted and what didn't when there's an error + + // Unapproved site (doesn't count toward workday count) + $site = Site::factory()->ppc()->create(['project_id' => $project->id, 'status' => EntityStatusStateMachine::AWAITING_APPROVAL]); + $report = SiteReport::factory()->ppc()->create(['site_id' => $site->id, 'status' => EntityStatusStateMachine::APPROVED]); + $workday = Workday::factory()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 3]); + + // Approved site + $site = Site::factory()->ppc()->create(['project_id' => $project->id, 'status' => EntityStatusStateMachine::APPROVED]); + $report = SiteReport::factory()->ppc()->create(['site_id' => $site->id, 'status' => EntityStatusStateMachine::APPROVED]); + $workday = Workday::factory()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 5]); + $report = SiteReport::factory()->ppc()->create(['site_id' => $site->id, 'status' => EntityStatusStateMachine::AWAITING_APPROVAL]); + $workday = Workday::factory()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 7]); + // Unsubmitted report (doesn't count toward workday count) + $report = SiteReport::factory()->ppc()->create(['site_id' => $site->id, 'status' => EntityStatusStateMachine::STARTED]); + $workday = Workday::factory()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 11]); + + $report = ProjectReport::factory()->ppc()->create(['project_id' => $project->id, 'status' => EntityStatusStateMachine::APPROVED]); + $workday = Workday::factory()->projectReport()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 13]); + $report = ProjectReport::factory()->ppc()->create(['project_id' => $project->id, 'status' => EntityStatusStateMachine::AWAITING_APPROVAL]); + $workday = Workday::factory()->projectReport()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 17]); + // Unsubmitted report (doesn't count toward workday count) + $report = ProjectReport::factory()->ppc()->create(['project_id' => $project->id, 'status' => EntityStatusStateMachine::STARTED]); + $workday = Workday::factory()->projectReport()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 19]); + + // 42 = 5 and 7 from the approved site's reports and 13 and 17 from the project reports + $this->assertEquals(42, $project->workday_count); + } + public static function permissionsDataProvider() { return [ diff --git a/tests/Unit/Models/V2/Sites/SiteTest.php b/tests/Unit/Models/V2/Sites/SiteTest.php index afd58959a..ca9d25c90 100644 --- a/tests/Unit/Models/V2/Sites/SiteTest.php +++ b/tests/Unit/Models/V2/Sites/SiteTest.php @@ -5,6 +5,9 @@ use App\Models\V2\Sites\Site; use App\Models\V2\Sites\SiteMonitoring; use App\Models\V2\Sites\SiteReport; +use App\Models\V2\Workdays\Workday; +use App\Models\V2\Workdays\WorkdayDemographic; +use App\StateMachines\EntityStatusStateMachine; use Tests\TestCase; class SiteTest extends TestCase @@ -57,6 +60,26 @@ public function test_it_deletes_its_own_site_monitorings(string $permission, str } } + public function test_workday_count() + { + $site = Site::factory()->ppc()->create(); + + $report = SiteReport::factory()->ppc()->create(['site_id' => $site->id, 'status' => EntityStatusStateMachine::APPROVED]); + $workday = Workday::factory()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 3]); + + $report = SiteReport::factory()->ppc()->create(['site_id' => $site->id, 'status' => EntityStatusStateMachine::AWAITING_APPROVAL]); + $workday = Workday::factory()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 5]); + + // Unsubmitted report (doesn't count toward workday count) + $report = SiteReport::factory()->ppc()->create(['site_id' => $site->id, 'status' => EntityStatusStateMachine::STARTED]); + $workday = Workday::factory()->create(['workdayable_id' => $report->id]); + WorkdayDemographic::factory()->create(['workday_id' => $workday->id, 'amount' => 7]); + + $this->assertEquals(8, $site->workday_count); + } + public static function permissionsDataProvider() { return [ diff --git a/tests/V2/Nurseries/AdminIndexNurseriesControllerTest.php b/tests/V2/Nurseries/AdminIndexNurseriesControllerTest.php index b24bf9f5d..86f76aef0 100644 --- a/tests/V2/Nurseries/AdminIndexNurseriesControllerTest.php +++ b/tests/V2/Nurseries/AdminIndexNurseriesControllerTest.php @@ -61,7 +61,7 @@ public function test_nursery_index_has_required_filters() 'country', 'organisation_uuid', 'project_uuid', - 'framework', + 'framework_key', ]; foreach ($filters as $filter) {