Import supplier customer directory from TecnoRepair legacy
This commit is contained in:
parent
f065149831
commit
2810d2c70e
441
app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php
Normal file
441
app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Amministratore;
|
||||
use App\Models\AssistenzaTecnorepairScheda;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\FornitoreCliente;
|
||||
use App\Models\RubricaUniversale;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TecnoRepairImportRubricaClientiCommand extends Command
|
||||
{
|
||||
protected $signature = 'tecnorepair:import-rubrica-clienti
|
||||
{amministratore : ID o codice amministratore}
|
||||
{--fornitore-id= : ID fornitore da collegare ai clienti}
|
||||
{--fornitore-term=NETHOME : Ragione sociale o frammento per trovare il fornitore di riferimento}
|
||||
{--dry-run : Simula import senza scrivere}';
|
||||
|
||||
protected $description = 'Materializza la rubrica clienti del fornitore a partire dai record legacy TecnoRepair importati da TClienti.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$admin = $this->resolveAdmin((string) $this->argument('amministratore'));
|
||||
if (! $admin instanceof Amministratore) {
|
||||
$this->error('Amministratore non trovato.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$fornitore = $this->resolveFornitore($admin, (string) $this->option('fornitore-id'), (string) $this->option('fornitore-term'));
|
||||
if (! $fornitore instanceof Fornitore) {
|
||||
$this->error('Fornitore non trovato per questo amministratore.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
$candidates = AssistenzaTecnorepairScheda::query()
|
||||
->where('amministratore_id', (int) $admin->id)
|
||||
->where('fornitore_id', (int) $fornitore->id)
|
||||
->orderByDesc('date_received')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
$customers = $this->collectUniqueCustomers($candidates);
|
||||
|
||||
$stats = [
|
||||
'legacy_rows' => $candidates->count(),
|
||||
'clienti_distinti' => $customers->count(),
|
||||
'rubrica_create' => 0,
|
||||
'rubrica_match' => 0,
|
||||
'rubrica_update' => 0,
|
||||
'fornitore_rows' => 0,
|
||||
];
|
||||
|
||||
$runner = function () use ($customers, $fornitore, $admin, &$stats): void {
|
||||
foreach ($customers as $customer) {
|
||||
$rubrica = $this->resolveOrCreateRubrica($admin, $customer, $stats);
|
||||
|
||||
FornitoreCliente::query()->updateOrCreate(
|
||||
[
|
||||
'fornitore_id' => (int) $fornitore->id,
|
||||
'legacy_cliente_id' => $customer['legacy_cliente_id'],
|
||||
],
|
||||
[
|
||||
'amministratore_id' => (int) $admin->id,
|
||||
'rubrica_id' => $rubrica?->id,
|
||||
'display_name' => $customer['display_name'],
|
||||
'phone' => $customer['phone'],
|
||||
'phone_alt' => $customer['phone_alt'],
|
||||
'email' => $customer['email'],
|
||||
'indirizzo' => $customer['indirizzo'],
|
||||
'cap' => $customer['cap'],
|
||||
'citta' => $customer['citta'],
|
||||
'provincia' => $customer['provincia'],
|
||||
'partita_iva' => $customer['partita_iva'],
|
||||
'codice_fiscale' => $customer['codice_fiscale'],
|
||||
'note' => $customer['note'],
|
||||
'source' => 'tecnorepair_tclienti',
|
||||
'imported_from_path' => $customer['imported_from_path'],
|
||||
'imported_at' => now(),
|
||||
'metadata' => $customer['metadata'],
|
||||
]
|
||||
);
|
||||
|
||||
$stats['fornitore_rows']++;
|
||||
}
|
||||
};
|
||||
|
||||
if ($dryRun) {
|
||||
foreach ($customers as $customer) {
|
||||
$this->resolveOrCreateRubrica($admin, $customer, $stats, true);
|
||||
$stats['fornitore_rows']++;
|
||||
}
|
||||
} else {
|
||||
DB::transaction($runner);
|
||||
}
|
||||
|
||||
$this->info('Import rubrica clienti completato.');
|
||||
$this->table(['Campo', 'Valore'], [
|
||||
['Amministratore', (string) ($admin->denominazione_studio ?: trim((string) ($admin->nome ?? '') . ' ' . (string) ($admin->cognome ?? '')))],
|
||||
['Fornitore', (string) ($fornitore->ragione_sociale ?: ('Fornitore #' . $fornitore->id))],
|
||||
['Dry run', $dryRun ? 'si' : 'no'],
|
||||
['Schede legacy lette', (string) $stats['legacy_rows']],
|
||||
['Clienti distinti', (string) $stats['clienti_distinti']],
|
||||
['Rubrica nuova', (string) $stats['rubrica_create']],
|
||||
['Rubrica match', (string) $stats['rubrica_match']],
|
||||
['Rubrica aggiornata', (string) $stats['rubrica_update']],
|
||||
['Righe fornitore_clienti', (string) $stats['fornitore_rows']],
|
||||
]);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function resolveAdmin(string $value): ?Amministratore
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Amministratore::query()
|
||||
->when(is_numeric($value), fn($query) => $query->orWhere('id', (int) $value))
|
||||
->orWhere('codice_amministratore', $value)
|
||||
->orWhere('codice_univoco', $value)
|
||||
->first();
|
||||
}
|
||||
|
||||
private function resolveFornitore(Amministratore $admin, string $fornitoreIdOption, string $fornitoreTermOption): ?Fornitore
|
||||
{
|
||||
$fornitoreId = (int) $fornitoreIdOption;
|
||||
if ($fornitoreId > 0) {
|
||||
return Fornitore::query()
|
||||
->where('amministratore_id', (int) $admin->id)
|
||||
->whereKey($fornitoreId)
|
||||
->first();
|
||||
}
|
||||
|
||||
$term = trim($fornitoreTermOption);
|
||||
if ($term === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Fornitore::query()
|
||||
->where('amministratore_id', (int) $admin->id)
|
||||
->where(function ($query) use ($term): void {
|
||||
$query->where('ragione_sociale', 'like', '%' . $term . '%')
|
||||
->orWhere('nome', 'like', '%' . $term . '%')
|
||||
->orWhere('cognome', 'like', '%' . $term . '%');
|
||||
})
|
||||
->orderByDesc('is_tecnorepair_primary')
|
||||
->orderBy('ragione_sociale')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, AssistenzaTecnorepairScheda> $rows
|
||||
* @return Collection<int, array<string, mixed>>
|
||||
*/
|
||||
private function collectUniqueCustomers(Collection $rows): Collection
|
||||
{
|
||||
$customers = [];
|
||||
|
||||
foreach ($rows as $scheda) {
|
||||
$cliente = is_array($scheda->metadata['cliente'] ?? null) ? $scheda->metadata['cliente'] : [];
|
||||
|
||||
$legacyClienteId = $this->toInt($cliente['ID'] ?? $scheda->legacy_cliente_id);
|
||||
$displayName = $this->cleanString($cliente['NomeCognome'] ?? $scheda->customer_name);
|
||||
$phone = $this->cleanString($cliente['NumeroTelefono'] ?? $scheda->customer_phone);
|
||||
$phoneAlt = $this->cleanString($cliente['TelFisso'] ?? $scheda->customer_phone_alt);
|
||||
$email = $this->cleanString($cliente['Email'] ?? $scheda->customer_email);
|
||||
$indirizzo = $this->cleanString($cliente['Indirizzo'] ?? null);
|
||||
$cap = $this->cleanString($cliente['Cap'] ?? null);
|
||||
$citta = $this->cleanString($cliente['Citta'] ?? null);
|
||||
$provincia = $this->cleanString($cliente['Prov'] ?? null);
|
||||
$partitaIva = $this->cleanString($cliente['PIVA'] ?? null);
|
||||
$codiceFiscale = $this->cleanString($cliente['CodFis'] ?? null);
|
||||
$note = $this->cleanString($cliente['Annotazioni'] ?? null);
|
||||
|
||||
$identity = $legacyClienteId !== null
|
||||
? 'legacy-' . $legacyClienteId
|
||||
: $this->buildIdentityKey($displayName, $email, $phone, $phoneAlt);
|
||||
|
||||
if ($identity === '' || ($displayName === null && $phone === null && $email === null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! isset($customers[$identity])) {
|
||||
$customers[$identity] = [
|
||||
'legacy_cliente_id' => $legacyClienteId,
|
||||
'display_name' => $displayName,
|
||||
'phone' => $phone,
|
||||
'phone_alt' => $phoneAlt,
|
||||
'email' => $email,
|
||||
'indirizzo' => $indirizzo,
|
||||
'cap' => $cap,
|
||||
'citta' => $citta,
|
||||
'provincia' => $provincia,
|
||||
'partita_iva' => $partitaIva,
|
||||
'codice_fiscale' => $codiceFiscale,
|
||||
'note' => $note,
|
||||
'imported_from_path'=> $this->cleanString($scheda->imported_from_path),
|
||||
'metadata' => [
|
||||
'cliente' => $cliente,
|
||||
'ultima_scheda_id' => (int) $scheda->id,
|
||||
'ultima_scheda_num' => (string) ($scheda->legacy_numero_scheda ?? ''),
|
||||
],
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (['display_name', 'phone', 'phone_alt', 'email', 'indirizzo', 'cap', 'citta', 'provincia', 'partita_iva', 'codice_fiscale', 'note', 'imported_from_path'] as $field) {
|
||||
if (($customers[$identity][$field] ?? null) === null && ${Str::camel($field)} !== null) {
|
||||
$customers[$identity][$field] = ${Str::camel($field)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collect(array_values($customers));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $customer
|
||||
* @param array<string, int> $stats
|
||||
*/
|
||||
private function resolveOrCreateRubrica(Amministratore $admin, array $customer, array &$stats, bool $dryRun = false): ?RubricaUniversale
|
||||
{
|
||||
$query = RubricaUniversale::query()->where('amministratore_id', (int) $admin->id);
|
||||
|
||||
$email = $this->cleanString($customer['email'] ?? null);
|
||||
$phone = $this->normalizePhone($customer['phone'] ?? null);
|
||||
$phoneAlt = $this->normalizePhone($customer['phone_alt'] ?? null);
|
||||
$phoneForRubrica = $this->normalizePhoneField($customer['phone'] ?? null);
|
||||
$phoneAltForRubrica = $this->normalizePhoneField($customer['phone_alt'] ?? null);
|
||||
$cf = $this->cleanString($customer['codice_fiscale'] ?? null);
|
||||
$piva = $this->cleanString($customer['partita_iva'] ?? null);
|
||||
$name = $this->cleanString($customer['display_name'] ?? null);
|
||||
$provincia = $this->normalizeProvince($customer['provincia'] ?? null);
|
||||
|
||||
$rubrica = null;
|
||||
|
||||
if ($cf !== null) {
|
||||
$rubrica = (clone $query)->where('codice_fiscale', $cf)->first();
|
||||
}
|
||||
if (! $rubrica && $piva !== null) {
|
||||
$rubrica = (clone $query)->where('partita_iva', $piva)->first();
|
||||
}
|
||||
if (! $rubrica && $email !== null) {
|
||||
$rubrica = (clone $query)->where('email', $email)->first();
|
||||
}
|
||||
if (! $rubrica && $phone !== null) {
|
||||
$rubrica = (clone $query)->where(function ($phoneQuery) use ($phone): void {
|
||||
$phoneQuery->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$phone])
|
||||
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$phone])
|
||||
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_casa, ''), '[^0-9]', '') = ?", [$phone]);
|
||||
})->first();
|
||||
}
|
||||
if (! $rubrica && $phoneAlt !== null) {
|
||||
$rubrica = (clone $query)->where(function ($phoneQuery) use ($phoneAlt): void {
|
||||
$phoneQuery->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$phoneAlt])
|
||||
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$phoneAlt])
|
||||
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_casa, ''), '[^0-9]', '') = ?", [$phoneAlt]);
|
||||
})->first();
|
||||
}
|
||||
if (! $rubrica && $name !== null) {
|
||||
$rubrica = (clone $query)->where(function ($nameQuery) use ($name): void {
|
||||
$nameQuery->whereRaw("LOWER(TRIM(CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))) = ?", [Str::lower($name)])
|
||||
->orWhereRaw("LOWER(TRIM(COALESCE(ragione_sociale, ''))) = ?", [Str::lower($name)]);
|
||||
})->first();
|
||||
}
|
||||
|
||||
if ($rubrica instanceof RubricaUniversale) {
|
||||
$stats['rubrica_match']++;
|
||||
if (! $dryRun) {
|
||||
$dirty = false;
|
||||
|
||||
foreach ([
|
||||
'email' => $email,
|
||||
'telefono_cellulare' => $phoneForRubrica,
|
||||
'telefono_ufficio' => $phoneAltForRubrica,
|
||||
'indirizzo' => $this->cleanString($customer['indirizzo'] ?? null),
|
||||
'cap' => $this->cleanString($customer['cap'] ?? null),
|
||||
'citta' => $this->cleanString($customer['citta'] ?? null),
|
||||
'provincia' => $provincia,
|
||||
'partita_iva' => $piva,
|
||||
'codice_fiscale' => $cf,
|
||||
] as $field => $value) {
|
||||
if ($value !== null && ! filled($rubrica->{$field})) {
|
||||
$rubrica->{$field} = $value;
|
||||
$dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($dirty) {
|
||||
$rubrica->save();
|
||||
$stats['rubrica_update']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $rubrica;
|
||||
}
|
||||
|
||||
$stats['rubrica_create']++;
|
||||
if ($dryRun) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$nome, $cognome] = $this->splitDisplayName($name);
|
||||
|
||||
return RubricaUniversale::query()->create([
|
||||
'amministratore_id' => (int) $admin->id,
|
||||
'nome' => $nome,
|
||||
'cognome' => $cognome,
|
||||
'tipo_contatto' => 'persona_fisica',
|
||||
'codice_fiscale' => $cf,
|
||||
'partita_iva' => $piva,
|
||||
'indirizzo' => $this->cleanString($customer['indirizzo'] ?? null),
|
||||
'cap' => $this->cleanString($customer['cap'] ?? null),
|
||||
'citta' => $this->cleanString($customer['citta'] ?? null),
|
||||
'provincia' => $provincia,
|
||||
'telefono_cellulare' => $phoneForRubrica,
|
||||
'telefono_ufficio' => $phoneAltForRubrica,
|
||||
'email' => $email,
|
||||
'note' => $this->buildRubricaNote($customer),
|
||||
'categoria' => 'cliente',
|
||||
'stato' => 'attivo',
|
||||
'data_inserimento' => now(),
|
||||
'data_ultima_modifica' => now(),
|
||||
'creato_da' => auth()->id(),
|
||||
'modificato_da' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function splitDisplayName(?string $name): array
|
||||
{
|
||||
$name = trim((string) $name);
|
||||
if ($name === '') {
|
||||
return ['Cliente', 'TecnoRepair'];
|
||||
}
|
||||
|
||||
$parts = preg_split('/\s+/', $name) ?: [];
|
||||
if (count($parts) <= 1) {
|
||||
return [$name, ''];
|
||||
}
|
||||
|
||||
return [trim((string) array_shift($parts)), trim(implode(' ', $parts))];
|
||||
}
|
||||
|
||||
private function buildRubricaNote(array $customer): string
|
||||
{
|
||||
$lines = array_filter([
|
||||
'Import TecnoRepair TClienti per rubrica fornitore.',
|
||||
isset($customer['legacy_cliente_id']) && $customer['legacy_cliente_id'] !== null ? 'Legacy cliente ID: ' . $customer['legacy_cliente_id'] : null,
|
||||
$this->cleanString($customer['note'] ?? null),
|
||||
]);
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private function buildIdentityKey(?string $displayName, ?string $email, ?string $phone, ?string $phoneAlt): string
|
||||
{
|
||||
if ($email !== null) {
|
||||
return 'email-' . Str::lower($email);
|
||||
}
|
||||
|
||||
if ($phone !== null) {
|
||||
return 'phone-' . preg_replace('/\D+/', '', $phone);
|
||||
}
|
||||
|
||||
if ($phoneAlt !== null) {
|
||||
return 'phone-' . preg_replace('/\D+/', '', $phoneAlt);
|
||||
}
|
||||
|
||||
if ($displayName !== null) {
|
||||
return 'name-' . Str::slug(Str::lower($displayName));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function normalizePhone(mixed $value): ?string
|
||||
{
|
||||
$value = preg_replace('/\D+/', '', (string) $value) ?? '';
|
||||
|
||||
return $value !== '' ? $value : null;
|
||||
}
|
||||
|
||||
private function normalizePhoneField(mixed $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/\+?[0-9][0-9\s().\/-]{4,}/', $value, $matches) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = preg_replace('/[^0-9+]/', '', (string) $matches[0]) ?? '';
|
||||
$normalized = ltrim($normalized, '+') === '' ? '' : $normalized;
|
||||
|
||||
return $normalized !== '' ? Str::limit($normalized, 30, '') : null;
|
||||
}
|
||||
|
||||
private function normalizeProvince(mixed $value): ?string
|
||||
{
|
||||
$value = Str::upper(trim((string) $value));
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = preg_replace('/[^A-Z]/', '', $value) ?? '';
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Str::limit($value, 2, '');
|
||||
}
|
||||
|
||||
private function cleanString(mixed $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
|
||||
return $value !== '' ? $value : null;
|
||||
}
|
||||
|
||||
private function toInt(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '' || ! is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
namespace App\Filament\Pages\Fornitore;
|
||||
|
||||
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
|
||||
use App\Models\AssistenzaTecnorepairScheda;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\FornitoreCliente;
|
||||
use App\Models\TicketIntervento;
|
||||
use App\Models\User;
|
||||
use BackedEnum;
|
||||
|
|
@ -37,9 +40,21 @@ class RubricaClienti extends Page
|
|||
|
||||
public string $search = '';
|
||||
|
||||
public string $activeTab = 'elenco';
|
||||
|
||||
public ?int $selectedClienteId = null;
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $rows = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
public array $counters = [
|
||||
'totali' => 0,
|
||||
'collegati' => 0,
|
||||
'con_email' => 0,
|
||||
'con_cellulare' => 0,
|
||||
];
|
||||
|
||||
/** @var array<string, mixed>|null */
|
||||
public ?array $googleWorkspaceStatus = null;
|
||||
|
||||
|
|
@ -71,15 +86,151 @@ public function updatedSearch(): void
|
|||
$this->refreshRows();
|
||||
}
|
||||
|
||||
public function selectTab(string $tab): void
|
||||
{
|
||||
if (! in_array($tab, ['elenco', 'scheda'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->activeTab = $tab;
|
||||
}
|
||||
|
||||
public function selectCliente(int $clienteId): void
|
||||
{
|
||||
$this->selectedClienteId = $clienteId > 0 ? $clienteId : null;
|
||||
$this->activeTab = 'scheda';
|
||||
}
|
||||
|
||||
public function refreshRows(): void
|
||||
{
|
||||
if (! $this->fornitoreId) {
|
||||
$this->rows = [];
|
||||
$this->counters = ['totali' => 0, 'collegati' => 0, 'con_email' => 0, 'con_cellulare' => 0];
|
||||
return;
|
||||
}
|
||||
|
||||
$term = Str::lower(trim($this->search));
|
||||
|
||||
$legacyCounts = AssistenzaTecnorepairScheda::query()
|
||||
->where('fornitore_id', $this->fornitoreId)
|
||||
->selectRaw('legacy_cliente_id, COUNT(*) as totale_schede, MAX(date_received) as ultima_data')
|
||||
->groupBy('legacy_cliente_id')
|
||||
->get()
|
||||
->mapWithKeys(fn(AssistenzaTecnorepairScheda $row): array => [
|
||||
(int) ($row->legacy_cliente_id ?? 0) => [
|
||||
'totale_schede' => (int) ($row->totale_schede ?? 0),
|
||||
'ultima_data' => optional($row->ultima_data)->format('d/m/Y') ?: '-',
|
||||
],
|
||||
])
|
||||
->all();
|
||||
|
||||
$items = FornitoreCliente::query()
|
||||
->with('rubrica:id,nome,cognome,ragione_sociale,email,telefono_cellulare,telefono_ufficio,categoria')
|
||||
->where('fornitore_id', $this->fornitoreId)
|
||||
->when($term !== '', function ($query) use ($term): void {
|
||||
$like = '%' . $term . '%';
|
||||
|
||||
$query->where(function ($inner) use ($like): void {
|
||||
$inner->where('display_name', 'like', $like)
|
||||
->orWhere('phone', 'like', $like)
|
||||
->orWhere('phone_alt', 'like', $like)
|
||||
->orWhere('email', 'like', $like)
|
||||
->orWhere('citta', 'like', $like)
|
||||
->orWhere('indirizzo', 'like', $like);
|
||||
});
|
||||
})
|
||||
->orderByRaw('CASE WHEN rubrica_id IS NULL THEN 1 ELSE 0 END')
|
||||
->orderBy('display_name')
|
||||
->limit(500)
|
||||
->get();
|
||||
|
||||
if ($items->isEmpty()) {
|
||||
$this->rows = $this->buildTicketFallbackRows($term);
|
||||
$this->counters = [
|
||||
'totali' => count($this->rows),
|
||||
'collegati' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['rubrica_id'] ?? null))->count(),
|
||||
'con_email' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['email'] ?? null))->count(),
|
||||
'con_cellulare' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['phone'] ?? null))->count(),
|
||||
];
|
||||
|
||||
$this->selectedClienteId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rows = $items->map(function (FornitoreCliente $cliente) use ($legacyCounts): array {
|
||||
$legacy = $legacyCounts[(int) ($cliente->legacy_cliente_id ?? 0)] ?? ['totale_schede' => 0, 'ultima_data' => '-'];
|
||||
|
||||
return [
|
||||
'id' => (int) $cliente->id,
|
||||
'legacy_cliente_id'=> (int) ($cliente->legacy_cliente_id ?? 0),
|
||||
'display_name' => (string) ($cliente->display_name ?: 'Cliente TecnoRepair'),
|
||||
'phone' => (string) ($cliente->phone ?? ''),
|
||||
'phone_alt' => (string) ($cliente->phone_alt ?? ''),
|
||||
'email' => (string) ($cliente->email ?? ''),
|
||||
'indirizzo' => (string) ($cliente->indirizzo ?? ''),
|
||||
'citta' => (string) ($cliente->citta ?? ''),
|
||||
'provincia' => (string) ($cliente->provincia ?? ''),
|
||||
'rubrica_id' => (int) ($cliente->rubrica_id ?? 0),
|
||||
'rubrica_url' => $cliente->rubrica_id ? RubricaUniversaleScheda::getUrl(['record' => (int) $cliente->rubrica_id], panel: 'admin-filament') : null,
|
||||
'rubrica_label' => (string) ($cliente->rubrica?->categoria ?? ''),
|
||||
'totale_schede' => (int) ($legacy['totale_schede'] ?? 0),
|
||||
'ultima_data' => (string) ($legacy['ultima_data'] ?? '-'),
|
||||
'updated_at' => optional($cliente->updated_at)->format('d/m/Y H:i') ?: '-',
|
||||
'source' => 'tecnorepair_tclienti',
|
||||
];
|
||||
})->all();
|
||||
|
||||
$this->counters = [
|
||||
'totali' => count($this->rows),
|
||||
'collegati' => (int) collect($this->rows)->filter(fn(array $row): bool => (int) ($row['rubrica_id'] ?? 0) > 0)->count(),
|
||||
'con_email' => (int) collect($this->rows)->filter(fn(array $row): bool => trim((string) ($row['email'] ?? '')) !== '')->count(),
|
||||
'con_cellulare' => (int) collect($this->rows)->filter(fn(array $row): bool => trim((string) ($row['phone'] ?? '')) !== '')->count(),
|
||||
];
|
||||
|
||||
if ((int) ($this->selectedClienteId ?? 0) <= 0 && $this->rows !== []) {
|
||||
$this->selectedClienteId = (int) $this->rows[0]['id'];
|
||||
}
|
||||
}
|
||||
|
||||
public function getSelectedClienteProperty(): ?FornitoreCliente
|
||||
{
|
||||
$selectedId = (int) ($this->selectedClienteId ?? 0);
|
||||
if ($selectedId <= 0 || ! $this->fornitoreId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return FornitoreCliente::query()
|
||||
->with('rubrica')
|
||||
->where('fornitore_id', $this->fornitoreId)
|
||||
->find($selectedId);
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
public function getSelectedClienteSchedeProperty(): array
|
||||
{
|
||||
$cliente = $this->selectedCliente;
|
||||
if (! $cliente instanceof FornitoreCliente || ! $this->fornitoreId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return AssistenzaTecnorepairScheda::query()
|
||||
->where('fornitore_id', $this->fornitoreId)
|
||||
->when((int) ($cliente->legacy_cliente_id ?? 0) > 0, fn($query) => $query->where('legacy_cliente_id', (int) $cliente->legacy_cliente_id))
|
||||
->orderByDesc('date_received')
|
||||
->orderByDesc('id')
|
||||
->limit(30)
|
||||
->get()
|
||||
->map(fn(AssistenzaTecnorepairScheda $scheda): array => [
|
||||
'id' => (int) $scheda->id,
|
||||
'numero' => (string) ($scheda->legacy_numero_scheda ?: ('#' . $scheda->id)),
|
||||
'prodotto' => (string) ($scheda->product_model ?: '-'),
|
||||
'codice' => (string) ($scheda->product_code ?: ''),
|
||||
'seriali' => (string) $scheda->serial_label,
|
||||
'stato' => (string) ($scheda->status_label ?: '-'),
|
||||
'status_badge' => (string) $scheda->status_badge_classes,
|
||||
'ingresso' => optional($scheda->date_received)->format('d/m/Y') ?: '-',
|
||||
])->all();
|
||||
|
||||
$items = TicketIntervento::query()
|
||||
->with(['ticket.stabile', 'ticket.soggettoRichiedente', 'ticket.apertoDaUser'])
|
||||
->where('fornitore_id', $this->fornitoreId)
|
||||
|
|
@ -144,6 +295,19 @@ public function getProdottiUrl(): string
|
|||
return ProdottiCatalogo::getUrl(['fornitore' => (int) $this->fornitoreId], panel: 'admin-filament');
|
||||
}
|
||||
|
||||
public function getImportCommandHintProperty(): string
|
||||
{
|
||||
$fornitore = (int) ($this->fornitoreId ?? 0) > 0
|
||||
? Fornitore::query()->select(['id', 'amministratore_id'])->find((int) $this->fornitoreId)
|
||||
: null;
|
||||
|
||||
if (! $fornitore instanceof Fornitore) {
|
||||
return 'php artisan tecnorepair:import-rubrica-clienti {amministratore} --fornitore-id={id}';
|
||||
}
|
||||
|
||||
return 'php artisan tecnorepair:import-rubrica-clienti ' . (int) ($fornitore->amministratore_id ?? 0) . ' --fornitore-id=' . (int) $fornitore->id;
|
||||
}
|
||||
|
||||
public function getGoogleConnectUrl(): string
|
||||
{
|
||||
return route('oauth.google.redirect', [
|
||||
|
|
@ -226,6 +390,59 @@ protected function buildRubricaUrlForCurrentUser(int $rubricaId): ?string
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function buildTicketFallbackRows(string $term = ''): array
|
||||
{
|
||||
$items = TicketIntervento::query()
|
||||
->with(['ticket.stabile', 'ticket.soggettoRichiedente', 'ticket.apertoDaUser'])
|
||||
->where('fornitore_id', $this->fornitoreId)
|
||||
->latest('updated_at')
|
||||
->limit(250)
|
||||
->get();
|
||||
|
||||
$grouped = [];
|
||||
|
||||
foreach ($items as $intervento) {
|
||||
$contact = $this->resolveCallerDataForRubrica($intervento);
|
||||
$key = $contact['identity_key'] !== '' ? $contact['identity_key'] : ('ticket-' . (int) $intervento->ticket_id);
|
||||
|
||||
if ($term !== '') {
|
||||
$haystack = Str::lower(implode(' ', [$contact['contatto'], $contact['telefono'], $contact['email'], $contact['stabile'], $contact['origine']]));
|
||||
if (! Str::contains($haystack, $term)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (! isset($grouped[$key])) {
|
||||
$grouped[$key] = [
|
||||
'id' => 0,
|
||||
'legacy_cliente_id' => 0,
|
||||
'display_name' => $contact['contatto'],
|
||||
'phone' => $contact['telefono'],
|
||||
'phone_alt' => '',
|
||||
'email' => $contact['email'],
|
||||
'indirizzo' => '',
|
||||
'citta' => '',
|
||||
'provincia' => '',
|
||||
'rubrica_id' => 0,
|
||||
'rubrica_url' => $contact['rubrica_url'],
|
||||
'rubrica_label' => $contact['rubrica_label'],
|
||||
'totale_schede' => 1,
|
||||
'ultima_data' => optional($intervento->updated_at)->format('d/m/Y') ?: '-',
|
||||
'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
||||
'source' => 'ticket_fallback',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$grouped[$key]['totale_schede']++;
|
||||
}
|
||||
|
||||
return array_values($grouped);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
|
|
|
|||
58
app/Models/FornitoreCliente.php
Normal file
58
app/Models/FornitoreCliente.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class FornitoreCliente extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'fornitore_clienti';
|
||||
|
||||
protected $fillable = [
|
||||
'amministratore_id',
|
||||
'fornitore_id',
|
||||
'rubrica_id',
|
||||
'legacy_cliente_id',
|
||||
'display_name',
|
||||
'phone',
|
||||
'phone_alt',
|
||||
'email',
|
||||
'indirizzo',
|
||||
'cap',
|
||||
'citta',
|
||||
'provincia',
|
||||
'partita_iva',
|
||||
'codice_fiscale',
|
||||
'note',
|
||||
'source',
|
||||
'imported_from_path',
|
||||
'imported_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'imported_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function amministratore(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Amministratore::class, 'amministratore_id');
|
||||
}
|
||||
|
||||
public function fornitore(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Fornitore::class, 'fornitore_id');
|
||||
}
|
||||
|
||||
public function rubrica(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(RubricaUniversale::class, 'rubrica_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('fornitore_clienti')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('fornitore_clienti', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('amministratore_id')->nullable()->constrained('amministratori')->nullOnDelete();
|
||||
$table->foreignId('fornitore_id')->constrained('fornitori')->cascadeOnDelete();
|
||||
$table->foreignId('rubrica_id')->nullable()->constrained('rubrica_universale')->nullOnDelete();
|
||||
$table->unsignedBigInteger('legacy_cliente_id')->nullable();
|
||||
$table->string('display_name')->nullable();
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('phone_alt')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('indirizzo')->nullable();
|
||||
$table->string('cap')->nullable();
|
||||
$table->string('citta')->nullable();
|
||||
$table->string('provincia')->nullable();
|
||||
$table->string('partita_iva')->nullable();
|
||||
$table->string('codice_fiscale')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->string('source')->nullable();
|
||||
$table->string('imported_from_path')->nullable();
|
||||
$table->timestamp('imported_at')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['fornitore_id', 'legacy_cliente_id'], 'fornitore_clienti_fornitore_legacy_unique');
|
||||
$table->index(['fornitore_id', 'display_name'], 'fornitore_clienti_fornitore_name_idx');
|
||||
$table->index(['fornitore_id', 'rubrica_id'], 'fornitore_clienti_fornitore_rubrica_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('fornitore_clienti');
|
||||
}
|
||||
};
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="text-lg font-semibold">Rubrica clienti fornitore</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
@if($this->fornitoreLabel)
|
||||
Contatti emersi dai ticket e dalle lavorazioni di {{ $this->fornitoreLabel }}, agganciati quando possibile all'anagrafica unica.
|
||||
Elenco clienti del singolo fornitore, con sorgente TecnoRepair `TClienti` e collegamento alla scheda condivisa di anagrafica unica quando disponibile.
|
||||
@else
|
||||
Seleziona un fornitore per aprire la rubrica clienti.
|
||||
@endif
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
||||
Questa vista resta stretta sui contatti del singolo fornitore, ma il punto di verità anagrafico rimane la scheda dell'anagrafica unica. Da qui puoi filtrare i contatti operativi e saltare direttamente sulla scheda condivisa quando il legame esiste gia.
|
||||
Questa vista resta stretta sul fornitore, ma la scheda definitiva resta quella condivisa di anagrafica unica. Prima importa i clienti con <span class="font-mono text-xs">{{ $this->importCommandHint }}</span>, poi usa questa pagina per lavorare in modo compatto su elenco, recapiti e storico apparecchi.
|
||||
</div>
|
||||
|
||||
@if($this->missingAdminContext)
|
||||
|
|
@ -28,46 +28,163 @@
|
|||
Questa vista richiede un fornitore selezionato.
|
||||
</div>
|
||||
@else
|
||||
<div class="grid gap-4 xl:grid-cols-[2fr,1fr]">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="grid gap-3 xl:grid-cols-[1.2fr,0.8fr]">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button" wire:click="selectTab('elenco')" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'elenco' ? 'bg-slate-800 text-white' : 'bg-white text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50' }}">Elenco compatto</button>
|
||||
<button type="button" wire:click="selectTab('scheda')" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'scheda' ? 'bg-slate-800 text-white' : 'bg-white text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50' }}">Scheda cliente</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 xl:grid-cols-4">
|
||||
<div class="rounded-lg border bg-slate-50 px-3 py-2 text-xs">Totali <span class="font-semibold">{{ $counters['totali'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-emerald-50 px-3 py-2 text-xs text-emerald-800">Collegati <span class="font-semibold">{{ $counters['collegati'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-sky-50 px-3 py-2 text-xs text-sky-800">Con email <span class="font-semibold">{{ $counters['con_email'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-amber-50 px-3 py-2 text-xs text-amber-800">Con cellulare <span class="font-semibold">{{ $counters['con_cellulare'] ?? 0 }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-[1.4fr,0.6fr]">
|
||||
<div>
|
||||
@if($activeTab === 'elenco')
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Cerca contatto</span>
|
||||
<input type="text" wire:model.live.debounce.300ms="search" class="w-full rounded-lg border-gray-300" placeholder="Nome, telefono, email, stabile" />
|
||||
<span class="mb-1 block font-medium">Cerca cliente</span>
|
||||
<input type="text" wire:model.live.debounce.300ms="search" class="w-full rounded-lg border-gray-300" placeholder="Nome, telefono, email, citta" />
|
||||
</label>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full border-collapse border text-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 text-slate-700">
|
||||
<th class="border px-2 py-2 text-left">Cliente</th>
|
||||
<th class="border px-2 py-2 text-left">Recapiti</th>
|
||||
<th class="border px-2 py-2 text-left">Zona</th>
|
||||
<th class="border px-2 py-2 text-left">Schede</th>
|
||||
<th class="border px-2 py-2 text-left">Rubrica</th>
|
||||
<th class="border px-2 py-2 text-left">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($rows as $row)
|
||||
<div class="rounded-xl border p-4 text-sm shadow-sm">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-semibold">{{ $row['contatto'] }}</div>
|
||||
<div class="mt-1 text-xs text-gray-500">Origine: {{ $row['origine'] }} · Interventi: {{ $row['totale_interventi'] }}</div>
|
||||
<tr class="{{ ($selectedClienteId ?? 0) === (int) ($row['id'] ?? 0) && (int) ($row['id'] ?? 0) > 0 ? 'bg-sky-50' : 'bg-white hover:bg-slate-50' }}">
|
||||
<td class="border px-2 py-2 align-top">
|
||||
<div class="font-medium text-slate-900">{{ $row['display_name'] }}</div>
|
||||
<div class="text-[11px] text-slate-500">Fonte {{ $row['source'] === 'tecnorepair_tclienti' ? 'TClienti' : 'ticket' }}</div>
|
||||
</td>
|
||||
<td class="border px-2 py-2 align-top">
|
||||
<div>{{ $row['phone'] !== '' ? $row['phone'] : '-' }}</div>
|
||||
<div class="text-[11px] text-slate-500">{{ $row['phone_alt'] !== '' ? $row['phone_alt'] : ($row['email'] !== '' ? $row['email'] : 'nessun recapito') }}</div>
|
||||
</td>
|
||||
<td class="border px-2 py-2 align-top">
|
||||
<div>{{ $row['citta'] !== '' ? $row['citta'] : '-' }}</div>
|
||||
<div class="text-[11px] text-slate-500">{{ $row['indirizzo'] !== '' ? $row['indirizzo'] : '' }}</div>
|
||||
</td>
|
||||
<td class="border px-2 py-2 align-top">
|
||||
<div class="font-medium">{{ $row['totale_schede'] ?? 0 }}</div>
|
||||
<div class="text-[11px] text-slate-500">Ultima {{ $row['ultima_data'] ?? '-' }}</div>
|
||||
</td>
|
||||
<td class="border px-2 py-2 align-top">
|
||||
@if($row['rubrica_url'])
|
||||
<div class="mt-1 text-xs text-indigo-700">Anagrafica unica collegata{{ $row['rubrica_label'] !== '' ? ' · ' . $row['rubrica_label'] : '' }}</div>
|
||||
<a href="{{ $row['rubrica_url'] }}" class="inline-flex items-center rounded-md bg-white px-2 py-1 text-[11px] font-medium text-indigo-700 ring-1 ring-inset ring-indigo-300 hover:bg-indigo-50">Scheda condivisa</a>
|
||||
@else
|
||||
<span class="text-[11px] text-amber-700">Non collegato</span>
|
||||
@endif
|
||||
<div class="mt-2 text-xs text-gray-500">Stabile: {{ $row['stabile'] }}</div>
|
||||
<div class="mt-1 text-xs text-gray-500">Ultimo aggiornamento: {{ $row['updated_at'] }}</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if($row['rubrica_url'])
|
||||
<a href="{{ $row['rubrica_url'] }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-300 hover:bg-indigo-50">Scheda anagrafica</a>
|
||||
</td>
|
||||
<td class="border px-2 py-2 align-top">
|
||||
@if((int) ($row['id'] ?? 0) > 0)
|
||||
<button type="button" wire:click="selectCliente({{ (int) $row['id'] }})" class="inline-flex items-center rounded-md bg-slate-800 px-2 py-1 text-[11px] font-medium text-white hover:bg-slate-700">Apri</button>
|
||||
@endif
|
||||
@if($row['telefono'] !== '')
|
||||
<a href="tel:{{ preg_replace('/\s+/', '', (string) $row['telefono']) }}" class="inline-flex items-center rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-500">Chiama</a>
|
||||
@endif
|
||||
@if($row['email'] !== '')
|
||||
<a href="mailto:{{ $row['email'] }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Email</a>
|
||||
@endif
|
||||
<a href="{{ $row['ultima_scheda_url'] }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Ultima scheda</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed p-6 text-sm text-gray-500">Nessun contatto disponibile per il filtro corrente.</div>
|
||||
<tr>
|
||||
<td colspan="6" class="border px-2 py-6 text-center text-sm text-slate-500">Nessun cliente disponibile. Importa prima `TClienti` nell'archivio legacy e poi lancia il comando rubrica clienti.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
@php
|
||||
$selected = $this->selectedCliente;
|
||||
@endphp
|
||||
@if(! $selected)
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
||||
Seleziona un cliente dall'elenco compatto per aprire la scheda fornitore limitata.
|
||||
</div>
|
||||
@else
|
||||
<div class="grid gap-4 xl:grid-cols-[1.1fr,0.9fr]">
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border bg-slate-50 p-4">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Scheda cliente</div>
|
||||
<div class="mt-2 text-lg font-semibold text-slate-900">{{ $selected->display_name ?: 'Cliente TecnoRepair' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Legacy cliente ID {{ $selected->legacy_cliente_id ?: '-' }} · aggiornato {{ optional($selected->updated_at)->format('d/m/Y H:i') ?: '-' }}</div>
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-lg border bg-white px-3 py-2 text-sm">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Cellulare</div>
|
||||
<div class="mt-1">{{ $selected->phone ?: '-' }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white px-3 py-2 text-sm">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Fisso / email</div>
|
||||
<div class="mt-1">{{ $selected->phone_alt ?: ($selected->email ?: '-') }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white px-3 py-2 text-sm sm:col-span-2">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Indirizzo</div>
|
||||
<div class="mt-1">{{ trim(implode(', ', array_filter([$selected->indirizzo, $selected->cap, $selected->citta, $selected->provincia]))) !== '' ? trim(implode(', ', array_filter([$selected->indirizzo, $selected->cap, $selected->citta, $selected->provincia]))) : '-' }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white px-3 py-2 text-sm">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Codice fiscale</div>
|
||||
<div class="mt-1">{{ $selected->codice_fiscale ?: '-' }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white px-3 py-2 text-sm">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">P. IVA</div>
|
||||
<div class="mt-1">{{ $selected->partita_iva ?: '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($selected->rubrica_id)
|
||||
<div class="mt-4">
|
||||
<a href="{{ \App\Filament\Pages\Gescon\RubricaUniversaleScheda::getUrl(['record' => (int) $selected->rubrica_id], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500">Apri scheda anagrafica condivisa</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Storico apparecchi / seriali</div>
|
||||
<div class="mt-3 overflow-x-auto">
|
||||
<table class="min-w-full border-collapse border text-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 text-slate-700">
|
||||
<th class="border px-2 py-2 text-left">Scheda</th>
|
||||
<th class="border px-2 py-2 text-left">Prodotto</th>
|
||||
<th class="border px-2 py-2 text-left">Seriali</th>
|
||||
<th class="border px-2 py-2 text-left">Stato</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($this->selectedClienteSchede as $scheda)
|
||||
<tr class="bg-white hover:bg-slate-50">
|
||||
<td class="border px-2 py-2 align-top">
|
||||
<div class="font-medium">{{ $scheda['numero'] }}</div>
|
||||
<div class="text-[11px] text-slate-500">{{ $scheda['ingresso'] }}</div>
|
||||
</td>
|
||||
<td class="border px-2 py-2 align-top">
|
||||
<div>{{ $scheda['prodotto'] }}</div>
|
||||
<div class="text-[11px] text-slate-500">{{ $scheda['codice'] !== '' ? $scheda['codice'] : 'senza codice' }}</div>
|
||||
</td>
|
||||
<td class="border px-2 py-2 align-top">{{ $scheda['seriali'] }}</td>
|
||||
<td class="border px-2 py-2 align-top"><span class="inline-flex rounded-full border px-2 py-1 text-[11px] font-medium {{ $scheda['status_badge'] }}">{{ $scheda['stato'] }}</span></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="border px-2 py-4 text-center text-slate-500">Nessuna scheda legacy collegata a questo cliente.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold">Google workspace</div>
|
||||
@if($googleWorkspaceStatus)
|
||||
|
|
@ -86,11 +203,20 @@
|
|||
</form>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-3 text-xs text-gray-500">Questo blocco prepara l'uso fornitore di rubrica, calendario e Drive sullo stesso backend Google gia presente nel progetto.</div>
|
||||
@else
|
||||
<div class="mt-3 text-sm text-gray-500">Fornitore senza amministratore collegato: stato Google non disponibile.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-slate-50 p-4 text-sm text-slate-700">
|
||||
Il modello dati prodotti e seriali è già separato: un prodotto unico può avere più seriali collegati. Nella fase successiva si può rifinire la UI della scheda intervento in stile TecnoRepair e riusare la pipeline FE lato fornitore senza auto-import.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user