commit 5e146b8b603a32b0464833946a26a7fc144a4970 Author: OpenClaw Agent Date: Wed Jul 8 22:34:42 2026 -0400 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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc006f0 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/app/Actions/Team/NotifyTeamOfNewApiLead.php b/app/Actions/Team/NotifyTeamOfNewApiLead.php new file mode 100644 index 0000000..3d72c7e --- /dev/null +++ b/app/Actions/Team/NotifyTeamOfNewApiLead.php @@ -0,0 +1,75 @@ +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 '#'; + } + } +} diff --git a/app/Notifications/NewApiLeadNotification.php b/app/Notifications/NewApiLeadNotification.php new file mode 100644 index 0000000..9ca7420 --- /dev/null +++ b/app/Notifications/NewApiLeadNotification.php @@ -0,0 +1,36 @@ + */ + 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'); + } +} diff --git a/app/Observers/CompanyObserver.php b/app/Observers/CompanyObserver.php new file mode 100644 index 0000000..4e4994e --- /dev/null +++ b/app/Observers/CompanyObserver.php @@ -0,0 +1,50 @@ +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(); + } +} diff --git a/app/Observers/PeopleObserver.php b/app/Observers/PeopleObserver.php new file mode 100644 index 0000000..8a33c64 --- /dev/null +++ b/app/Observers/PeopleObserver.php @@ -0,0 +1,28 @@ +invalidateAiSummary(); + } + + public function created(People $people): void + { + if ($people->creation_source === CreationSource::API) { + new NotifyTeamOfNewApiLead()->execute($people); + } + } +} diff --git a/database/migrations/2026_07_08_220000_add_notify_on_api_lead_to_teams_table.php b/database/migrations/2026_07_08_220000_add_notify_on_api_lead_to_teams_table.php new file mode 100644 index 0000000..b24d98c --- /dev/null +++ b/database/migrations/2026_07_08_220000_add_notify_on_api_lead_to_teams_table.php @@ -0,0 +1,24 @@ +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'); + }); + } +};