diff --git a/app/Enums/ApplicationStatus.php b/app/Enums/ApplicationStatus.php new file mode 100644 index 0000000..2561d14 --- /dev/null +++ b/app/Enums/ApplicationStatus.php @@ -0,0 +1,15 @@ + $interactions + */ +final class Application extends Model +{ + /** @use HasFactory */ + use HasFactory; + + protected $fillable = [ + 'company_id', + 'role_title', + 'source', + 'status', + 'applied_at', + 'notes', + ]; + + /** @phpstan-return BelongsTo */ + public function company(): BelongsTo + { + return $this->belongsTo(Company::class); + } + + /** @phpstan-return HasMany */ + public function interactions(): HasMany + { + return $this->hasMany(Interaction::class); + } + + protected function casts(): array + { + return [ + 'status' => ApplicationStatus::class, + 'applied_at' => 'immutable_datetime', + ]; + } +} diff --git a/app/Models/Company.php b/app/Models/Company.php new file mode 100644 index 0000000..9771476 --- /dev/null +++ b/app/Models/Company.php @@ -0,0 +1,62 @@ + $applications + * @property-read Collection $contacts + */ +final class Company extends Model +{ + /** @use HasFactory */ + use HasFactory; + + protected $fillable = [ + 'name', + 'kvk_number', + 'website', + 'city', + 'sbi_code', + 'sbi_description', + 'enriched_at', + ]; + + /** @phpstan-return HasMany */ + public function applications(): HasMany + { + return $this->hasMany(Application::class); + } + + /** @phpstan-return HasMany */ + public function contacts(): HasMany + { + return $this->hasMany(Contact::class); + } + + protected function casts(): array + { + return [ + 'enriched_at' => 'immutable_datetime', + ]; + } +} diff --git a/app/Models/Contact.php b/app/Models/Contact.php new file mode 100644 index 0000000..8e4bc11 --- /dev/null +++ b/app/Models/Contact.php @@ -0,0 +1,51 @@ + $interactions + */ +final class Contact extends Model +{ + /** @use HasFactory */ + use HasFactory; + + protected $fillable = [ + 'company_id', + 'name', + 'role', + 'email', + 'phone', + ]; + + /** @phpstan-return BelongsTo */ + public function company(): BelongsTo + { + return $this->belongsTo(Company::class); + } + + /** @phpstan-return HasMany */ + public function interactions(): HasMany + { + return $this->hasMany(Interaction::class); + } +} diff --git a/app/Models/Interaction.php b/app/Models/Interaction.php new file mode 100644 index 0000000..5298604 --- /dev/null +++ b/app/Models/Interaction.php @@ -0,0 +1,59 @@ + */ + use HasFactory; + + protected $fillable = [ + 'application_id', + 'contact_id', + 'type', + 'occurred_at', + 'body', + ]; + + /** @phpstan-return BelongsTo */ + public function application(): BelongsTo + { + return $this->belongsTo(Application::class); + } + + /** @phpstan-return BelongsTo */ + public function contact(): BelongsTo + { + return $this->belongsTo(Contact::class); + } + + protected function casts(): array + { + return [ + 'type' => InteractionType::class, + 'occurred_at' => 'immutable_datetime', + ]; + } +} diff --git a/database/factories/ApplicationFactory.php b/database/factories/ApplicationFactory.php new file mode 100644 index 0000000..6e0ff99 --- /dev/null +++ b/database/factories/ApplicationFactory.php @@ -0,0 +1,64 @@ + + */ +final class ApplicationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'company_id' => Company::factory(), + 'role_title' => $this->faker->jobTitle(), + 'source' => null, + 'status' => ApplicationStatus::APPLIED, + 'applied_at' => null, + 'notes' => null, + ]; + } + + public function lead(): self + { + return $this->set('status', ApplicationStatus::LEAD); + } + + public function interviewing(): self + { + return $this->set('status', ApplicationStatus::INTERVIEWING); + } + + public function offer(): self + { + return $this->set('status', ApplicationStatus::OFFER); + } + + public function rejected(): self + { + return $this->set('status', ApplicationStatus::REJECTED); + } + + public function withdrawn(): self + { + return $this->set('status', ApplicationStatus::WITHDRAWN); + } + + /** @param Company|Factory $company */ + public function forCompany(Company|Factory $company): self + { + return $this->set('company_id', $company); + } +} diff --git a/database/factories/CompanyFactory.php b/database/factories/CompanyFactory.php new file mode 100644 index 0000000..72d3788 --- /dev/null +++ b/database/factories/CompanyFactory.php @@ -0,0 +1,32 @@ + + */ +final class CompanyFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->company(), + 'kvk_number' => null, + 'website' => null, + 'city' => null, + 'sbi_code' => null, + 'sbi_description' => null, + 'enriched_at' => null, + ]; + } +} diff --git a/database/factories/ContactFactory.php b/database/factories/ContactFactory.php new file mode 100644 index 0000000..5a8fce0 --- /dev/null +++ b/database/factories/ContactFactory.php @@ -0,0 +1,37 @@ + + */ +final class ContactFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'company_id' => Company::factory(), + 'name' => $this->faker->name(), + 'role' => null, + 'email' => null, + 'phone' => null, + ]; + } + + /** @param Company|Factory $company */ + public function forCompany(Company|Factory $company): self + { + return $this->set('company_id', $company); + } +} diff --git a/database/factories/InteractionFactory.php b/database/factories/InteractionFactory.php new file mode 100644 index 0000000..1eaf4c1 --- /dev/null +++ b/database/factories/InteractionFactory.php @@ -0,0 +1,60 @@ + + */ +final class InteractionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'application_id' => Application::factory(), + 'contact_id' => null, + 'type' => InteractionType::NOTE, + 'occurred_at' => $this->faker->dateTimeBetween('-30 days'), + 'body' => $this->faker->sentence(), + ]; + } + + public function email(): self + { + return $this->set('type', InteractionType::EMAIL); + } + + public function call(): self + { + return $this->set('type', InteractionType::CALL); + } + + public function interview(): self + { + return $this->set('type', InteractionType::INTERVIEW); + } + + /** @param Application|Factory $application */ + public function forApplication(Application|Factory $application): self + { + return $this->set('application_id', $application); + } + + /** @param Contact|Factory $contact */ + public function forContact(Contact|Factory $contact): self + { + return $this->set('contact_id', $contact); + } +} diff --git a/database/migrations/2026_07_09_131447_create_companies_table.php b/database/migrations/2026_07_09_131447_create_companies_table.php new file mode 100644 index 0000000..475d995 --- /dev/null +++ b/database/migrations/2026_07_09_131447_create_companies_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('name'); + $table->string('kvk_number')->nullable(); + $table->string('website')->nullable(); + $table->string('city')->nullable(); + $table->string('sbi_code') + ->nullable() + ->comment('from KVK'); + $table->string('sbi_description') + ->nullable() + ->comment('from KVK'); + $table->timestamp('enriched_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('companies'); + } +}; diff --git a/database/migrations/2026_07_09_131728_create_applications_table.php b/database/migrations/2026_07_09_131728_create_applications_table.php new file mode 100644 index 0000000..732210c --- /dev/null +++ b/database/migrations/2026_07_09_131728_create_applications_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignIdFor(Company::class); + $table->string('role_title') + ->comment('e.g. "Medior Laravel Developer"'); + $table->string('source') + ->nullable() + ->comment('recruiter / direct'); + $table->string('status')->index(); + $table->timestamp('applied_at')->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('applications'); + } +}; diff --git a/database/migrations/2026_07_09_131821_create_contacts_table.php b/database/migrations/2026_07_09_131821_create_contacts_table.php new file mode 100644 index 0000000..d70df9b --- /dev/null +++ b/database/migrations/2026_07_09_131821_create_contacts_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignIdFor(Company::class); + $table->string('name'); + $table->string('role')->nullable(); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contacts'); + } +}; diff --git a/database/migrations/2026_07_09_131838_create_interactions_table.php b/database/migrations/2026_07_09_131838_create_interactions_table.php new file mode 100644 index 0000000..b085a4b --- /dev/null +++ b/database/migrations/2026_07_09_131838_create_interactions_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignIdFor(Application::class); + $table->foreignIdFor(Contact::class) + ->nullable() + ->comment('not every moment has a person'); + $table->string('type'); + $table->timestamp('occurred_at'); + $table->text('body')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('interactions'); + } +};