Overlay Docker para Coolify: notificación de leads API sobre imagen pública Relaticle

FROM ghcr.io/relaticle/relaticle:latest + COPY de los 5 ficheros
custom (migration + Notification + Action + 2 Observers) que
implementan la notificación in-app/email a un team cuando se crea
una Company o People vía API con creation_source=API.

Se elige overlay en vez de fork completo para no perder las
mejoras de upstream en cada build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 22:34:42 -04:00
commit 5e146b8b60
6 changed files with 243 additions and 0 deletions
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Actions\Team;
use App\Models\Company;
use App\Models\People;
use App\Models\Team;
use App\Notifications\NewApiLeadNotification;
use Filament\Actions\Action;
use Filament\Notifications\Notification as FilamentNotification;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Notification as NotificationFacade;
final readonly class NotifyTeamOfNewApiLead
{
public function execute(Company|People $record): void
{
$team = $record->team;
if ($team === null || ! $team->notify_on_api_lead) {
return;
}
$recipients = $team->allUsers();
if ($recipients->isEmpty()) {
return;
}
[$label, $url] = $this->resolveLabelAndUrl($record);
defer(function () use ($recipients, $label, $record, $url): void {
$recipients->each(function ($recipient) use ($label, $record, $url): void {
FilamentNotification::make()
->title("Nuevo {$label} desde la web: {$record->name}")
->actions([
Action::make('view')
->button()
->label("Ver {$label}")
->url($url)
->markAsRead(),
])
->icon(Heroicon::OutlinedUserPlus)
->iconColor('success')
->sendToDatabase($recipient);
});
NotificationFacade::send(
$recipients,
new NewApiLeadNotification($label, $record->name, $url)
);
});
}
/** @return array{0: string, 1: string} */
private function resolveLabelAndUrl(Company|People $record): array
{
if ($record instanceof Company) {
return ['empresa', $this->resolveUrl(\App\Filament\Resources\CompanyResource::class, $record)];
}
return ['candidato', $this->resolveUrl(\App\Filament\Resources\PeopleResource::class, $record)];
}
private function resolveUrl(string $resourceClass, Company|People $record): string
{
try {
return $resourceClass::getUrl('view', ['record' => $record]);
} catch (\Throwable) {
return '#';
}
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
final class NewApiLeadNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $recordLabel,
private readonly string $recordName,
private readonly string $recordUrl,
) {}
/** @return list<string> */
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject("Nuevo {$this->recordLabel} desde la web: {$this->recordName}")
->line("Se ha recibido un nuevo {$this->recordLabel} a través del formulario web: **{$this->recordName}**.")
->action('Ver en el CRM', $this->recordUrl)
->salutation('Relaticle');
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Observers;
use App\Actions\Team\NotifyTeamOfNewApiLead;
use App\Enums\CreationSource;
use App\Enums\CustomFields\CompanyField;
use App\Jobs\FetchFaviconForCompany;
use App\Models\Company;
final readonly class CompanyObserver
{
public function saved(Company $company): void
{
$company->invalidateAiSummary();
$this->dispatchFaviconFetchIfNeeded($company);
}
public function created(Company $company): void
{
if ($company->creation_source === CreationSource::API) {
new NotifyTeamOfNewApiLead()->execute($company);
}
}
private function dispatchFaviconFetchIfNeeded(Company $company): void
{
$domainField = $company->customFields()
->whereBelongsTo($company->team)
->where('code', CompanyField::DOMAINS->value)
->first();
if ($domainField === null) {
return;
}
$company->load('customFieldValues.customField.options');
$domains = $company->getCustomFieldValue($domainField);
$firstDomain = is_array($domains) ? ($domains[0] ?? null) : $domains;
if (blank($firstDomain)) {
return;
}
dispatch(new FetchFaviconForCompany($company))->afterCommit();
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Observers;
use App\Actions\Team\NotifyTeamOfNewApiLead;
use App\Enums\CreationSource;
use App\Models\People;
final readonly class PeopleObserver
{
/**
* Handle the People "saved" event.
* Invalidate AI summary when person data changes.
*/
public function saved(People $people): void
{
$people->invalidateAiSummary();
}
public function created(People $people): void
{
if ($people->creation_source === CreationSource::API) {
new NotifyTeamOfNewApiLead()->execute($people);
}
}
}