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:
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
#
|
||||||
|
# Overlay de Triptyk sobre la imagen pública de Relaticle.
|
||||||
|
# No es un fork: solo copia nuestros ficheros custom encima de
|
||||||
|
# ghcr.io/relaticle/relaticle:latest, así cada build trae las
|
||||||
|
# mejoras de upstream automáticamente. Si upstream toca alguno de
|
||||||
|
# estos ficheros de forma incompatible, el build fallará aquí
|
||||||
|
# (ruidoso, no silencioso) — ver issue rafa/triptyk-crm #(pendiente).
|
||||||
|
#
|
||||||
|
# Ficheros custom que vive este overlay:
|
||||||
|
# - migration: teams.notify_on_api_lead
|
||||||
|
# - Notifications/NewApiLeadNotification (mail)
|
||||||
|
# - Actions/Team/NotifyTeamOfNewApiLead (bell + mail al team)
|
||||||
|
# - Observers/CompanyObserver + PeopleObserver (created() -> notifica si creation_source=API)
|
||||||
|
|
||||||
|
FROM ghcr.io/relaticle/relaticle:latest
|
||||||
|
|
||||||
|
USER www-data
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
COPY --chown=www-data:www-data database/migrations/2026_07_08_220000_add_notify_on_api_lead_to_teams_table.php ./database/migrations/
|
||||||
|
COPY --chown=www-data:www-data app/Notifications/NewApiLeadNotification.php ./app/Notifications/
|
||||||
|
COPY --chown=www-data:www-data app/Actions/Team/NotifyTeamOfNewApiLead.php ./app/Actions/Team/
|
||||||
|
COPY --chown=www-data:www-data app/Observers/CompanyObserver.php ./app/Observers/
|
||||||
|
COPY --chown=www-data:www-data app/Observers/PeopleObserver.php ./app/Observers/
|
||||||
|
|
||||||
|
# Los ficheros nuevos (Notification/Action) no están en el classmap
|
||||||
|
# optimizado que ya trae la imagen base -> hay que regenerarlo o
|
||||||
|
# Composer no los encontrará en producción.
|
||||||
|
RUN composer dump-autoload --optimize --no-dev --working-dir=/var/www/html
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('teams', function (Blueprint $table): void {
|
||||||
|
$table->boolean('notify_on_api_lead')->default(false)->after('personal_team');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('teams', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('notify_on_api_lead');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user