From 4e86b77c15c28e05b210d70a5627a89fcaea9767 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 20 Feb 2025 16:51:00 -0800 Subject: [PATCH 1/8] [TM-1718] Create jobs demographics and convert jobs totals from reports. --- .../OneOff/MigrateJobsToDemographics.php | 132 ++++++++++++++++++ app/Models/Traits/HasDemographics.php | 8 ++ app/Models/V2/Demographics/Demographic.php | 6 +- .../Demographics/DemographicCollections.php | 10 ++ .../V2/Demographics/DemographicEntry.php | 2 +- app/Models/V2/Projects/ProjectReport.php | 11 ++ 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/OneOff/MigrateJobsToDemographics.php diff --git a/app/Console/Commands/OneOff/MigrateJobsToDemographics.php b/app/Console/Commands/OneOff/MigrateJobsToDemographics.php new file mode 100644 index 000000000..2539999e5 --- /dev/null +++ b/app/Console/Commands/OneOff/MigrateJobsToDemographics.php @@ -0,0 +1,132 @@ + [ + 'gender' => [ + 'male' => 'ft_men', + 'female' => 'ft_women', + 'non-binary' => 'ft_other' + ], + 'age' => [ + 'youth' => 'ft_youth', + 'non-youth' => 'ft_jobs_non_youth' + ], + 'total' => 'ft_total' + ], + 'part-time' => [ + 'gender' => [ + 'male' => 'pt_men', + 'female' => 'pt_women', + 'non-binary' => 'pt_other' + ], + 'age' => [ + 'youth' => 'pt_youth', + 'non-youth' => 'pt_non_youth' + ], + 'total' => 'pt_total' + ], + 'volunteer' => [ + 'gender' => [ + 'male' => 'volunteer_men', + 'female' => 'volunteer_women', + 'non-binary' => 'volunteer_other' + ], + 'age' => [ + 'youth' => 'volunteer_youth', + 'non-youth' => 'volunteer_non_youth' + ], + 'caste' => [ + 'marginalized' => 'volunteer_scstobc', + ], + 'total' => 'volunteer_total' + ] + ]; + + /** + * Execute the console command. + */ + public function handle() + { + $this->info("Moving project report jobs data to Demographics..."); + $this->withProgressBar(ProjectReport::count(), function ($progressBar) { + ProjectReport::chunkById(100, function ($projectReports) use ($progressBar) { + foreach ($projectReports as $projectReport) { + $this->convertJobs($projectReport); + $progressBar->advance(); + } + }); + }); + + $this->info("\n\nCompleted moving project report jobs data to Demographics."); + } + + private function convertJobs(ProjectReport $projectReport): void + { + foreach (self::JOBS_MAPPING as $collection => $types) { + /** @var Demographic $demographic */ + $demographic = null; + foreach ($types as $type => $subtypes) { + if ($type == "total") { + $field = $subtypes; + if ($demographic != null) { + $total = $demographic->entries()->gender()->sum('amount'); + if ($projectReport[$field] > $total) { + // we've got a total that's greater than the sum of gender values, create a "unknown" gender + // row to fill the gap + $demographic->entries()->create([ + 'type' => 'gender', + 'subtype' => 'unknown', + 'amount' => $projectReport[$field] - $total + ]); + } + } + } else { + // If none of the fields for this type exist, skip + $fields = collect(array_values($subtypes)); + if ($fields->first(fn($field) => $projectReport[$field] > 0) == null) { + continue; + } + + if ($demographic == null) { + $demographic = $projectReport->demographics()->create([ + 'type' => 'jobs', + 'collection' => $collection, + ]); + } + foreach ($subtypes as $subtype => $field) { + $value = $projectReport[$field]; + if ($value > 0) { + $demographic->entries()->create([ + 'type' => $type, + 'subtype' => $subtype, + 'amount' => $value + ]); + } + } + } + } + } + } +} diff --git a/app/Models/Traits/HasDemographics.php b/app/Models/Traits/HasDemographics.php index 7a927befe..dbfdef7fc 100644 --- a/app/Models/Traits/HasDemographics.php +++ b/app/Models/Traits/HasDemographics.php @@ -16,6 +16,9 @@ trait HasDemographics 'workdaysConvergenceTotal' => ['type' => Demographic::WORKDAY_TYPE, 'collections' => 'convergence'], 'directRestorationPartners' => ['type' => Demographic::RESTORATION_PARTNER_TYPE, 'collections' => 'direct'], 'indirectRestorationPartners' => ['type' => Demographic::RESTORATION_PARTNER_TYPE, 'collections' => 'indirect'], + 'jobsFullTimeTotal' => ['type' => Demographic::JOBS_TYPE, 'collections' => 'full-time'], + 'jobsPartTimeTotal' => ['type' => Demographic::JOBS_TYPE, 'collections' => 'part-time'], + 'jobsVolunteerTotal' => ['type' => Demographic::JOBS_TYPE, 'collections' => 'volunteer'], ]; public static function bootHasDemographics() @@ -40,6 +43,11 @@ public static function bootHasDemographics() $collectionSets['direct'], $collectionSets['indirect'], ])->flatten(), + Demographic::JOBS_TYPE => collect([ + $collectionSets['full-time'], + $collectionSets['part-time'], + $collectionSets['volunteer'], + ])->flatten(), default => throw new InternalErrorException("Unrecognized demographic type: $demographicType"), }; $collections->each(function ($collection) use ($attributePrefix) { diff --git a/app/Models/V2/Demographics/Demographic.php b/app/Models/V2/Demographics/Demographic.php index 0338ad41b..94129e34f 100644 --- a/app/Models/V2/Demographics/Demographic.php +++ b/app/Models/V2/Demographics/Demographic.php @@ -33,8 +33,9 @@ class Demographic extends Model implements HandlesLinkedFieldSync public const WORKDAY_TYPE = 'workdays'; public const RESTORATION_PARTNER_TYPE = 'restoration-partners'; + public const JOBS_TYPE = 'jobs'; - public const VALID_TYPES = [self::WORKDAY_TYPE, self::RESTORATION_PARTNER_TYPE]; + public const VALID_TYPES = [self::WORKDAY_TYPE, self::RESTORATION_PARTNER_TYPE, self::JOBS_TYPE]; // In TM-1681 we moved several "name" values to "subtype". This check helps make sure that both in-flight // work at the time of release, and updates from update requests afterward honor that change. @@ -191,6 +192,9 @@ public function getReadableCollectionAttribute(): ?string SiteReport::class => DemographicCollections::WORKDAYS_SITE_COLLECTIONS, default => null }, + self::JOBS_TYPE => match ($this->demographical_type) { + ProjectReport::class => DemographicCollections::JOBS_PROJECT_COLLECTIONS, + }, default => null }; if (empty($collections)) { diff --git a/app/Models/V2/Demographics/DemographicCollections.php b/app/Models/V2/Demographics/DemographicCollections.php index dfa212e18..c61c0c309 100644 --- a/app/Models/V2/Demographics/DemographicCollections.php +++ b/app/Models/V2/Demographics/DemographicCollections.php @@ -88,4 +88,14 @@ class DemographicCollections self::DIRECT_OTHER => 'Direct Other', self::INDIRECT_OTHER => 'Indirect Other', ]; + + public const FULL_TIME = 'full-time'; + public const PART_TIME = 'part-time'; + public const VOLUNTEER = 'volunteer'; + + public const JOBS_PROJECT_COLLECTIONS = [ + self::FULL_TIME => 'Full-time', + self::PART_TIME => 'Part-time', + self::VOLUNTEER => 'Volunteer' + ]; } diff --git a/app/Models/V2/Demographics/DemographicEntry.php b/app/Models/V2/Demographics/DemographicEntry.php index 3887a59d6..efd1f6b7c 100644 --- a/app/Models/V2/Demographics/DemographicEntry.php +++ b/app/Models/V2/Demographics/DemographicEntry.php @@ -24,7 +24,7 @@ class DemographicEntry extends Model public const GENDER = 'gender'; public const GENDERS = ['male', 'female', 'non-binary', 'unknown']; public const AGE = 'age'; - public const AGES = ['youth', 'adult', 'elder', 'unknown']; + public const AGES = ['youth', 'non-youth', 'adult', 'elder', 'unknown']; public const ETHNICITY = 'ethnicity'; public const ETHNICITIES = ['indigenous', 'other', 'unknown']; public const CASTE = 'caste'; diff --git a/app/Models/V2/Projects/ProjectReport.php b/app/Models/V2/Projects/ProjectReport.php index bcd550afb..6a534c1ef 100644 --- a/app/Models/V2/Projects/ProjectReport.php +++ b/app/Models/V2/Projects/ProjectReport.php @@ -262,6 +262,17 @@ class ProjectReport extends Model implements MediaModel, AuditableContract, Repo DemographicCollections::INDIRECT_OTHER, ], ], + Demographic::JOBS_TYPE => [ + 'full-time' => [ + DemographicCollections::FULL_TIME, + ], + 'part-time' => [ + DemographicCollections::PART_TIME, + ], + 'volunteer' => [ + DemographicCollections::VOLUNTEER, + ] + ] ]; public function registerMediaConversions(Media $media = null): void From fc6c0db09b14c0270bd9c6b71fe643d2f2fe1d72 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 21 Feb 2025 09:44:20 -0800 Subject: [PATCH 2/8] [TM-1718] Update to make sure gender and age are balanced. --- .../OneOff/MigrateJobsToDemographics.php | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/Console/Commands/OneOff/MigrateJobsToDemographics.php b/app/Console/Commands/OneOff/MigrateJobsToDemographics.php index 2539999e5..acc61562a 100644 --- a/app/Console/Commands/OneOff/MigrateJobsToDemographics.php +++ b/app/Console/Commands/OneOff/MigrateJobsToDemographics.php @@ -91,14 +91,24 @@ private function convertJobs(ProjectReport $projectReport): void if ($type == "total") { $field = $subtypes; if ($demographic != null) { - $total = $demographic->entries()->gender()->sum('amount'); - if ($projectReport[$field] > $total) { - // we've got a total that's greater than the sum of gender values, create a "unknown" gender - // row to fill the gap + // Make sure gender / age demographics are balanced and reach at least to the "_total" field + // for this type of job from the original report. Pad gender and age demographics with an + // "unknown" if needed. + $genderTotal = $demographic->entries()->gender()->sum('amount'); + $ageTotal = $demographic->entries()->age()->sum('amount'); + $targetTotal = max($genderTotal, $ageTotal, $projectReport[$field]); + if ($genderTotal < $targetTotal) { $demographic->entries()->create([ 'type' => 'gender', 'subtype' => 'unknown', - 'amount' => $projectReport[$field] - $total + 'amount' => $targetTotal - $genderTotal, + ]); + } + if ($ageTotal < $targetTotal) { + $demographic->entries()->create([ + 'type' => 'age', + 'subtype' => 'unknown', + 'amount' => $targetTotal - $ageTotal, ]); } } From e5d57d1c74726d01cb058b6c45a5ef9dfc3cf3fc Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Mon, 24 Feb 2025 10:07:04 -0800 Subject: [PATCH 3/8] [TM-1718] Demographics API read has moved to v3. --- .../GetDemographicsForEntityController.php | 55 ------ .../definitions/V2RestorationPartnerRead.yml | 13 -- openapi-src/V2/definitions/V2WorkdayRead.yml | 13 -- openapi-src/V2/definitions/_index.yml | 4 - ...et-v2-restoration-partners-entity-uuid.yml | 24 --- .../Workdays/get-v2-workdays-entity-uuid.yml | 24 --- openapi-src/V2/paths/_index.yml | 6 - resources/docs/swagger-v2.yml | 164 ------------------ routes/api_v2.php | 5 - .../GetWorkdaysForEntityControllerTest.php | 143 --------------- 10 files changed, 451 deletions(-) delete mode 100644 app/Http/Controllers/V2/Demographics/GetDemographicsForEntityController.php delete mode 100644 openapi-src/V2/definitions/V2RestorationPartnerRead.yml delete mode 100644 openapi-src/V2/definitions/V2WorkdayRead.yml delete mode 100644 openapi-src/V2/paths/RestorationPartners/get-v2-restoration-partners-entity-uuid.yml delete mode 100644 openapi-src/V2/paths/Workdays/get-v2-workdays-entity-uuid.yml delete mode 100644 tests/V2/Workdays/GetWorkdaysForEntityControllerTest.php diff --git a/app/Http/Controllers/V2/Demographics/GetDemographicsForEntityController.php b/app/Http/Controllers/V2/Demographics/GetDemographicsForEntityController.php deleted file mode 100644 index 9fadd9c95..000000000 --- a/app/Http/Controllers/V2/Demographics/GetDemographicsForEntityController.php +++ /dev/null @@ -1,55 +0,0 @@ -authorize('update', $entity); - - $property = Str::camel($demographicType); - $demographics = $entity->$property()->visible()->get(); - - $expectedCollections = match ($demographicType) { - Demographic::RESTORATION_PARTNER_TYPE => match ($entity->shortName) { - 'project-report' => array_keys(DemographicCollections::RESTORATION_PARTNERS_PROJECT_COLLECTIONS), - default => throw new NotFoundHttpException(), - }, - Demographic::WORKDAY_TYPE => match ($entity->shortName) { - 'site-report' => array_keys(DemographicCollections::WORKDAYS_SITE_COLLECTIONS), - 'project-report' => array_keys(DemographicCollections::WORKDAYS_PROJECT_COLLECTIONS), - default => throw new NotFoundHttpException(), - }, - default => throw new NotFoundHttpException() - }; - $collections = $demographics->pluck('collection'); - foreach ($expectedCollections as $collection) { - if (! $collections->contains($collection)) { - $demographic = new Demographic(); - // Allows the resource to return an API response with no demographics, but still containing - // the collection and readable collection name. - $demographic['type'] = $demographicType; - $demographic['demographical_type'] = get_class($entity); - $demographic['collection'] = $collection; - $demographics->push($demographic); - } - } - - return DemographicResource::collection($demographics); - } -} diff --git a/openapi-src/V2/definitions/V2RestorationPartnerRead.yml b/openapi-src/V2/definitions/V2RestorationPartnerRead.yml deleted file mode 100644 index d73463dff..000000000 --- a/openapi-src/V2/definitions/V2RestorationPartnerRead.yml +++ /dev/null @@ -1,13 +0,0 @@ -title: V2RestorationPartnerRead -type: object -properties: - uuid: - type: string - collection: - type: string - readable_collection: - type: string - demographics: - type: array - items: - $ref: './_index.yml#/Demographic' diff --git a/openapi-src/V2/definitions/V2WorkdayRead.yml b/openapi-src/V2/definitions/V2WorkdayRead.yml deleted file mode 100644 index a2d1b0b31..000000000 --- a/openapi-src/V2/definitions/V2WorkdayRead.yml +++ /dev/null @@ -1,13 +0,0 @@ -title: V2WorkdayRead -type: object -properties: - uuid: - type: string - collection: - type: string - readable_collection: - type: string - demographics: - type: array - items: - $ref: './_index.yml#/Demographic' diff --git a/openapi-src/V2/definitions/_index.yml b/openapi-src/V2/definitions/_index.yml index ee17f5500..276d13783 100644 --- a/openapi-src/V2/definitions/_index.yml +++ b/openapi-src/V2/definitions/_index.yml @@ -144,10 +144,6 @@ V2SeedingRead: $ref: './V2SeedingRead.yml' V2SeedingPaginated: $ref: './V2SeedingPaginated.yml' -V2WorkdayRead: - $ref: './V2WorkdayRead.yml' -V2RestorationPartnerRead: - $ref: './V2RestorationPartnerRead.yml' V2DisturbanceRead: $ref: './V2DisturbanceRead.yml' V2DisturbancePaginated: diff --git a/openapi-src/V2/paths/RestorationPartners/get-v2-restoration-partners-entity-uuid.yml b/openapi-src/V2/paths/RestorationPartners/get-v2-restoration-partners-entity-uuid.yml deleted file mode 100644 index 04a144617..000000000 --- a/openapi-src/V2/paths/RestorationPartners/get-v2-restoration-partners-entity-uuid.yml +++ /dev/null @@ -1,24 +0,0 @@ -operationId: get-v2-restoration-partners-entity-uuid -summary: View all restoration partners for a given entity -tags: - - V2 Restoration Partners -parameters: - - type: string - name: ENTITY - in: path - required: true - description: allowed values project-report - - type: string - name: UUID - in: path - required: true -responses: - '200': - description: OK - schema: - type: object - properties: - data: - type: array - items: - $ref: '../../definitions/_index.yml#/V2RestorationPartnerRead' diff --git a/openapi-src/V2/paths/Workdays/get-v2-workdays-entity-uuid.yml b/openapi-src/V2/paths/Workdays/get-v2-workdays-entity-uuid.yml deleted file mode 100644 index b88b41b92..000000000 --- a/openapi-src/V2/paths/Workdays/get-v2-workdays-entity-uuid.yml +++ /dev/null @@ -1,24 +0,0 @@ -operationId: get-v2-workdays-entity-uuid -summary: View all workdays for a given entity -tags: - - V2 Workdays -parameters: - - type: string - name: ENTITY - in: path - required: true - description: allowed values project-report/site-report - - type: string - name: UUID - in: path - required: true -responses: - '200': - description: OK - schema: - type: object - properties: - data: - type: array - items: - $ref: '../../definitions/_index.yml#/V2WorkdayRead' diff --git a/openapi-src/V2/paths/_index.yml b/openapi-src/V2/paths/_index.yml index 897962b34..9535dd2ef 100644 --- a/openapi-src/V2/paths/_index.yml +++ b/openapi-src/V2/paths/_index.yml @@ -110,12 +110,6 @@ /v2/update-requests/{ENTITY}/{UUID}: get: $ref: './UpdateRequests/get-v2-update-requests-entity-uuid.yml' -/v2/workdays/{ENTITY}/{UUID}: - get: - $ref: './Workdays/get-v2-workdays-entity-uuid.yml' -/v2/restoration-partners/{ENTITY}/{UUID}: - get: - $ref: './RestorationPartners/get-v2-restoration-partners-entity-uuid.yml' /v2/stratas/{ENTITY}/{UUID}: get: $ref: './Stratas/get-v2-stratas-entity-uuid.yml' diff --git a/resources/docs/swagger-v2.yml b/resources/docs/swagger-v2.yml index 55ca431e5..32c75160d 100644 --- a/resources/docs/swagger-v2.yml +++ b/resources/docs/swagger-v2.yml @@ -8700,64 +8700,6 @@ definitions: type: integer unfiltered_total: type: integer - V2WorkdayRead: - title: V2WorkdayRead - type: object - properties: - uuid: - type: string - collection: - type: string - readable_collection: - type: string - demographics: - type: array - items: - title: Demographic - type: object - properties: - type: - type: string - enum: - - gender - - age - - ethnicity - - caste - subtype: - type: string - name: - type: string - amount: - type: integer - V2RestorationPartnerRead: - title: V2RestorationPartnerRead - type: object - properties: - uuid: - type: string - collection: - type: string - readable_collection: - type: string - demographics: - type: array - items: - title: Demographic - type: object - properties: - type: - type: string - enum: - - gender - - age - - ethnicity - - caste - subtype: - type: string - name: - type: string - amount: - type: integer V2DisturbanceRead: title: V2DisturbanceRead type: object @@ -57369,112 +57311,6 @@ paths: type: object created_by: type: object - '/v2/workdays/{ENTITY}/{UUID}': - get: - operationId: get-v2-workdays-entity-uuid - summary: View all workdays for a given entity - tags: - - V2 Workdays - parameters: - - type: string - name: ENTITY - in: path - required: true - description: allowed values project-report/site-report - - type: string - name: UUID - in: path - required: true - responses: - '200': - description: OK - schema: - type: object - properties: - data: - type: array - items: - title: V2WorkdayRead - type: object - properties: - uuid: - type: string - collection: - type: string - readable_collection: - type: string - demographics: - type: array - items: - title: Demographic - type: object - properties: - type: - type: string - enum: - - gender - - age - - ethnicity - - caste - subtype: - type: string - name: - type: string - amount: - type: integer - '/v2/restoration-partners/{ENTITY}/{UUID}': - get: - operationId: get-v2-restoration-partners-entity-uuid - summary: View all restoration partners for a given entity - tags: - - V2 Restoration Partners - parameters: - - type: string - name: ENTITY - in: path - required: true - description: allowed values project-report - - type: string - name: UUID - in: path - required: true - responses: - '200': - description: OK - schema: - type: object - properties: - data: - type: array - items: - title: V2RestorationPartnerRead - type: object - properties: - uuid: - type: string - collection: - type: string - readable_collection: - type: string - demographics: - type: array - items: - title: Demographic - type: object - properties: - type: - type: string - enum: - - gender - - age - - ethnicity - - caste - subtype: - type: string - name: - type: string - amount: - type: integer '/v2/stratas/{ENTITY}/{UUID}': get: operationId: get-v2-stratas-entity-uuid diff --git a/routes/api_v2.php b/routes/api_v2.php index acf03d24e..16a93d06d 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -35,7 +35,6 @@ 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\Demographics\GetDemographicsForEntityController; use App\Http\Controllers\V2\Entities\AdminSendReminderController; use App\Http\Controllers\V2\Entities\AdminSoftDeleteEntityController; use App\Http\Controllers\V2\Entities\AdminStatusEntityController; @@ -521,10 +520,6 @@ Route::get('/{entityType}/{uuid}/aggregate-reports', GetAggregateReportsController::class) ->whereIn('entityType', ['project', 'site']); -ModelInterfaceBindingMiddleware::forSlugs(['project-report', 'site-report'], function () { - Route::get('/{entity}', GetDemographicsForEntityController::class); -}, prefix: '{demographicType}')->whereIn('demographicType', ['workdays', 'restoration-partners']); - Route::prefix('leadership-team')->group(function () { Route::post('/', StoreLeadershipTeamController::class); Route::patch('/{leadershipTeam}', UpdateLeadershipTeamController::class); diff --git a/tests/V2/Workdays/GetWorkdaysForEntityControllerTest.php b/tests/V2/Workdays/GetWorkdaysForEntityControllerTest.php deleted file mode 100644 index 3872481bf..000000000 --- a/tests/V2/Workdays/GetWorkdaysForEntityControllerTest.php +++ /dev/null @@ -1,143 +0,0 @@ -create(); - $owner = User::factory()->create(['organisation_id' => $organisation->id]); - - $user = User::factory()->create(); - - $project = Project::factory()->create([ - 'organisation_id' => $organisation->id, - 'framework_key' => 'ppc', - ]); - - $site = Site::factory()->create([ - 'project_id' => $project->id, - 'framework_key' => 'ppc', - 'status' => EntityStatusStateMachine::STARTED, - ]); - - $report = SiteReport::factory()->create([ - 'site_id' => $site->id, - 'framework_key' => 'ppc', - 'status' => EntityStatusStateMachine::STARTED, - ]); - - $uri = '/api/v2/workdays/site-report/' . $report->uuid; - - $this->actingAs($user) - ->getJson($uri) - ->assertStatus(403); - - // The endpoint should return a workday for each collection with empty demographics for each - $response = $this->actingAs($owner) - ->getJson($uri) - ->assertSuccessful() - ->assertJsonCount(count(DemographicCollections::WORKDAYS_SITE_COLLECTIONS), 'data') - ->decodeResponseJson(); - foreach ($response['data'] as $workday) { - $this->assertCount(0, $workday['demographics']); - } - } - - public function test_populated_workdays() - { - $organisation = Organisation::factory()->create(); - $owner = User::factory()->create(['organisation_id' => $organisation->id]); - - $project = Project::factory()->create([ - 'organisation_id' => $organisation->id, - 'framework_key' => 'ppc', - ]); - - $site = Site::factory()->create([ - 'project_id' => $project->id, - 'framework_key' => 'ppc', - 'status' => EntityStatusStateMachine::STARTED, - ]); - - $report = SiteReport::factory()->create([ - 'site_id' => $site->id, - 'framework_key' => 'ppc', - 'status' => EntityStatusStateMachine::STARTED, - ]); - - $workday = Demographic::factory()->create([ - 'demographical_id' => $report->id, - ]); - $femaleCount = DemographicEntry::factory()->gender()->create([ - 'demographic_id' => $workday->id, - 'subtype' => 'female', - ])->amount; - $nonBinaryCount = DemographicEntry::factory()->gender()->create([ - 'demographic_id' => $workday->id, - 'subtype' => 'non-binary', - ])->amount; - $youthCount = DemographicEntry::factory()->age()->create([ - 'demographic_id' => $workday->id, - 'subtype' => 'youth', - ])->amount; - $otherAgeCount = DemographicEntry::factory()->age()->create([ - 'demographic_id' => $workday->id, - 'subtype' => 'other', - ])->amount; - $indigenousCount = DemographicEntry::factory()->ethnicity()->create([ - 'demographic_id' => $workday->id, - 'subtype' => 'indigenous', - 'name' => 'Ohlone', - ])->amount; - - $uri = '/api/v2/workdays/site-report/' . $report->uuid; - - $response = $this->actingAs($owner) - ->getJson($uri) - ->assertSuccessful() - ->assertJsonCount(count(DemographicCollections::WORKDAYS_SITE_COLLECTIONS), 'data') - ->decodeResponseJson(); - $foundCollection = false; - Log::info('response: ' . json_encode($response['data'], JSON_PRETTY_PRINT)); - foreach ($response['data'] as $workdayData) { - $demographics = $workdayData['demographics']; - if ($workdayData['collection'] != $workday->collection) { - $this->assertCount(0, $demographics); - - continue; - } - - $foundCollection = true; - $this->assertCount(5, $demographics); - - // They should be in creation order - $expected = [ - ['type' => 'gender', 'subtype' => 'female', 'name' => null, 'amount' => $femaleCount], - ['type' => 'gender', 'subtype' => 'non-binary', 'name' => null, 'amount' => $nonBinaryCount], - ['type' => 'age', 'subtype' => 'youth', 'name' => null, 'amount' => $youthCount], - ['type' => 'age', 'subtype' => 'other', 'name' => null, 'amount' => $otherAgeCount], - ['type' => 'ethnicity', 'subtype' => 'indigenous', 'name' => 'Ohlone', 'amount' => $indigenousCount], - ]; - $this->assertEquals($expected, $demographics); - } - - $this->assertTrue($foundCollection); - } -} From 11078b4fb191f258be52a260de2845ee824d8525 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Mon, 24 Feb 2025 15:00:48 -0800 Subject: [PATCH 4/8] [TM-1718] Graduate volunteers to its own type. --- .../OneOff/MigrateJobsToDemographics.php | 122 ++++++++++-------- app/Models/Traits/HasDemographics.php | 4 +- app/Models/V2/Demographics/Demographic.php | 3 +- .../Demographics/DemographicCollections.php | 8 +- app/Models/V2/Projects/ProjectReport.php | 6 +- 5 files changed, 81 insertions(+), 62 deletions(-) diff --git a/app/Console/Commands/OneOff/MigrateJobsToDemographics.php b/app/Console/Commands/OneOff/MigrateJobsToDemographics.php index acc61562a..201ccd1f8 100644 --- a/app/Console/Commands/OneOff/MigrateJobsToDemographics.php +++ b/app/Console/Commands/OneOff/MigrateJobsToDemographics.php @@ -27,41 +27,49 @@ class MigrateJobsToDemographics extends Command 'gender' => [ 'male' => 'ft_men', 'female' => 'ft_women', - 'non-binary' => 'ft_other' + 'non-binary' => 'ft_other', ], 'age' => [ 'youth' => 'ft_youth', - 'non-youth' => 'ft_jobs_non_youth' + 'non-youth' => 'ft_jobs_non_youth', ], - 'total' => 'ft_total' + 'total' => 'ft_total', ], 'part-time' => [ 'gender' => [ 'male' => 'pt_men', 'female' => 'pt_women', - 'non-binary' => 'pt_other' + 'non-binary' => 'pt_other', ], 'age' => [ 'youth' => 'pt_youth', - 'non-youth' => 'pt_non_youth' + 'non-youth' => 'pt_non_youth', ], - 'total' => 'pt_total' + 'total' => 'pt_total', ], + ]; + + protected const VOLUNTEERS_MAPPING = [ 'volunteer' => [ 'gender' => [ 'male' => 'volunteer_men', 'female' => 'volunteer_women', - 'non-binary' => 'volunteer_other' + 'non-binary' => 'volunteer_other', ], 'age' => [ 'youth' => 'volunteer_youth', - 'non-youth' => 'volunteer_non_youth' + 'non-youth' => 'volunteer_non_youth', ], 'caste' => [ 'marginalized' => 'volunteer_scstobc', ], - 'total' => 'volunteer_total' - ] + 'total' => 'volunteer_total', + ], + ]; + + protected const MIGRATION_MAPPING = [ + 'jobs' => self::JOBS_MAPPING, + 'volunteers' => self::VOLUNTEERS_MAPPING, ]; /** @@ -69,7 +77,7 @@ class MigrateJobsToDemographics extends Command */ public function handle() { - $this->info("Moving project report jobs data to Demographics..."); + $this->info('Moving project report jobs data to Demographics...'); $this->withProgressBar(ProjectReport::count(), function ($progressBar) { ProjectReport::chunkById(100, function ($projectReports) use ($progressBar) { foreach ($projectReports as $projectReport) { @@ -84,56 +92,58 @@ public function handle() private function convertJobs(ProjectReport $projectReport): void { - foreach (self::JOBS_MAPPING as $collection => $types) { - /** @var Demographic $demographic */ - $demographic = null; - foreach ($types as $type => $subtypes) { - if ($type == "total") { - $field = $subtypes; - if ($demographic != null) { - // Make sure gender / age demographics are balanced and reach at least to the "_total" field - // for this type of job from the original report. Pad gender and age demographics with an - // "unknown" if needed. - $genderTotal = $demographic->entries()->gender()->sum('amount'); - $ageTotal = $demographic->entries()->age()->sum('amount'); - $targetTotal = max($genderTotal, $ageTotal, $projectReport[$field]); - if ($genderTotal < $targetTotal) { - $demographic->entries()->create([ - 'type' => 'gender', - 'subtype' => 'unknown', - 'amount' => $targetTotal - $genderTotal, - ]); + foreach (self::MIGRATION_MAPPING as $demographicType => $mapping) { + foreach ($mapping as $collection => $types) { + /** @var Demographic $demographic */ + $demographic = null; + foreach ($types as $type => $subtypes) { + if ($type == 'total') { + $field = $subtypes; + if ($demographic != null) { + // Make sure gender / age demographics are balanced and reach at least to the "_total" field + // for this type of job from the original report. Pad gender and age demographics with an + // "unknown" if needed. + $genderTotal = $demographic->entries()->gender()->sum('amount'); + $ageTotal = $demographic->entries()->age()->sum('amount'); + $targetTotal = max($genderTotal, $ageTotal, $projectReport[$field]); + if ($genderTotal < $targetTotal) { + $demographic->entries()->create([ + 'type' => 'gender', + 'subtype' => 'unknown', + 'amount' => $targetTotal - $genderTotal, + ]); + } + if ($ageTotal < $targetTotal) { + $demographic->entries()->create([ + 'type' => 'age', + 'subtype' => 'unknown', + 'amount' => $targetTotal - $ageTotal, + ]); + } } - if ($ageTotal < $targetTotal) { - $demographic->entries()->create([ - 'type' => 'age', - 'subtype' => 'unknown', - 'amount' => $targetTotal - $ageTotal, - ]); + } else { + // If none of the fields for this type exist, skip + $fields = collect(array_values($subtypes)); + if ($fields->first(fn ($field) => $projectReport[$field] > 0) == null) { + continue; } - } - } else { - // If none of the fields for this type exist, skip - $fields = collect(array_values($subtypes)); - if ($fields->first(fn($field) => $projectReport[$field] > 0) == null) { - continue; - } - if ($demographic == null) { - $demographic = $projectReport->demographics()->create([ - 'type' => 'jobs', - 'collection' => $collection, - ]); - } - foreach ($subtypes as $subtype => $field) { - $value = $projectReport[$field]; - if ($value > 0) { - $demographic->entries()->create([ - 'type' => $type, - 'subtype' => $subtype, - 'amount' => $value + if ($demographic == null) { + $demographic = $projectReport->demographics()->create([ + 'type' => $demographicType, + 'collection' => $collection, ]); } + foreach ($subtypes as $subtype => $field) { + $value = $projectReport[$field]; + if ($value > 0) { + $demographic->entries()->create([ + 'type' => $type, + 'subtype' => $subtype, + 'amount' => $value, + ]); + } + } } } } diff --git a/app/Models/Traits/HasDemographics.php b/app/Models/Traits/HasDemographics.php index dbfdef7fc..cdbf52cb6 100644 --- a/app/Models/Traits/HasDemographics.php +++ b/app/Models/Traits/HasDemographics.php @@ -18,7 +18,7 @@ trait HasDemographics 'indirectRestorationPartners' => ['type' => Demographic::RESTORATION_PARTNER_TYPE, 'collections' => 'indirect'], 'jobsFullTimeTotal' => ['type' => Demographic::JOBS_TYPE, 'collections' => 'full-time'], 'jobsPartTimeTotal' => ['type' => Demographic::JOBS_TYPE, 'collections' => 'part-time'], - 'jobsVolunteerTotal' => ['type' => Demographic::JOBS_TYPE, 'collections' => 'volunteer'], + 'volunteersTotal' => ['type' => Demographic::VOLUNTEERS_TYPE, 'collections' => 'volunteer'], ]; public static function bootHasDemographics() @@ -46,6 +46,8 @@ public static function bootHasDemographics() Demographic::JOBS_TYPE => collect([ $collectionSets['full-time'], $collectionSets['part-time'], + ])->flatten(), + Demographic::VOLUNTEERS_TYPE => collect([ $collectionSets['volunteer'], ])->flatten(), default => throw new InternalErrorException("Unrecognized demographic type: $demographicType"), diff --git a/app/Models/V2/Demographics/Demographic.php b/app/Models/V2/Demographics/Demographic.php index 94129e34f..e797540e5 100644 --- a/app/Models/V2/Demographics/Demographic.php +++ b/app/Models/V2/Demographics/Demographic.php @@ -34,8 +34,9 @@ class Demographic extends Model implements HandlesLinkedFieldSync public const WORKDAY_TYPE = 'workdays'; public const RESTORATION_PARTNER_TYPE = 'restoration-partners'; public const JOBS_TYPE = 'jobs'; + public const VOLUNTEERS_TYPE = 'volunteers'; - public const VALID_TYPES = [self::WORKDAY_TYPE, self::RESTORATION_PARTNER_TYPE, self::JOBS_TYPE]; + public const VALID_TYPES = [self::WORKDAY_TYPE, self::RESTORATION_PARTNER_TYPE, self::JOBS_TYPE, self::VOLUNTEERS_TYPE]; // In TM-1681 we moved several "name" values to "subtype". This check helps make sure that both in-flight // work at the time of release, and updates from update requests afterward honor that change. diff --git a/app/Models/V2/Demographics/DemographicCollections.php b/app/Models/V2/Demographics/DemographicCollections.php index c61c0c309..fd8dcdc49 100644 --- a/app/Models/V2/Demographics/DemographicCollections.php +++ b/app/Models/V2/Demographics/DemographicCollections.php @@ -91,11 +91,15 @@ class DemographicCollections public const FULL_TIME = 'full-time'; public const PART_TIME = 'part-time'; - public const VOLUNTEER = 'volunteer'; public const JOBS_PROJECT_COLLECTIONS = [ self::FULL_TIME => 'Full-time', self::PART_TIME => 'Part-time', - self::VOLUNTEER => 'Volunteer' + ]; + + public const VOLUNTEER = 'volunteer'; + + public const VOLUNTEERS_PROJECT_COLLECTIONS = [ + self::VOLUNTEER => 'Volunteer', ]; } diff --git a/app/Models/V2/Projects/ProjectReport.php b/app/Models/V2/Projects/ProjectReport.php index 6a534c1ef..2a7679773 100644 --- a/app/Models/V2/Projects/ProjectReport.php +++ b/app/Models/V2/Projects/ProjectReport.php @@ -269,10 +269,12 @@ class ProjectReport extends Model implements MediaModel, AuditableContract, Repo 'part-time' => [ DemographicCollections::PART_TIME, ], + ], + Demographic::VOLUNTEERS_TYPE => [ 'volunteer' => [ DemographicCollections::VOLUNTEER, - ] - ] + ], + ], ]; public function registerMediaConversions(Media $media = null): void From 35efd3329fd7f76de023522d716654f1d013a964 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Mon, 24 Feb 2025 17:11:58 -0800 Subject: [PATCH 5/8] [TM-1718] Handle jobs / volunteers as a linked field type. --- app/Models/Traits/UsesLinkedFields.php | 2 ++ app/Models/V2/Demographics/Demographic.php | 3 +++ config/wri/linked-fields.php | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/app/Models/Traits/UsesLinkedFields.php b/app/Models/Traits/UsesLinkedFields.php index 214e316e0..0cb84bd80 100644 --- a/app/Models/Traits/UsesLinkedFields.php +++ b/app/Models/Traits/UsesLinkedFields.php @@ -264,6 +264,8 @@ private function syncRelation(string $property, string $inputType, $data, bool $ 'disturbances', 'workdays', 'restorationPartners', + 'jobs', + 'volunteers', 'stratas', 'invasive', 'seedings', diff --git a/app/Models/V2/Demographics/Demographic.php b/app/Models/V2/Demographics/Demographic.php index e797540e5..59d75f5d8 100644 --- a/app/Models/V2/Demographics/Demographic.php +++ b/app/Models/V2/Demographics/Demographic.php @@ -196,6 +196,9 @@ public function getReadableCollectionAttribute(): ?string self::JOBS_TYPE => match ($this->demographical_type) { ProjectReport::class => DemographicCollections::JOBS_PROJECT_COLLECTIONS, }, + self::VOLUNTEERS_TYPE => match ($this->demographical_type) { + ProjectReport::class => DemographicCollections::VOLUNTEERS_PROJECT_COLLECTIONS, + }, default => null }; if (empty($collections)) { diff --git a/config/wri/linked-fields.php b/config/wri/linked-fields.php index ed1b5d575..484ea778d 100644 --- a/config/wri/linked-fields.php +++ b/config/wri/linked-fields.php @@ -681,6 +681,27 @@ 'resource' => 'App\Http\Resources\V2\Demographics\DemographicResource', 'input_type' => 'restorationPartners', 'collection' => 'indirect-other' + ], + 'pro-rep-full-time-jobs' => [ + 'property' => 'jobsFullTime', + 'label' => 'Full-time Jobs', + 'resource' => 'App\Http\Resources\V2\Demographics\DemographicResource', + 'input_type' => 'jobs', + 'collection' => 'full-time' + ], + 'pro-rep-part-time-jobs' => [ + 'property' => 'jobsPartTime', + 'label' => 'Part-time Jobs', + 'resource' => 'App\Http\Resources\V2\Demographics\DemographicResource', + 'input_type' => 'jobs', + 'collection' => 'part-time' + ], + 'pro-rep-volunteers' => [ + 'property' => 'volunteersVolunteer', + 'label' => 'Volunteers', + 'resource' => 'App\Http\Resources\V2\Demographics\DemographicResource', + 'input_type' => 'volunteers', + 'collection' => 'volunteer' ] ], 'file-collections' => [ From 905943bb6dbd17cb8b5563d08c925e5c8fe47685 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Tue, 25 Feb 2025 12:00:51 -0800 Subject: [PATCH 6/8] [TM-1718] Remove unused fields --- .../ProjectReports/ProjectReportResource.php | 24 ----------- .../V2/SiteReports/SiteReportResource.php | 3 -- .../V2/definitions/ProjectReportRead.yml | 40 +------------------ openapi-src/V2/definitions/SiteReportRead.yml | 4 +- resources/docs/swagger-v2.yml | 40 ------------------- 5 files changed, 2 insertions(+), 109 deletions(-) diff --git a/app/Http/Resources/V2/ProjectReports/ProjectReportResource.php b/app/Http/Resources/V2/ProjectReports/ProjectReportResource.php index 2dd10fb54..38c67eed9 100644 --- a/app/Http/Resources/V2/ProjectReports/ProjectReportResource.php +++ b/app/Http/Resources/V2/ProjectReports/ProjectReportResource.php @@ -26,8 +26,6 @@ public function toArray($request) 'due_at' => $this->due_at, 'completion' => $this->completion, 'readable_completion_status' => $this->readable_completion_status, - 'workdays_paid' => $this->workdays_paid, - 'workdays_volunteer' => $this->workdays_volunteer, 'total_unique_restoration_partners' => $this->total_unique_restoration_partners, 'direct_restoration_partners' => $this->direct_restoration_partners, 'indirect_restoration_partners' => $this->indirect_restoration_partners, @@ -42,35 +40,18 @@ public function toArray($request) 'pct_survival_to_date' => $this->pct_survival_to_date, 'survival_calculation' => $this->survival_calculation, 'survival_comparison' => $this->survival_comparison, - 'ft_women' => $this->ft_women, - 'ft_men' => $this->ft_men, - 'ft_youth' => $this->ft_youth, 'ft_smallholder_farmers' => $this->ft_smallholder_farmers, - 'ft_total' => $this->ft_total, - 'pt_non_youth' => $this->pt_non_youth, - 'pt_women' => $this->pt_women, - 'pt_men' => $this->pt_men, - 'pt_youth' => $this->pt_youth, 'pt_smallholder_farmers' => $this->pt_smallholder_farmers, - 'pt_total' => $this->pt_total, - 'workdays_total' => $this->workdays_total, 'seasonal_women' => $this->seasonal_women, 'seasonal_men' => $this->seasonal_men, 'seasonal_youth' => $this->seasonal_youth, 'seasonal_smallholder_farmers' => $this->seasonal_smallholder_farmers, 'seasonal_total' => $this->seasonal_total, - 'volunteer_women' => $this->volunteer_women, - 'volunteer_men' => $this->volunteer_men, - 'volunteer_youth' => $this->volunteer_youth, 'volunteer_smallholder_farmers' => $this->volunteer_smallholder_farmers, - 'volunteer_total' => $this->volunteer_total, 'shared_drive_link' => $this->shared_drive_link, 'planted_trees' => $this->planted_trees, 'new_jobs_description' => $this->new_jobs_description, 'volunteers_work_description' => $this->volunteers_work_description, - 'ft_jobs_non_youth' => $this->ft_jobs_non_youth, - 'ft_jobs_youth' => $this->ft_jobs_youth, - 'volunteer_non_youth' => $this->volunteer_non_youth, 'beneficiaries' => $this->beneficiaries, 'beneficiaries_description' => $this->beneficiaries_description, 'beneficiaries_women' => $this->beneficiaries_women, @@ -87,7 +68,6 @@ public function toArray($request) 'project' => new ProjectLiteResource($this->project), 'site_reports_count' => $this->site_reports_count, 'nursery_reports_count' => $this->nursery_reports_count, - 'total_jobs_created' => $this->total_jobs_created, 'task_uuid' => $this->task_uuid, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, @@ -112,7 +92,6 @@ public function toArray($request) 'convergence_schemes' => $this->convergence_schemes, 'convergence_amount' => $this->convergence_amount, 'community_partners_assets_description' => $this->community_partners_assets_description, - 'volunteer_scstobc' => $this->volunteer_scstobc, 'beneficiaries_scstobc_farmers' => $this->beneficiaries_scstobc_farmers, 'beneficiaries_scstobc' => $this->beneficiaries_scstobc, 'people_knowledge_skills_increased' => $this->people_knowledge_skills_increased, @@ -123,9 +102,6 @@ public function toArray($request) 'non_tree_total' => $this->non_tree_total, 'total_community_partners' => $this->total_community_partners, 'business_milestones' => $this->business_milestones, - 'ft_other' => $this->ft_other, - 'pt_other' => $this->pt_other, - 'volunteer_other' => $this->volunteer_other, 'beneficiaries_other' => $this->beneficiaries_other, 'beneficiaries_training_women' => $this->beneficiaries_training_women, 'beneficiaries_training_men' => $this->beneficiaries_training_men, diff --git a/app/Http/Resources/V2/SiteReports/SiteReportResource.php b/app/Http/Resources/V2/SiteReports/SiteReportResource.php index d03f466a8..a90f8f2ea 100644 --- a/app/Http/Resources/V2/SiteReports/SiteReportResource.php +++ b/app/Http/Resources/V2/SiteReports/SiteReportResource.php @@ -30,8 +30,6 @@ public function toArray($request) 'nothing_to_report' => $this->nothing_to_report, 'title' => $this->title, - 'workdays_paid' => $this->workdays_paid, - 'workdays_volunteer' => $this->workdays_volunteer, 'seeds_planted' => $this->seeds_planted, 'technical_narrative' => $this->technical_narrative, 'public_narrative' => $this->public_narrative, @@ -40,7 +38,6 @@ public function toArray($request) 'polygon_status' => $this->polygon_status, 'paid_other_activity_description' => $this->paid_other_activity_description, - 'total_workdays_count' => $this->total_workdays_count, 'total_trees_planted_count' => $this->total_trees_planted_count, 'total_seeds_planted_count' => $this->total_seeds_planted_count, diff --git a/openapi-src/V2/definitions/ProjectReportRead.yml b/openapi-src/V2/definitions/ProjectReportRead.yml index c95d902e4..db70c203f 100644 --- a/openapi-src/V2/definitions/ProjectReportRead.yml +++ b/openapi-src/V2/definitions/ProjectReportRead.yml @@ -37,46 +37,14 @@ properties: type: string survival_comparison: type: string - ft_women: - type: integer - ft_men: - type: integer - ft_youth: - type: integer - ft_smallholder_farmers: - type: integer - ft_total: - type: integer - pt_women: - type: integer - pt_men: - type: integer - pt_youth: - type: integer - pt_smallholder_farmers: - type: integer - pt_total: - type: integer seasonal_women: type: integer seasonal_men: type: integer seasonal_youth: type: integer - seasonal_smallholder_farmers: - type: integer seasonal_total: type: integer - volunteer_women: - type: integer - volunteer_men: - type: integer - volunteer_youth: - type: integer - volunteer_smallholder_farmers: - type: integer - volunteer_total: - type: integer shared_drive_link: type: string planted_trees: @@ -89,12 +57,6 @@ properties: type: integer volunteers_work_description: type: string - ft_jobs_non_youth: - type: integer - ft_jobs_youth: - type: integer - volunteer_non_youth: - type: integer beneficiaries: type: integer beneficiaries_description: @@ -278,4 +240,4 @@ properties: created_by: $ref: './_index.yml#/UserRead' seedlings_grown: - type: integer \ No newline at end of file + type: integer diff --git a/openapi-src/V2/definitions/SiteReportRead.yml b/openapi-src/V2/definitions/SiteReportRead.yml index 8f1c3caa6..454fa164e 100644 --- a/openapi-src/V2/definitions/SiteReportRead.yml +++ b/openapi-src/V2/definitions/SiteReportRead.yml @@ -21,8 +21,6 @@ properties: $ref: './_index.yml#/UserRead' approved_by: $ref: './_index.yml#/UserRead' - workdays_volunteer: - type: integer technical_narrative: type: string public_narrative: @@ -164,4 +162,4 @@ properties: organisation: $ref: './_index.yml#/V2OrganisationRead' task_uuid: - type: string \ No newline at end of file + type: string diff --git a/resources/docs/swagger-v2.yml b/resources/docs/swagger-v2.yml index 32c75160d..2b06cbf61 100644 --- a/resources/docs/swagger-v2.yml +++ b/resources/docs/swagger-v2.yml @@ -39108,8 +39108,6 @@ definitions: created_at: type: string format: date-time - workdays_volunteer: - type: integer technical_narrative: type: string public_narrative: @@ -40211,46 +40209,14 @@ definitions: type: string survival_comparison: type: string - ft_women: - type: integer - ft_men: - type: integer - ft_youth: - type: integer - ft_smallholder_farmers: - type: integer - ft_total: - type: integer - pt_women: - type: integer - pt_men: - type: integer - pt_youth: - type: integer - pt_smallholder_farmers: - type: integer - pt_total: - type: integer seasonal_women: type: integer seasonal_men: type: integer seasonal_youth: type: integer - seasonal_smallholder_farmers: - type: integer seasonal_total: type: integer - volunteer_women: - type: integer - volunteer_men: - type: integer - volunteer_youth: - type: integer - volunteer_smallholder_farmers: - type: integer - volunteer_total: - type: integer shared_drive_link: type: string planted_trees: @@ -40263,12 +40229,6 @@ definitions: type: integer volunteers_work_description: type: string - ft_jobs_non_youth: - type: integer - ft_jobs_youth: - type: integer - volunteer_non_youth: - type: integer beneficiaries: type: integer beneficiaries_description: From c1b70ff068a7f796887cfea3051bfc1a4e592002 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Tue, 25 Feb 2025 14:15:21 -0800 Subject: [PATCH 7/8] [TM-1718] Source the jobs data for dashboard from demographics. --- app/Services/Dashboard/JobsCreatedService.php | 101 +++++++----------- 1 file changed, 37 insertions(+), 64 deletions(-) diff --git a/app/Services/Dashboard/JobsCreatedService.php b/app/Services/Dashboard/JobsCreatedService.php index c9a8813c7..46f479586 100644 --- a/app/Services/Dashboard/JobsCreatedService.php +++ b/app/Services/Dashboard/JobsCreatedService.php @@ -3,8 +3,10 @@ namespace App\Services\Dashboard; use App\Helpers\TerrafundDashboardQueryHelper; +use App\Models\V2\Demographics\Demographic; use App\Models\V2\Projects\ProjectReport; use Illuminate\Http\Request; +use Illuminate\Support\Collection; class JobsCreatedService { @@ -12,85 +14,56 @@ public function calculateJobsCreated(Request $request) { $query = TerrafundDashboardQueryHelper::buildQueryFromRequest($request); - $rawProjectIds = $query - ->select('v2_projects.id', 'organisations.type') - ->get(); + $projectIds = $query->select('v2_projects.id', 'organisations.type'); + /** @var Collection $demographics */ + $demographics = Demographic::where([ + 'demographical_type' => ProjectReport::class, + 'demographical_id' => ProjectReport::whereIn('project_id', $projectIds) + ->where('status', 'approved')->select('id'), + 'visible' => true, + 'type' => 'jobs', + ])->with('entries')->get(); - $allProjectIds = $this->getAllProjectIds($rawProjectIds); - - $totalJobsCreated = $this->getTotalJobsCreated($allProjectIds); - $jobsCreatedDetailed = $this->getJobsCreatedDetailed($allProjectIds); + $all = $this->entries($demographics); + $ft = $this->entries($this->forCollection($demographics, 'full-time')); + $pt = $this->entries($this->forCollection($demographics, 'part-time')); return (object) [ - 'totalJobsCreated' => $totalJobsCreated, - 'total_ft' => (int) $jobsCreatedDetailed->total_ft, - 'total_pt' => (int) $jobsCreatedDetailed->total_pt, - 'total_men' => $this->calculateTotalMen($jobsCreatedDetailed), - 'total_pt_men' => (int) $jobsCreatedDetailed->total_pt_men, - 'total_ft_men' => (int) $jobsCreatedDetailed->total_ft_men, - 'total_women' => $this->calculateTotalWomen($jobsCreatedDetailed), - 'total_pt_women' => (int) $jobsCreatedDetailed->total_pt_women, - 'total_ft_women' => (int) $jobsCreatedDetailed->total_ft_women, - 'total_youth' => $this->calculateTotalYouth($jobsCreatedDetailed), - 'total_pt_youth' => (int) $jobsCreatedDetailed->total_pt_youth, - 'total_ft_youth' => (int) $jobsCreatedDetailed->total_ft_youth, - 'total_non_youth' => $this->calculateTotalNonYouth($jobsCreatedDetailed), - 'total_pt_non_youth' => (int) $jobsCreatedDetailed->total_pt_non_youth, - 'total_ft_non_youth' => (int) $jobsCreatedDetailed->total_ft_non_youth, + 'totalJobsCreated' => $this->sum($this->forType($all, 'gender')), + 'total_ft' => $this->sum($this->forType($ft, 'gender')), + 'total_pt' => $this->sum($this->forType($pt, 'gender')), + 'total_men' => $this->sum($this->forType($all, 'gender', 'male')), + 'total_pt_men' => $this->sum($this->forType($pt, 'gender', 'male')), + 'total_ft_men' => $this->sum($this->forType($ft, 'gender', 'male')), + 'total_women' => $this->sum($this->forType($all, 'gender', 'female')), + 'total_pt_women' => $this->sum($this->forType($pt, 'gender', 'female')), + 'total_ft_women' => $this->sum($this->forType($ft, 'gender', 'female')), + 'total_youth' => $this->sum($this->forType($all, 'age', 'youth')), + 'total_pt_youth' => $this->sum($this->forType($pt, 'age', 'youth')), + 'total_ft_youth' => $this->sum($this->forType($ft, 'age', 'youth')), + 'total_non_youth' => $this->sum($this->forType($all, 'age', 'non-youth')), + 'total_pt_non_youth' => $this->sum($this->forType($pt, 'age', 'non-youth')), + 'total_ft_non_youth' => $this->sum($this->forType($ft, 'age', 'non-youth')), ]; } - private function getAllProjectIds($projectIds) - { - return $projectIds->pluck('id')->toArray(); - } - - private function calculateTotalMen($jobsCreatedDetailed) + private function sum(Collection $entries): int { - return $jobsCreatedDetailed->total_pt_men + $jobsCreatedDetailed->total_ft_men; + return (int) $entries->pluck('amount')->sum() ?? 0; } - private function calculateTotalWomen($jobsCreatedDetailed) + private function entries(Collection $demographics): Collection { - return $jobsCreatedDetailed->total_pt_women + $jobsCreatedDetailed->total_ft_women; + return $demographics->map(fn ($d) => $d->entries)->flatten(); } - private function calculateTotalYouth($jobsCreatedDetailed) + private function forCollection(Collection $demographics, string $collection): Collection { - return $jobsCreatedDetailed->total_pt_youth + $jobsCreatedDetailed->total_ft_youth; - } - - private function calculateTotalNonYouth($jobsCreatedDetailed) - { - return $jobsCreatedDetailed->total_pt_non_youth + $jobsCreatedDetailed->total_ft_non_youth; - } - - private function getTotalJobsCreated($projectIds) - { - $sumData = ProjectReport::whereIn('project_id', $projectIds) - ->where('status', 'approved') - ->selectRaw('SUM(ft_total) as total_ft, SUM(pt_total) as total_pt') - ->first(); - - return $sumData->total_ft + $sumData->total_pt; + return $demographics->filter(fn ($d) => $d->collection == $collection); } - private function getJobsCreatedDetailed($projectIds) + private function forType(Collection $entries, string $type, string $subtype = null): Collection { - return ProjectReport::whereIn('project_id', $projectIds) - ->where('status', 'approved') - ->selectRaw( - 'SUM(ft_total) as total_ft, - SUM(pt_total) as total_pt, - SUM(pt_men) as total_pt_men, - SUM(ft_men) as total_ft_men, - SUM(pt_women) as total_pt_women, - SUM(ft_women) as total_ft_women, - SUM(pt_youth) as total_pt_youth, - SUM(ft_youth) as total_ft_youth, - SUM(pt_non_youth) as total_pt_non_youth, - SUM(ft_jobs_non_youth) as total_ft_non_youth' - ) - ->first(); + return $entries->filter(fn ($e) => $e->type == $type && ($subtype == null || $e->subtype == $subtype)); } } From fe1a5c34a033046fc08028f580563d82d43c934a Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Tue, 25 Feb 2025 15:02:49 -0800 Subject: [PATCH 8/8] [TM-1718] Support jobs via demographics in entity export. --- app/Exports/V2/BaseExportFormSubmission.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Exports/V2/BaseExportFormSubmission.php b/app/Exports/V2/BaseExportFormSubmission.php index af1c95ad8..823190a10 100644 --- a/app/Exports/V2/BaseExportFormSubmission.php +++ b/app/Exports/V2/BaseExportFormSubmission.php @@ -70,6 +70,8 @@ protected function getAnswer(array $field, array $answers, ?string $frameworkKey case 'workdays': case 'restorationPartners': + case 'jobs': + case 'volunteers': $list = []; $demographic = $answer->first(); if ($demographic == null) { @@ -88,7 +90,7 @@ protected function getAnswer(array $field, array $answers, ?string $frameworkKey $list[] = 'age:(' . implode(')(', $types['age']) . ')'; if ($frameworkKey == 'hbf') { $list[] = 'caste:(' . implode(')(', $types['caste']) . ')'; - } else { + } elseif ($field['input_type'] == 'workdays' || $field['input_type'] == 'restorationPartners') { $list[] = 'ethnicity:(' . implode(')(', $types['ethnicity']) . ')'; }