diff --git a/app/Enums/PartnershipEntityType.php b/app/Enums/PartnershipEntityType.php new file mode 100644 index 0000000..ce295a2 --- /dev/null +++ b/app/Enums/PartnershipEntityType.php @@ -0,0 +1,25 @@ +value); + } + + /** + * @return array + */ + public static function options(): array + { + return collect(self::cases()) + ->mapWithKeys(fn (self $type): array => [$type->value => $type->label()]) + ->all(); + } +} diff --git a/app/Filament/Resources/PartnershipIntents/Pages/CreatePartnershipIntent.php b/app/Filament/Resources/PartnershipIntents/Pages/CreatePartnershipIntent.php new file mode 100644 index 0000000..feadd77 --- /dev/null +++ b/app/Filament/Resources/PartnershipIntents/Pages/CreatePartnershipIntent.php @@ -0,0 +1,11 @@ + ListPartnershipIntents::route('/'), + ]; + } + + protected static function translationKey(): string + { + return 'partnership_intents'; + } +} diff --git a/app/Filament/Resources/PartnershipIntents/Schemas/PartnershipIntentForm.php b/app/Filament/Resources/PartnershipIntents/Schemas/PartnershipIntentForm.php new file mode 100644 index 0000000..28c290c --- /dev/null +++ b/app/Filament/Resources/PartnershipIntents/Schemas/PartnershipIntentForm.php @@ -0,0 +1,31 @@ +components([ + TextInput::make('entity_name')->disabled(), + Select::make('entity_type') + ->options(PartnershipEntityType::options()) + ->disabled(), + TextInput::make('activity_domain')->disabled(), + TextInput::make('contact_name')->disabled(), + TextInput::make('contact_role')->disabled(), + TextInput::make('phone')->disabled(), + TextInput::make('email')->disabled(), + Textarea::make('intent')->disabled()->columnSpanFull(), + Toggle::make('personal_data_consent')->disabled(), + ]); + } +} diff --git a/app/Filament/Resources/PartnershipIntents/Tables/PartnershipIntentsTable.php b/app/Filament/Resources/PartnershipIntents/Tables/PartnershipIntentsTable.php new file mode 100644 index 0000000..db49d99 --- /dev/null +++ b/app/Filament/Resources/PartnershipIntents/Tables/PartnershipIntentsTable.php @@ -0,0 +1,25 @@ +columns([ + TextColumn::make('entity_name')->searchable(), + TextColumn::make('entity_type') + ->formatStateUsing(fn (PartnershipEntityType $state): string => $state->label()), + TextColumn::make('contact_name')->searchable(), + TextColumn::make('email')->searchable(), + TextColumn::make('intent')->limit(80)->wrap(), + TextColumn::make('created_at')->dateTime()->sortable(), + ]) + ->defaultSort('created_at', 'desc'); + } +} diff --git a/app/Models/PartnershipIntent.php b/app/Models/PartnershipIntent.php new file mode 100644 index 0000000..939cd59 --- /dev/null +++ b/app/Models/PartnershipIntent.php @@ -0,0 +1,38 @@ + */ + use HasFactory; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'entity_type' => PartnershipEntityType::class, + 'personal_data_consent' => 'boolean', + ]; + } +} diff --git a/database/factories/PartnershipIntentFactory.php b/database/factories/PartnershipIntentFactory.php new file mode 100644 index 0000000..893eb2b --- /dev/null +++ b/database/factories/PartnershipIntentFactory.php @@ -0,0 +1,32 @@ + + */ +class PartnershipIntentFactory extends Factory +{ + /** + * @return array + */ + public function definition(): array + { + return [ + 'entity_name' => fake()->company(), + 'entity_type' => fake()->randomElement(PartnershipEntityType::cases()), + 'activity_domain' => fake()->words(3, true), + 'contact_name' => fake()->name(), + 'contact_role' => fake()->jobTitle(), + 'phone' => fake()->phoneNumber(), + 'email' => fake()->safeEmail(), + 'intent' => fake()->paragraph(), + 'personal_data_consent' => true, + 'locale' => 'ro', + ]; + } +} diff --git a/database/migrations/2026_10_07_081527_create_partnership_intents_table.php b/database/migrations/2026_10_07_081527_create_partnership_intents_table.php new file mode 100644 index 0000000..0b7c4da --- /dev/null +++ b/database/migrations/2026_10_07_081527_create_partnership_intents_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('entity_name'); + $table->string('entity_type'); + $table->string('activity_domain'); + $table->string('contact_name'); + $table->string('contact_role'); + $table->string('phone'); + $table->string('email'); + $table->text('intent'); + $table->boolean('personal_data_consent'); + $table->string('locale', 5); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('partnership_intents'); + } +}; diff --git a/lang/en/admin.php b/lang/en/admin.php index 7b3e551..9fe9f90 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -84,6 +84,11 @@ 'label' => 'Professional Resource', 'plural' => 'Professional Resources', ], + 'partnership_intents' => [ + 'navigation' => 'Partnership intents', + 'label' => 'Partnership intent', + 'plural' => 'Partnership intents', + ], 'resource_submissions' => [ 'navigation' => 'Resource Submissions', 'label' => 'Resource Submission', diff --git a/lang/en/enums.php b/lang/en/enums.php index 3686b76..c3d7d18 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -25,6 +25,11 @@ 'curriculum' => 'Curricula and training materials', ], 'resource_status' => ['draft' => 'Draft', 'published' => 'Published'], + 'partnership_entity_type' => [ + 'public_institution' => 'Public institution', + 'ngo' => 'NGO', + 'private_company' => 'Private company', + ], 'organization_type' => [ 'public_institution' => 'Public institutions', 'ngo' => 'Non-governmental organizations', diff --git a/lang/en/partnership.php b/lang/en/partnership.php new file mode 100644 index 0000000..60953df --- /dev/null +++ b/lang/en/partnership.php @@ -0,0 +1,27 @@ + 'Partnership intent form', + 'intro' => [ + 'ZEROTRAFIC is a collaboration space dedicated to preventing and combating human trafficking. To respond effectively to needs in the field, we want to develop partnerships with public institutions, non-governmental organizations, and private companies that can contribute expertise, resources, or concrete initiatives.', + 'If your organization wants to get involved, please complete the form below. This helps us understand availability and directions for collaboration, so we can build relevant and sustainable projects together.', + 'The data you send will be used exclusively by the National Agency Against Trafficking in Persons (ANITP) to review partnership opportunities and to contact interested people.', + 'Thank you for your interest and for wanting to contribute to the prevention of human trafficking.', + ], + 'entity_name' => 'Entity name', + 'entity_type' => 'Entity type', + 'entity_type_placeholder' => 'Select a type', + 'activity_domain' => 'Field of activity', + 'contact_name' => 'Contact person', + 'contact_role' => 'Role / position', + 'phone' => 'Phone number', + 'email' => 'Email address', + 'intent' => 'Collaboration intent', + 'intent_placeholder' => 'Briefly describe how you would like to contribute', + 'consent' => 'I agree to the processing of my personal data by ANITP for the purpose of reviewing the partnership intent and contacting me.', + 'submit' => 'Submit the form', + 'success' => 'Thank you. The partnership intent has been recorded.', + 'cta_title' => 'Become a partner', + 'cta_body' => 'Public institutions, non-governmental organizations, and private companies can propose a collaboration with ANITP.', + 'cta_button' => 'Complete the form', +]; diff --git a/lang/ro/admin.php b/lang/ro/admin.php index 89e483a..6935afa 100644 --- a/lang/ro/admin.php +++ b/lang/ro/admin.php @@ -84,6 +84,11 @@ 'label' => 'Resursă profesională', 'plural' => 'Resurse profesionale', ], + 'partnership_intents' => [ + 'navigation' => 'Intenții de parteneriat', + 'label' => 'Intenție de parteneriat', + 'plural' => 'Intenții de parteneriat', + ], 'resource_submissions' => [ 'navigation' => 'Trimiteri resurse', 'label' => 'Trimitere resursă', diff --git a/lang/ro/enums.php b/lang/ro/enums.php index 1abc558..6f7788c 100644 --- a/lang/ro/enums.php +++ b/lang/ro/enums.php @@ -25,6 +25,11 @@ 'curriculum' => 'Curricule și materiale formări', ], 'resource_status' => ['draft' => 'Schiță', 'published' => 'Publicat'], + 'partnership_entity_type' => [ + 'public_institution' => 'Instituție publică', + 'ngo' => 'ONG', + 'private_company' => 'Companie privată', + ], 'organization_type' => [ 'public_institution' => 'Instituții publice', 'ngo' => 'Organizații nonguvernamentale', diff --git a/lang/ro/partnership.php b/lang/ro/partnership.php new file mode 100644 index 0000000..cd1cca0 --- /dev/null +++ b/lang/ro/partnership.php @@ -0,0 +1,27 @@ + 'Formular intenție parteneriat', + 'intro' => [ + 'Platforma ZEROTRAFIC este un spațiu de colaborare dedicat prevenirii și combaterii traficului de persoane. Pentru a răspunde cât mai eficient nevoilor din teritoriu, ne dorim să dezvoltăm parteneriate cu instituții publice, organizații neguvernamentale și companii private care pot contribui cu expertiză, resurse sau inițiative concrete.', + 'Dacă organizația dumneavoastră dorește să se implice, vă invităm să completați formularul de mai jos. Prin acest demers, ne propunem să înțelegem mai bine disponibilitatea și direcțiile de colaborare, astfel încât să putem construi împreună proiecte relevante și sustenabile.', + 'Datele transmise vor fi utilizate exclusiv de Agenția Națională Împotriva Traficului de Persoane (ANITP) pentru analiza oportunităților de parteneriat și pentru contactarea persoanelor interesate.', + 'Vă mulțumim pentru interesul acordat și pentru dorința de a contribui la prevenirea traficului de persoane.', + ], + 'entity_name' => 'Numele entității', + 'entity_type' => 'Tipul entității', + 'entity_type_placeholder' => 'Selectează tipul', + 'activity_domain' => 'Domeniul de activitate', + 'contact_name' => 'Persoana de contact', + 'contact_role' => 'Rolul / funcția', + 'phone' => 'Numărul de telefon', + 'email' => 'Adresa de email', + 'intent' => 'Intenția de colaborare', + 'intent_placeholder' => 'Descrieți pe scurt modul în care doriți să contribuiți', + 'consent' => 'Sunt de acord cu prelucrarea datelor cu caracter personal de către ANITP în scopul analizării intenției de parteneriat și al contactării mele.', + 'submit' => 'Trimite formularul', + 'success' => 'Mulțumim. Intenția de parteneriat a fost înregistrată.', + 'cta_title' => 'Devino partener', + 'cta_body' => 'Instituțiile publice, organizațiile neguvernamentale și companiile private pot propune o colaborare cu ANITP.', + 'cta_button' => 'Completează formularul', +]; diff --git "a/resources/views/pages/\342\232\241about.blade.php" "b/resources/views/pages/\342\232\241about.blade.php" index 15b82e2..7c03ef0 100644 --- "a/resources/views/pages/\342\232\241about.blade.php" +++ "b/resources/views/pages/\342\232\241about.blade.php" @@ -62,11 +62,5 @@ public function pageCopy(): ?StaticPage - -
-

{{ __('about.cta_title') }}

-

{{ __('about.cta_body') }}

- {{ __('about.cta_button') }} -
diff --git "a/resources/views/pages/\342\232\241contact.blade.php" "b/resources/views/pages/\342\232\241contact.blade.php" index 1f5013d..a33d993 100644 --- "a/resources/views/pages/\342\232\241contact.blade.php" +++ "b/resources/views/pages/\342\232\241contact.blade.php" @@ -70,5 +70,11 @@ public function organizationKeys(): array @endforeach + +
+

{{ __('partnership.cta_title') }}

+

{{ __('partnership.cta_body') }}

+ {{ __('partnership.cta_button') }} +
diff --git "a/resources/views/pages/\342\232\241partnership.blade.php" "b/resources/views/pages/\342\232\241partnership.blade.php" new file mode 100644 index 0000000..5d5e296 --- /dev/null +++ "b/resources/views/pages/\342\232\241partnership.blade.php" @@ -0,0 +1,106 @@ +validate([ + 'entityName' => ['required', 'string', 'max:255'], + 'entityType' => ['required', Rule::enum(PartnershipEntityType::class)], + 'activityDomain' => ['required', 'string', 'max:255'], + 'contactName' => ['required', 'string', 'max:255'], + 'contactRole' => ['required', 'string', 'max:255'], + 'phone' => ['required', 'string', 'max:50'], + 'email' => ['required', 'email', 'max:255'], + 'intent' => ['required', 'string', 'max:5000'], + 'personalDataConsent' => ['accepted'], + ]); + + PartnershipIntent::query()->create([ + 'entity_name' => $validated['entityName'], + 'entity_type' => $validated['entityType'], + 'activity_domain' => $validated['activityDomain'], + 'contact_name' => $validated['contactName'], + 'contact_role' => $validated['contactRole'], + 'phone' => $validated['phone'], + 'email' => $validated['email'], + 'intent' => $validated['intent'], + 'personal_data_consent' => true, + 'locale' => app()->getLocale(), + ]); + + $this->reset(); + $this->submitted = true; + } +}; +?> + +
+ + +
+ + @foreach (__('partnership.intro') as $paragraph) +

{{ $paragraph }}

+ @endforeach +
+ + @if ($submitted) + + @endif + +
+ + + @foreach (PartnershipEntityType::cases() as $type) + + @endforeach + + + + +
+ + +
+ + + + @error('personalDataConsent') +

{{ $message }}

+ @enderror + + {{ __('partnership.submit') }} + +
+
diff --git a/routes/web.php b/routes/web.php index f031a3c..cb6e116 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,7 +3,7 @@ use App\Http\Middleware\SetLocale; use Illuminate\Support\Facades\Route; -Route::redirect('/', '/' . config('cpc.default_locale', 'ro')); +Route::redirect('/', '/'.config('cpc.default_locale', 'ro')); Route::prefix('{locale}') ->where(['locale' => 'ro|en']) @@ -45,6 +45,7 @@ Route::livewire('/submit', 'pages::submit')->name('submit.index'); Route::livewire('/about', 'pages::about')->name('about'); Route::livewire('/contact', 'pages::contact')->name('contact'); + Route::livewire('/partnership', 'pages::partnership')->name('partnership.index'); Route::livewire('/partner-organizations', 'pages::partners-index')->name('partners.index'); Route::livewire('/terms', 'pages::static-page')->defaults('slug', 'terms')->name('terms'); Route::livewire('/privacy', 'pages::static-page')->defaults('slug', 'privacy')->name('privacy'); diff --git a/tests/Feature/LocaleTest.php b/tests/Feature/LocaleTest.php index 8b2be7d..9722a53 100644 --- a/tests/Feature/LocaleTest.php +++ b/tests/Feature/LocaleTest.php @@ -20,8 +20,8 @@ }); test('all public routes respond for both locales', function (string $path) { - $this->get('/ro' . $path)->assertOk(); - $this->get('/en' . $path)->assertOk(); + $this->get('/ro'.$path)->assertOk(); + $this->get('/en'.$path)->assertOk(); })->with([ '', '/resources', @@ -32,6 +32,7 @@ '/partner-organizations', '/terms', '/contact', + '/partnership', '/privacy', '/accessibility', ]); diff --git a/tests/Feature/PartnershipIntentTest.php b/tests/Feature/PartnershipIntentTest.php new file mode 100644 index 0000000..9221405 --- /dev/null +++ b/tests/Feature/PartnershipIntentTest.php @@ -0,0 +1,57 @@ +get('/ro/about') + ->assertSuccessful() + ->assertDontSee('Completează formularul', false); + + $this->get('/ro/contact') + ->assertSuccessful() + ->assertSee('Devino partener', false) + ->assertSee('Completează formularul', false) + ->assertSee(localized_route('partnership.index'), false); +}); + +test('a visitor can submit a partnership intent', function () { + $this->get('/ro/partnership') + ->assertSuccessful() + ->assertSee('Formular intenție parteneriat', false) + ->assertSee('Sunt de acord cu prelucrarea datelor cu caracter personal de către ANITP', false); + + Livewire::test('pages::partnership') + ->set('entityName', 'Asociația Exemplu') + ->set('entityType', PartnershipEntityType::Ngo->value) + ->set('activityDomain', 'Protecția copilului') + ->set('contactName', 'Maria Ionescu') + ->set('contactRole', 'Director') + ->set('phone', '021 555 0101') + ->set('email', 'maria@example.org') + ->set('intent', 'Vrem să contribuim cu formări pentru profesioniști.') + ->set('personalDataConsent', true) + ->call('submit') + ->assertHasNoErrors(); + + $intent = PartnershipIntent::query()->where('email', 'maria@example.org')->first(); + + expect($intent)->not->toBeNull() + ->and($intent->entity_type)->toBe(PartnershipEntityType::Ngo) + ->and($intent->personal_data_consent)->toBeTrue(); +}); + +test('admins can see partnership intents in the moderation tab', function () { + $intent = PartnershipIntent::factory()->create([ + 'entity_name' => 'Instituția Parteneră', + ]); + + $this->actingAs(User::factory()->admin()->create()); + + Livewire::test(ListPartnershipIntents::class) + ->assertSee($intent->entity_name) + ->assertSee(mb_substr($intent->intent, 0, 40)); +});