Batch 3: Integrări placeholder + Backup tenant

═══ Integrări (Marketing → Integrări) ═══
- /app/integrations Page cu 6 carduri (Telegram/WhatsApp/Google Ads/FB/SMS/Webhook)
- Toggle on/off per integrare; salvare în settings.integrations JSON
- Câmpuri specifice per integrare (token/key/id/secret)
- Banner explicativ: 'placeholder UI — implementare separată'

═══ Backup tenant ═══
- TenantBackupService::export($company) → ZIP cu:
  • data/ (1 JSON per tabel: clients/vehicles/leads/deals/work_orders cu sub-relații/...)
  • media/ (logo + favicon)
  • manifest.json (metadata + counts)
- /app/backup Page cu buton 'Descarcă backup acum'
- Streaming download cu deleteFileAfterSend
- Util pentru: backup local, migrare, audit, GDPR right-to-erasure

Total tenant routes: 104.
Toate cele ~26 module din prototip implementate (sau echivalent funcțional).
This commit is contained in:
2026-05-07 17:36:00 +00:00
parent 4b3201ca1c
commit 6c72fc7db1
5 changed files with 380 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Filament\Tenant\Pages;
use App\Models\Central\Company;
use App\Services\TenantBackupService;
use App\Tenancy\TenantManager;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
class Backup extends Page
{
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-archive-box-arrow-down';
protected static ?string $navigationLabel = 'Backup';
protected static string|\UnitEnum|null $navigationGroup = 'Admin';
protected static ?int $navigationSort = 96;
protected static ?string $title = 'Backup & Export date';
protected string $view = 'filament.tenant.pages.backup';
public function downloadBackup()
{
$tenant = app(TenantManager::class)->current();
if (! $tenant) abort(404);
// Re-fetch via central scope to avoid issues.
$company = Company::withoutGlobalScopes()->find($tenant->id);
if (! $company) abort(404);
$svc = app(TenantBackupService::class);
$file = $svc->export($company);
$filename = $svc->filename($company);
return response()->download($file, $filename, [
'Content-Type' => 'application/zip',
])->deleteFileAfterSend();
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
namespace App\Filament\Tenant\Pages;
use App\Tenancy\TenantManager;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
class Integrations extends Page
{
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-puzzle-piece';
protected static ?string $navigationLabel = 'Integrări';
protected static string|\UnitEnum|null $navigationGroup = 'Marketing';
protected static ?int $navigationSort = 63;
protected static ?string $title = 'Integrări externe';
protected string $view = 'filament.tenant.pages.integrations';
public array $config = [];
public function mount(): void
{
$tenant = app(TenantManager::class)->current();
$this->config = (array) ($tenant?->settings['integrations'] ?? []);
}
public function setStatus(string $key, bool $enabled): void
{
$this->config[$key]['enabled'] = $enabled;
$this->save();
}
public function updateField(string $key, string $field, ?string $value): void
{
$this->config[$key][$field] = $value;
}
public function save(): void
{
$tenant = app(TenantManager::class)->current();
if (! $tenant) return;
$tenant->update([
'settings' => array_merge((array) $tenant->settings, [
'integrations' => $this->config,
]),
]);
Notification::make()->title('Integrări salvate')->success()->send();
}
public function integrations(): array
{
return [
'telegram_bot' => [
'name' => 'Telegram Bot',
'icon' => '✈️',
'color' => '#229ED9',
'description' => 'Primește cereri din canalul Telegram + trimite confirmări automate.',
'fields' => [
'bot_token' => ['label' => 'Bot Token', 'type' => 'password', 'placeholder' => 'din @BotFather'],
'chat_id' => ['label' => 'Channel/Chat ID', 'type' => 'text', 'placeholder' => '@psauto_md'],
],
],
'whatsapp_business' => [
'name' => 'WhatsApp Business',
'icon' => '💬',
'color' => '#25D366',
'description' => 'Trimite mesaje template către clienți (programări, confirmări).',
'fields' => [
'phone_id' => ['label' => 'Phone Number ID', 'type' => 'text'],
'token' => ['label' => 'Permanent Access Token', 'type' => 'password'],
],
],
'google_ads' => [
'name' => 'Google Ads',
'icon' => '🔍',
'color' => '#EA4335',
'description' => 'Tracking conversii — leadurile generate de campanii Google.',
'fields' => [
'customer_id' => ['label' => 'Customer ID', 'type' => 'text', 'placeholder' => '123-456-7890'],
'conversion_id' => ['label' => 'Conversion ID', 'type' => 'text'],
],
],
'facebook' => [
'name' => 'Facebook / Instagram Lead Ads',
'icon' => '📘',
'color' => '#1877F2',
'description' => 'Importă lead-uri direct din Lead Ads forms.',
'fields' => [
'page_id' => ['label' => 'Page ID', 'type' => 'text'],
'access_token' => ['label' => 'Access Token', 'type' => 'password'],
],
],
'sms_gateway' => [
'name' => 'SMS Gateway',
'icon' => '📱',
'color' => '#6366F1',
'description' => 'Trimite SMS-uri prin gateway local (Moldcell/Orange API).',
'fields' => [
'provider' => ['label' => 'Provider', 'type' => 'text', 'placeholder' => 'moldcell / orange / twilio'],
'api_key' => ['label' => 'API Key', 'type' => 'password'],
'sender_id' => ['label' => 'Sender ID', 'type' => 'text'],
],
],
'webhook_in' => [
'name' => 'Webhook intrări (formulare site)',
'icon' => '🪝',
'color' => '#8B5CF6',
'description' => 'URL secret pentru POST direct cu lead-uri din site-ul tău.',
'fields' => [
'secret' => ['label' => 'Secret token', 'type' => 'text', 'placeholder' => 'random string'],
],
],
];
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace App\Services;
use App\Models\Central\Company;
use App\Models\Tenant\Appointment;
use App\Models\Tenant\Client;
use App\Models\Tenant\Deal;
use App\Models\Tenant\EmployeeProfile;
use App\Models\Tenant\Expense;
use App\Models\Tenant\Labor;
use App\Models\Tenant\Lead;
use App\Models\Tenant\Part;
use App\Models\Tenant\Payment;
use App\Models\Tenant\Purchase;
use App\Models\Tenant\PurchaseItem;
use App\Models\Tenant\Supplier;
use App\Models\Tenant\User;
use App\Models\Tenant\Vehicle;
use App\Models\Tenant\WorkOrder;
use App\Models\Tenant\WorkOrderPart;
use App\Models\Tenant\WorkOrderWork;
use ZipArchive;
class TenantBackupService
{
/**
* Export all tenant data to a temporary ZIP file. Returns absolute path.
* Caller is responsible for streaming + cleanup.
*/
public function export(Company $company): string
{
$tmp = storage_path('app/backups');
if (! is_dir($tmp)) @mkdir($tmp, 0775, true);
$file = $tmp . '/tenant-' . $company->slug . '-' . date('Ymd-His') . '.zip';
$zip = new ZipArchive();
if ($zip->open($file, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
throw new \RuntimeException('Nu pot crea fișierul ZIP la ' . $file);
}
// Tables to dump.
$tables = [
'company' => Company::withoutGlobalScopes()->where('id', $company->id)->get(),
'users' => User::all(),
'clients' => Client::all(),
'vehicles' => Vehicle::all(),
'leads' => Lead::all(),
'deals' => Deal::all(),
'appointments' => Appointment::all(),
'work_orders' => WorkOrder::with(['works', 'parts'])->get(),
'wo_works' => WorkOrderWork::all(),
'wo_parts' => WorkOrderPart::all(),
'labors' => Labor::all(),
'parts' => Part::all(),
'suppliers' => Supplier::all(),
'purchases' => Purchase::with('items')->get(),
'purchase_items' => PurchaseItem::all(),
'payments' => Payment::all(),
'expenses' => Expense::all(),
'employee_profiles' => EmployeeProfile::all(),
];
$manifest = [
'tenant' => $company->only(['id', 'slug', 'name', 'city', 'phone', 'email']),
'exported_at' => now()->toIso8601String(),
'app_version' => 'AutoCRM-1.0',
'counts' => [],
];
foreach ($tables as $name => $rows) {
$json = $rows->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$zip->addFromString("data/{$name}.json", $json);
$manifest['counts'][$name] = $rows->count();
}
$zip->addFromString('manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
// Optionally embed media files (logo + favicon).
foreach (['logo', 'favicon'] as $col) {
$m = $company->getFirstMedia($col);
if ($m && file_exists($m->getPath())) {
$zip->addFile($m->getPath(), 'media/' . $col . '.' . pathinfo($m->getPath(), PATHINFO_EXTENSION));
}
}
$zip->close();
return $file;
}
public function filename(Company $company): string
{
return 'tenant-' . $company->slug . '-' . date('Ymd-His') . '.zip';
}
}