diff --git a/app/Imports/MatchmakingProfileImport.php b/app/Imports/MatchmakingProfileImport.php index a422e700a..f1503a6c5 100644 --- a/app/Imports/MatchmakingProfileImport.php +++ b/app/Imports/MatchmakingProfileImport.php @@ -212,6 +212,33 @@ protected function generateSlug($organisationName, $email): string return $slug; } + /** + * Normalize CSV header keys to a consistent format + */ + protected function normalizeKey(string $key): string + { + $key = str_replace("\xc2\xa0", ' ', $key); // replace non-breaking space + $key = trim($key); + $key = preg_replace('/[^\pL\pN]+/u', '_', $key); + $key = trim($key, '_'); + return mb_strtolower($key); + } + + /** + * Normalize row keys and merge with original keys + */ + protected function normalizeRowKeys(array $row): array + { + $normalized = $row; + foreach ($row as $key => $value) { + $normalizedKey = $this->normalizeKey((string) $key); + if (!array_key_exists($normalizedKey, $normalized)) { + $normalized[$normalizedKey] = $value; + } + } + return $normalized; + } + /** * Get value from row by trying multiple possible key names */ @@ -221,6 +248,10 @@ protected function getRowValue(array $row, array $possibleKeys, $default = null) if (isset($row[$key]) && !empty($row[$key])) { return $row[$key]; } + $normalizedKey = $this->normalizeKey((string) $key); + if (isset($row[$normalizedKey]) && !empty($row[$normalizedKey])) { + return $row[$normalizedKey]; + } } return $default; } @@ -231,6 +262,8 @@ protected function getRowValue(array $row, array $possibleKeys, $default = null) public function model(array $row): ?Model { // Normalize row keys to handle various CSV header formats + $row = $this->normalizeRowKeys($row); + // Try to get email and organisation name with multiple possible key variations $email = $this->getRowValue($row, [ 'email', @@ -246,6 +279,28 @@ public function model(array $row): ?Model 'Organization name', ]); + // Name fields (volunteers) + $fullName = $this->getRowValue($row, [ + 'name', + 'Name', + ]); + $firstName = $this->getRowValue($row, [ + 'first_name', + 'First Name', + ]); + $lastName = $this->getRowValue($row, [ + 'last_name', + 'Last Name', + ]); + $jobTitle = $this->getRowValue($row, [ + 'job_title', + 'Job Title', + ]); + $mainEmailAddress = $this->getRowValue($row, [ + 'main_email_address', + 'Main email address', + ]); + // Skip rows without essential data if (empty($email) && empty($organisationName)) { Log::warning('[MatchmakingProfileImport] Skipping row - missing email and organisation_name', $row); @@ -255,11 +310,23 @@ public function model(array $row): ?Model // Trim values $email = $email ? trim($email) : null; $organisationName = $organisationName ? trim($organisationName) : null; + $fullName = $fullName ? trim($fullName) : null; + $firstName = $firstName ? trim($firstName) : null; + $lastName = $lastName ? trim($lastName) : null; + $jobTitle = $jobTitle ? trim($jobTitle) : null; + $mainEmailAddress = $mainEmailAddress ? trim($mainEmailAddress) : null; // Determine type - if organisation_name exists, it's an organisation, otherwise volunteer $type = !empty($organisationName) ? MatchmakingProfile::TYPE_ORGANISATION : MatchmakingProfile::TYPE_VOLUNTEER; + + // If we only have full name, split into first/last for volunteers + if ($type === MatchmakingProfile::TYPE_VOLUNTEER && empty($firstName) && empty($lastName) && !empty($fullName)) { + $parts = preg_split('/\s+/', $fullName); + $firstName = array_shift($parts); + $lastName = count($parts) ? implode(' ', $parts) : null; + } Log::info('[MatchmakingProfileImport] Processing row', [ 'type' => $type, @@ -485,6 +552,10 @@ public function model(array $row): ?Model $profileData = [ 'type' => $type, 'email' => $email, + 'first_name' => $firstName, + 'last_name' => $lastName, + 'job_title' => $jobTitle, + 'get_email_from' => $mainEmailAddress, 'organisation_name' => $organisationName, 'organisation_type' => $organisationType, 'organisation_mission' => $organisationMission, diff --git a/app/Nova/MatchmakingProfile.php b/app/Nova/MatchmakingProfile.php index faa55aeb5..4f8bfce69 100644 --- a/app/Nova/MatchmakingProfile.php +++ b/app/Nova/MatchmakingProfile.php @@ -29,84 +29,103 @@ class MatchmakingProfile extends Resource public function fields(Request $request) { return [ + // Index fields (ordered) ID::make()->sortable(), - Select::make('Type') ->options(array_combine(MatchmakingProfileModel::getValidTypes(), MatchmakingProfileModel::getValidTypes())) ->displayUsingLabels() ->sortable(), - + Text::make('Organisation Name')->sortable(), + Text::make('Email')->sortable(), + Text::make('Name', function () { + return $this->display_name; + })->onlyOnIndex(), + DateTime::make('Start Time')->sortable(), + DateTime::make('Completion Time')->sortable(), + DateTime::make('Last Modified Time', 'updated_at')->sortable(), + Text::make('Organisation Website', 'website')->onlyOnIndex(), + Text::make('Organisation Type', function () { + return is_array($this->organisation_type) ? implode(', ', $this->organisation_type) : $this->organisation_type; + })->onlyOnIndex(), + Text::make('Main Email Address', 'get_email_from')->onlyOnIndex(), + Text::make('Country/Region/Areas of operation', function () { + return $this->countryModel ? $this->countryModel->name : $this->country; + })->onlyOnIndex(), + Text::make('Want to tell us more about your organisation?', 'organisation_mission')->onlyOnIndex(), + Boolean::make('Do you give your consent to use your logo and display it in the matchmaking directory?', 'is_use_resource')->onlyOnIndex(), + Text::make('What kind of activities or support can you offer to schools and educators? (Select all that apply):', function () { + return is_array($this->support_activities) ? implode(', ', $this->support_activities) : $this->support_activities; + })->onlyOnIndex(), + Text::make('Are you interested in collaborating with schools to bring real-world expertise into the classroom?', 'interested_in_school_collaboration')->onlyOnIndex(), + Text::make('What types of schools are you most interested in working with?', function () { + return is_array($this->target_school_types) ? implode(', ', $this->target_school_types) : $this->target_school_types; + })->onlyOnIndex(), + Text::make('What areas of digital expertise does your organisation or you specialise in?', function () { + return is_array($this->digital_expertise_areas) ? implode(', ', $this->digital_expertise_areas) : $this->digital_expertise_areas; + })->onlyOnIndex(), + Boolean::make('Would you like to be part of the wider EU Code Week community and receive updates about future activities and events?', 'want_updates')->onlyOnIndex(), + Text::make('Do you have any additional information or comments that could help us better match you with schools and educators?', 'description')->onlyOnIndex(), + Boolean::make('By registering as a Digital Volunteer, you agree to being contacted later to share feedback about your experience.', 'agree_to_be_contacted_for_feedback')->onlyOnIndex(), + + // Detail fields Text::make('Slug') ->sortable() ->rules('required', 'max:255') ->hideFromIndex(), - Text::make('Avatar')->hideFromIndex(), - - Text::make('Email')->sortable(), - Text::make('First Name')->sortable(), - Text::make('Last Name')->sortable(), - Text::make('Job Title'), + Text::make('First Name')->hideFromIndex(), + Text::make('Last Name')->hideFromIndex(), + Text::make('Job Title')->hideFromIndex(), Text::make('Linkedin')->hideFromIndex(), Text::make('Facebook')->hideFromIndex(), - Text::make('Website'), - Text::make('Organisation Name')->sortable(), + Text::make('Website')->hideFromIndex(), - // Array fields as JSON textareas Textarea::make('Languages') ->resolveUsing(fn($v) => is_array($v) ? implode(', ', $v) : ($v ?: '')) ->fillUsing(fn($req, $mdl, $attr, $reqAttr) => $mdl->{$attr} = json_decode($req->{$reqAttr}, true)) - ->alwaysShow(), + ->hideFromIndex(), Textarea::make('Organisation Type') ->resolveUsing(fn($v) => is_array($v) ? implode(', ', $v) : ($v ?: '')) ->fillUsing(fn($req, $mdl, $attr, $reqAttr) => $mdl->{$attr} = json_decode($req->{$reqAttr}, true)) - ->alwaysShow(), - - Textarea::make('Organisation Mission')->alwaysShow(), + ->hideFromIndex(), - Text::make('Location'), + Textarea::make('Organisation Mission')->hideFromIndex(), + Text::make('Location')->hideFromIndex(), BelongsTo::make('Country', 'countryModel', \App\Nova\Country::class) ->nullable() ->sortable() - ->searchable(), + ->searchable() + ->hideFromIndex(), Textarea::make('Support Activities') ->resolveUsing(fn($v) => is_array($v) ? implode(', ', $v) : ($v ?: '')) ->fillUsing(fn($req, $mdl, $attr, $reqAttr) => $mdl->{$attr} = json_decode($req->{$reqAttr}, true)) - ->alwaysShow(), - - Text::make('Interested In School Collaboration')->sortable(), + ->hideFromIndex(), + Text::make('Interested In School Collaboration')->hideFromIndex(), Textarea::make('Target School Types') ->resolveUsing(fn($v) => is_array($v) ? implode(', ', $v) : ($v ?: '')) ->fillUsing(fn($req, $mdl, $attr, $reqAttr) => $mdl->{$attr} = json_decode($req->{$reqAttr}, true)) - ->alwaysShow(), - + ->hideFromIndex(), Textarea::make('Time Commitment') ->resolveUsing(fn($v) => is_array($v) ? implode(', ', $v) : ($v ?: '')) ->fillUsing(fn($req, $mdl, $attr, $reqAttr) => $mdl->{$attr} = json_decode($req->{$reqAttr}, true)) - ->alwaysShow(), - + ->hideFromIndex(), Boolean::make('Dark Avatar', 'avatar_dark')->hideFromIndex(), - Boolean::make('Can Start Immediately'), - Textarea::make('Why Volunteering')->alwaysShow(), - + Boolean::make('Can Start Immediately')->hideFromIndex(), + Textarea::make('Why Volunteering')->hideFromIndex(), Select::make('Format') ->options(MatchmakingProfileModel::getValidFormats()) ->displayUsingLabels() - ->sortable(), - - Boolean::make('Is Use Resource'), - Boolean::make('Want Updates'), - Boolean::make('Agree To Be Contacted For Feedback'), - Textarea::make('Description')->alwaysShow(), - - DateTime::make('Start Time')->sortable(), - DateTime::make('Completion Time')->sortable(), - - Boolean::make('Email Via Linkedin'), - Text::make('Get Email From'), + ->sortable() + ->hideFromIndex(), + Boolean::make('Is Use Resource')->hideFromIndex(), + Boolean::make('Want Updates')->hideFromIndex(), + Boolean::make('Agree To Be Contacted For Feedback')->hideFromIndex(), + Textarea::make('Description')->hideFromIndex(), + Boolean::make('Email Via Linkedin')->hideFromIndex(), + Text::make('Get Email From')->hideFromIndex(), ]; }