fix: stabilizza ticket mobile e bonifica nominativi
This commit is contained in:
parent
372ed81bdd
commit
0820e06772
270
app/Console/Commands/GesconRepairNominativiContactsCommand.php
Normal file
270
app/Console/Commands/GesconRepairNominativiContactsCommand.php
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\RubricaUniversale;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class GesconRepairNominativiContactsCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'gescon:repair-nominativi-contacts
|
||||||
|
{--stabile= : Codice stabile da bonificare}
|
||||||
|
{--limit=5000 : Max persone da analizzare}
|
||||||
|
{--apply : Applica realmente le modifiche}';
|
||||||
|
|
||||||
|
protected $description = 'Bonifica persone/rubrica dei nominativi materializzati adottando telefoni e recapiti dai duplicati rubrica piu ricchi.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('persone') || ! Schema::hasTable('persone_unita_relazioni') || ! Schema::hasTable('unita_immobiliari') || ! Schema::hasTable('stabili') || ! Schema::hasTable('rubrica_universale')) {
|
||||||
|
$this->error('Tabelle richieste non disponibili.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apply = (bool) $this->option('apply');
|
||||||
|
$limit = max(1, min((int) ($this->option('limit') ?? 5000), 50000));
|
||||||
|
$stabile = trim((string) ($this->option('stabile') ?? ''));
|
||||||
|
|
||||||
|
$query = DB::table('persone_unita_relazioni as pur')
|
||||||
|
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||||
|
->join('unita_immobiliari as ui', 'ui.id', '=', 'pur.unita_id')
|
||||||
|
->join('stabili as s', 's.id', '=', 'ui.stabile_id')
|
||||||
|
->select([
|
||||||
|
'p.id as persona_id',
|
||||||
|
'p.nome',
|
||||||
|
'p.cognome',
|
||||||
|
'p.ragione_sociale',
|
||||||
|
'p.telefono_principale',
|
||||||
|
'p.telefono_secondario',
|
||||||
|
'p.email_principale',
|
||||||
|
'p.email_pec',
|
||||||
|
'p.note',
|
||||||
|
's.id as stabile_id',
|
||||||
|
's.codice_stabile',
|
||||||
|
's.amministratore_id',
|
||||||
|
])
|
||||||
|
->distinct()
|
||||||
|
->orderBy('s.codice_stabile')
|
||||||
|
->orderBy('p.id')
|
||||||
|
->limit($limit);
|
||||||
|
|
||||||
|
if ($stabile !== '') {
|
||||||
|
$trimmed = ltrim($stabile, '0');
|
||||||
|
$query->where(function ($builder) use ($stabile, $trimmed): void {
|
||||||
|
$builder->where('s.codice_stabile', $stabile);
|
||||||
|
if ($trimmed !== '' && $trimmed !== $stabile) {
|
||||||
|
$builder->orWhere('s.codice_stabile', $trimmed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $query->get();
|
||||||
|
if ($rows->isEmpty()) {
|
||||||
|
$this->info('Nessuna persona da bonificare.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'processed' => 0,
|
||||||
|
'persona_updated' => 0,
|
||||||
|
'rubrica_updated' => 0,
|
||||||
|
'no_match' => 0,
|
||||||
|
'phone_conflicts' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$stats['processed']++;
|
||||||
|
|
||||||
|
$identityLabel = trim((string) ($row->ragione_sociale ?: trim(($row->nome ?? '') . ' ' . ($row->cognome ?? ''))));
|
||||||
|
$tokenKey = $this->normalizeTokenSetKey($identityLabel);
|
||||||
|
if ($tokenKey === null) {
|
||||||
|
$stats['no_match']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rubriche = RubricaUniversale::query()
|
||||||
|
->when((int) ($row->amministratore_id ?? 0) > 0, fn($builder) => $builder->where(function ($query) use ($row): void {
|
||||||
|
$query->where('amministratore_id', (int) $row->amministratore_id)
|
||||||
|
->orWhereNull('amministratore_id');
|
||||||
|
}))
|
||||||
|
->get()
|
||||||
|
->filter(fn(RubricaUniversale $rubrica): bool => $this->normalizeTokenSetKey($this->buildRubricaIdentityLabel($rubrica)) === $tokenKey)
|
||||||
|
->sortByDesc(fn(RubricaUniversale $rubrica): int => $this->scoreRubrica($rubrica))
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if ($rubriche->isEmpty()) {
|
||||||
|
$stats['no_match']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var RubricaUniversale $best */
|
||||||
|
$best = $rubriche->first();
|
||||||
|
$canonicalRubricaId = $this->extractRubricaIdFromPersonaNote((string) ($row->note ?? ''));
|
||||||
|
$canonicalRubrica = $canonicalRubricaId > 0 ? RubricaUniversale::query()->find($canonicalRubricaId) : null;
|
||||||
|
if (! $canonicalRubrica instanceof RubricaUniversale) {
|
||||||
|
$canonicalRubrica = $best;
|
||||||
|
}
|
||||||
|
|
||||||
|
$personaUpdates = $this->buildPersonaUpdates($row, $best, $stats);
|
||||||
|
$rubricaUpdates = $this->buildRubricaUpdates($canonicalRubrica, $best);
|
||||||
|
|
||||||
|
if ($apply && $personaUpdates !== []) {
|
||||||
|
try {
|
||||||
|
DB::table('persone')->where('id', (int) $row->persona_id)->update($personaUpdates + ['updated_at' => now()]);
|
||||||
|
$stats['persona_updated']++;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->warn('Skip persona #' . (int) $row->persona_id . ': ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
} elseif (! $apply && $personaUpdates !== []) {
|
||||||
|
$stats['persona_updated']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($apply && $rubricaUpdates !== []) {
|
||||||
|
$canonicalRubrica->fill($rubricaUpdates);
|
||||||
|
$canonicalRubrica->save();
|
||||||
|
$stats['rubrica_updated']++;
|
||||||
|
} elseif (! $apply && $rubricaUpdates !== []) {
|
||||||
|
$stats['rubrica_updated']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rubriche as $candidateRubrica) {
|
||||||
|
if (! $candidateRubrica instanceof RubricaUniversale || (int) $candidateRubrica->id === (int) $best->id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateUpdates = $this->buildRubricaUpdates($candidateRubrica, $best);
|
||||||
|
if ($apply && $candidateUpdates !== []) {
|
||||||
|
$candidateRubrica->fill($candidateUpdates);
|
||||||
|
$candidateRubrica->save();
|
||||||
|
$stats['rubrica_updated']++;
|
||||||
|
} elseif (! $apply && $candidateUpdates !== []) {
|
||||||
|
$stats['rubrica_updated']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = $apply ? 'Bonifica eseguita' : 'Dry-run bonifica';
|
||||||
|
$this->info($mode . ': ' . json_encode($stats, JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRubricaIdentityLabel(RubricaUniversale $rubrica): string
|
||||||
|
{
|
||||||
|
return trim(implode(' ', array_filter([
|
||||||
|
(string) ($rubrica->nome ?? ''),
|
||||||
|
(string) ($rubrica->cognome ?? ''),
|
||||||
|
(string) ($rubrica->ragione_sociale ?? ''),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeTokenSetKey(?string $value): ?string
|
||||||
|
{
|
||||||
|
$tokens = preg_split('/\s+/', Str::upper(trim((string) $value))) ?: [];
|
||||||
|
$tokens = array_values(array_filter(array_map(function (string $token): string {
|
||||||
|
return (string) preg_replace('/[^A-Z0-9]+/u', '', $token);
|
||||||
|
}, $tokens)));
|
||||||
|
|
||||||
|
if ($tokens === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sort($tokens, SORT_STRING);
|
||||||
|
|
||||||
|
return implode('|', $tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePhone(?string $value): ?string
|
||||||
|
{
|
||||||
|
$normalized = preg_replace('/[^\d+]/', '', (string) ($value ?? ''));
|
||||||
|
|
||||||
|
return is_string($normalized) && $normalized !== '' ? $normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeEmail(?string $value): ?string
|
||||||
|
{
|
||||||
|
$normalized = strtolower(trim((string) ($value ?? '')));
|
||||||
|
$normalized = preg_replace('/\s+/', '', $normalized);
|
||||||
|
|
||||||
|
return is_string($normalized) && $normalized !== '' ? $normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function scoreRubrica(RubricaUniversale $rubrica): int
|
||||||
|
{
|
||||||
|
$score = 0;
|
||||||
|
|
||||||
|
foreach (['telefono_cellulare', 'telefono_ufficio', 'telefono_casa', 'email', 'pec', 'codice_fiscale', 'partita_iva', 'riferimento_stabile', 'riferimento_unita'] as $field) {
|
||||||
|
if (filled($rubrica->{$field})) {
|
||||||
|
$score += in_array($field, ['telefono_cellulare', 'email', 'codice_fiscale', 'partita_iva'], true) ? 10 : 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractRubricaIdFromPersonaNote(string $note): int
|
||||||
|
{
|
||||||
|
if (preg_match('/rubrica_id=(\d+)/', $note, $matches)) {
|
||||||
|
return (int) ($matches[1] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param object $row
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildPersonaUpdates(object $row, RubricaUniversale $best, array &$stats): array
|
||||||
|
{
|
||||||
|
$updates = [];
|
||||||
|
|
||||||
|
$bestMobile = $this->normalizePhone((string) ($best->telefono_cellulare ?: $best->telefono_ufficio ?: $best->telefono_casa));
|
||||||
|
$bestOffice = $this->normalizePhone((string) ($best->telefono_ufficio ?: $best->telefono_casa));
|
||||||
|
$bestEmail = $this->normalizeEmail((string) ($best->email ?? ''));
|
||||||
|
$bestPec = $this->normalizeEmail((string) ($best->pec ?? ''));
|
||||||
|
|
||||||
|
if (! filled($row->telefono_principale) && $bestMobile !== null && ! $this->phoneExistsOnOtherPersona($bestMobile, (int) $row->persona_id)) {
|
||||||
|
$updates['telefono_principale'] = $bestMobile;
|
||||||
|
} elseif (! filled($row->telefono_principale) && $bestMobile !== null) {
|
||||||
|
$stats['phone_conflicts']++;
|
||||||
|
}
|
||||||
|
if (! filled($row->telefono_secondario) && $bestOffice !== null && $bestOffice !== ($updates['telefono_principale'] ?? $row->telefono_principale)) {
|
||||||
|
$updates['telefono_secondario'] = $bestOffice;
|
||||||
|
}
|
||||||
|
if (! filled($row->email_principale) && $bestEmail !== null) {
|
||||||
|
$updates['email_principale'] = $bestEmail;
|
||||||
|
}
|
||||||
|
if (! filled($row->email_pec) && $bestPec !== null) {
|
||||||
|
$updates['email_pec'] = $bestPec;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function phoneExistsOnOtherPersona(string $phone, int $personaId): bool
|
||||||
|
{
|
||||||
|
return DB::table('persone')
|
||||||
|
->where('telefono_principale', $phone)
|
||||||
|
->where('id', '!=', $personaId)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildRubricaUpdates(RubricaUniversale $canonical, RubricaUniversale $best): array
|
||||||
|
{
|
||||||
|
$updates = [];
|
||||||
|
|
||||||
|
foreach (['telefono_cellulare', 'telefono_ufficio', 'telefono_casa', 'email', 'pec', 'codice_fiscale', 'partita_iva', 'prefisso_cellulare', 'telefono_cellulare_nazionale'] as $field) {
|
||||||
|
if (! filled($canonical->{$field}) && filled($best->{$field})) {
|
||||||
|
$updates[$field] = $best->{$field};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -115,6 +115,8 @@ class TicketInterventoScheda extends Page
|
||||||
/** @var array<string, mixed>|null */
|
/** @var array<string, mixed>|null */
|
||||||
public ?array $googleWorkspaceStatus = null;
|
public ?array $googleWorkspaceStatus = null;
|
||||||
|
|
||||||
|
public bool $sessionTrackingAvailable = false;
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
@ -209,6 +211,11 @@ public function startIntervento(): void
|
||||||
|
|
||||||
public function startInterventoWithGeo(array $geo = []): void
|
public function startInterventoWithGeo(array $geo = []): void
|
||||||
{
|
{
|
||||||
|
if (! $this->sessionTrackingAvailable) {
|
||||||
|
Notification::make()->title('Sessioni intervento non disponibili')->body('Esegui prima la migrazione del database per attivare start/stop e checkpoint avanzati.')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$activeSession = $this->getActiveSession();
|
$activeSession = $this->getActiveSession();
|
||||||
if ($activeSession instanceof TicketInterventoSessione) {
|
if ($activeSession instanceof TicketInterventoSessione) {
|
||||||
Notification::make()->title('Esiste gia una sessione aperta')->body('Ferma prima la sessione attiva o registra un checkpoint.')->warning()->send();
|
Notification::make()->title('Esiste gia una sessione aperta')->body('Ferma prima la sessione attiva o registra un checkpoint.')->warning()->send();
|
||||||
|
|
@ -219,19 +226,19 @@ public function startInterventoWithGeo(array $geo = []): void
|
||||||
$geoData = $this->normalizeGeoPayload($geo);
|
$geoData = $this->normalizeGeoPayload($geo);
|
||||||
|
|
||||||
TicketInterventoSessione::query()->create([
|
TicketInterventoSessione::query()->create([
|
||||||
'ticket_intervento_id' => (int) $this->intervento->id,
|
'ticket_intervento_id' => (int) $this->intervento->id,
|
||||||
'ticket_id' => (int) $this->intervento->ticket_id,
|
'ticket_id' => (int) $this->intervento->ticket_id,
|
||||||
'fornitore_id' => (int) $this->fornitore->id,
|
'fornitore_id' => (int) $this->fornitore->id,
|
||||||
'fornitore_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id,
|
'fornitore_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id,
|
||||||
'created_by_user_id' => Auth::id(),
|
'created_by_user_id' => Auth::id(),
|
||||||
'session_type' => 'work',
|
'session_type' => 'work',
|
||||||
'note' => 'Avvio intervento',
|
'note' => 'Avvio intervento',
|
||||||
'started_at' => $startedAt,
|
'started_at' => $startedAt,
|
||||||
'start_latitude' => $geoData['lat'],
|
'start_latitude' => $geoData['lat'],
|
||||||
'start_longitude' => $geoData['lng'],
|
'start_longitude' => $geoData['lng'],
|
||||||
'start_accuracy_meters' => $geoData['accuracy'],
|
'start_accuracy_meters' => $geoData['accuracy'],
|
||||||
'start_source' => $geoData['source'],
|
'start_source' => $geoData['source'],
|
||||||
'geo_payload' => $geoData['payload'],
|
'geo_payload' => $geoData['payload'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->intervento->update([
|
$this->intervento->update([
|
||||||
|
|
@ -256,6 +263,11 @@ public function stopIntervento(): void
|
||||||
|
|
||||||
public function stopInterventoWithGeo(array $geo = []): void
|
public function stopInterventoWithGeo(array $geo = []): void
|
||||||
{
|
{
|
||||||
|
if (! $this->sessionTrackingAvailable) {
|
||||||
|
Notification::make()->title('Sessioni intervento non disponibili')->body('Esegui prima la migrazione del database per attivare start/stop e checkpoint avanzati.')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$activeSession = $this->getActiveSession();
|
$activeSession = $this->getActiveSession();
|
||||||
if (! $activeSession instanceof TicketInterventoSessione) {
|
if (! $activeSession instanceof TicketInterventoSessione) {
|
||||||
Notification::make()->title('Avvia prima il timer di intervento')->warning()->send();
|
Notification::make()->title('Avvia prima il timer di intervento')->warning()->send();
|
||||||
|
|
@ -293,6 +305,11 @@ public function stopInterventoWithGeo(array $geo = []): void
|
||||||
|
|
||||||
public function addPresenceCheckpoint(array $geo = []): void
|
public function addPresenceCheckpoint(array $geo = []): void
|
||||||
{
|
{
|
||||||
|
if (! $this->sessionTrackingAvailable) {
|
||||||
|
Notification::make()->title('Sessioni intervento non disponibili')->body('Esegui prima la migrazione del database per attivare start/stop e checkpoint avanzati.')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$geoData = $this->normalizeGeoPayload($geo);
|
$geoData = $this->normalizeGeoPayload($geo);
|
||||||
$stamp = now();
|
$stamp = now();
|
||||||
|
|
||||||
|
|
@ -349,21 +366,20 @@ public function addSuggestedMaterial(int $productId): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$identifier = $product->identifiers
|
$identifier = $product->identifiers
|
||||||
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id)
|
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id) ?? $product->identifiers->first();
|
||||||
?? $product->identifiers->first();
|
|
||||||
|
|
||||||
$offer = $product->offers->first();
|
$offer = $product->offers->first();
|
||||||
|
|
||||||
$this->materialiUtilizzati[] = [
|
$this->materialiUtilizzati[] = [
|
||||||
'product_id' => (int) $product->id,
|
'product_id' => (int) $product->id,
|
||||||
'descrizione' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))),
|
'descrizione' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))),
|
||||||
'barcode' => (string) ($identifier->code_value ?? ''),
|
'barcode' => (string) ($identifier->code_value ?? ''),
|
||||||
'qty' => 1,
|
'qty' => 1,
|
||||||
'unit_price' => $offer?->price_amount !== null ? (float) $offer->price_amount : null,
|
'unit_price' => $offer?->price_amount !== null ? (float) $offer->price_amount : null,
|
||||||
'currency' => (string) ($offer->currency ?? 'EUR'),
|
'currency' => (string) ($offer->currency ?? 'EUR'),
|
||||||
'source_type' => 'catalog',
|
'source_type' => 'catalog',
|
||||||
'source_label' => (string) ($offer?->source_name ?: 'Catalogo fornitore'),
|
'source_label' => (string) ($offer?->source_name ?: 'Catalogo fornitore'),
|
||||||
'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)),
|
'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->materialSearch = '';
|
$this->materialSearch = '';
|
||||||
|
|
@ -379,15 +395,15 @@ public function addManualMaterial(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->materialiUtilizzati[] = [
|
$this->materialiUtilizzati[] = [
|
||||||
'product_id' => null,
|
'product_id' => null,
|
||||||
'descrizione' => $term,
|
'descrizione' => $term,
|
||||||
'barcode' => '',
|
'barcode' => '',
|
||||||
'qty' => 1,
|
'qty' => 1,
|
||||||
'unit_price' => null,
|
'unit_price' => null,
|
||||||
'currency' => 'EUR',
|
'currency' => 'EUR',
|
||||||
'source_type' => 'manual',
|
'source_type' => 'manual',
|
||||||
'source_label' => 'Inserimento manuale',
|
'source_label' => 'Inserimento manuale',
|
||||||
'external_url' => $this->buildAmazonSearchUrl($term),
|
'external_url' => $this->buildAmazonSearchUrl($term),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->materialSearch = '';
|
$this->materialSearch = '';
|
||||||
|
|
@ -403,35 +419,35 @@ public function removeMateriale(int $index): void
|
||||||
public function saveRapporto(): void
|
public function saveRapporto(): void
|
||||||
{
|
{
|
||||||
$validated = $this->validate([
|
$validated = $this->validate([
|
||||||
'rapportoFornitore' => ['required', 'string', 'max:5000'],
|
'rapportoFornitore' => ['required', 'string', 'max:5000'],
|
||||||
'tempoMinuti' => ['nullable', 'integer', 'min:1', 'max:1440'],
|
'tempoMinuti' => ['nullable', 'integer', 'min:1', 'max:1440'],
|
||||||
'articoliUtilizzati' => ['nullable', 'array'],
|
'articoliUtilizzati' => ['nullable', 'array'],
|
||||||
'articoliUtilizzati.*' => ['nullable', 'string', 'max:120'],
|
'articoliUtilizzati.*' => ['nullable', 'string', 'max:120'],
|
||||||
'materialiUtilizzati' => ['nullable', 'array'],
|
'materialiUtilizzati' => ['nullable', 'array'],
|
||||||
'materialiUtilizzati.*.product_id' => ['nullable', 'integer'],
|
'materialiUtilizzati.*.product_id' => ['nullable', 'integer'],
|
||||||
'materialiUtilizzati.*.descrizione' => ['required', 'string', 'max:255'],
|
'materialiUtilizzati.*.descrizione' => ['required', 'string', 'max:255'],
|
||||||
'materialiUtilizzati.*.barcode' => ['nullable', 'string', 'max:120'],
|
'materialiUtilizzati.*.barcode' => ['nullable', 'string', 'max:120'],
|
||||||
'materialiUtilizzati.*.qty' => ['nullable', 'numeric', 'min:0.01', 'max:9999'],
|
'materialiUtilizzati.*.qty' => ['nullable', 'numeric', 'min:0.01', 'max:9999'],
|
||||||
'materialiUtilizzati.*.unit_price' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
|
'materialiUtilizzati.*.unit_price' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
|
||||||
'materialiUtilizzati.*.currency' => ['nullable', 'string', 'max:8'],
|
'materialiUtilizzati.*.currency' => ['nullable', 'string', 'max:8'],
|
||||||
'materialiUtilizzati.*.source_type' => ['nullable', 'string', 'max:32'],
|
'materialiUtilizzati.*.source_type' => ['nullable', 'string', 'max:32'],
|
||||||
'materialiUtilizzati.*.source_label' => ['nullable', 'string', 'max:120'],
|
'materialiUtilizzati.*.source_label' => ['nullable', 'string', 'max:120'],
|
||||||
'materialiUtilizzati.*.external_url' => ['nullable', 'string', 'max:1000'],
|
'materialiUtilizzati.*.external_url' => ['nullable', 'string', 'max:1000'],
|
||||||
'fotoLavoro' => ['nullable', 'array', 'max:10'],
|
'fotoLavoro' => ['nullable', 'array', 'max:10'],
|
||||||
'fotoLavoro.*' => ['nullable', 'image', 'max:10240'],
|
'fotoLavoro.*' => ['nullable', 'image', 'max:10240'],
|
||||||
'documentiLavoro' => ['nullable', 'array', 'max:10'],
|
'documentiLavoro' => ['nullable', 'array', 'max:10'],
|
||||||
'documentiLavoro.*' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,xls,xlsx,txt,jpg,jpeg,png,webp'],
|
'documentiLavoro.*' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,xls,xlsx,txt,jpg,jpeg,png,webp'],
|
||||||
'descrizioneFile' => ['nullable', 'array'],
|
'descrizioneFile' => ['nullable', 'array'],
|
||||||
'descrizioneFile.*' => ['nullable', 'string', 'max:255'],
|
'descrizioneFile.*' => ['nullable', 'string', 'max:255'],
|
||||||
'apparatoMarca' => ['nullable', 'string', 'max:120'],
|
'apparatoMarca' => ['nullable', 'string', 'max:120'],
|
||||||
'apparatoModello' => ['nullable', 'string', 'max:120'],
|
'apparatoModello' => ['nullable', 'string', 'max:120'],
|
||||||
'apparatoSeriale' => ['nullable', 'string', 'max:120'],
|
'apparatoSeriale' => ['nullable', 'string', 'max:120'],
|
||||||
'messaggioVeloce' => ['nullable', 'string', 'max:1500'],
|
'messaggioVeloce' => ['nullable', 'string', 'max:1500'],
|
||||||
'lavoroStandard' => ['nullable', 'boolean'],
|
'lavoroStandard' => ['nullable', 'boolean'],
|
||||||
'richiestaProforma' => ['nullable', 'boolean'],
|
'richiestaProforma' => ['nullable', 'boolean'],
|
||||||
'proformaCodice' => ['nullable', 'string', 'max:80'],
|
'proformaCodice' => ['nullable', 'string', 'max:80'],
|
||||||
'proformaNote' => ['nullable', 'string', 'max:2000'],
|
'proformaNote' => ['nullable', 'string', 'max:2000'],
|
||||||
'qrToken' => ['nullable', 'string', 'max:40'],
|
'qrToken' => ['nullable', 'string', 'max:40'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$rapporto = trim((string) $validated['rapportoFornitore']);
|
$rapporto = trim((string) $validated['rapportoFornitore']);
|
||||||
|
|
@ -445,7 +461,7 @@ public function saveRapporto(): void
|
||||||
$rapporto .= "\n\n" . $apparato;
|
$rapporto .= "\n\n" . $apparato;
|
||||||
}
|
}
|
||||||
|
|
||||||
$computedTempoMinuti = $validated['tempoMinuti'] ?? null;
|
$computedTempoMinuti = $validated['tempoMinuti'] ?? null;
|
||||||
$computedTerminatedAt = $this->intervento->terminato_at ?? now();
|
$computedTerminatedAt = $this->intervento->terminato_at ?? now();
|
||||||
|
|
||||||
if (! $computedTempoMinuti && $this->intervento->iniziato_at) {
|
if (! $computedTempoMinuti && $this->intervento->iniziato_at) {
|
||||||
|
|
@ -504,7 +520,7 @@ public function saveRapporto(): void
|
||||||
$payload['articoli_utilizzati'] = $articoli;
|
$payload['articoli_utilizzati'] = $articoli;
|
||||||
}
|
}
|
||||||
|
|
||||||
$allFiles = array_merge($this->fotoLavoro, $this->documentiLavoro);
|
$allFiles = array_merge($this->fotoLavoro, $this->documentiLavoro);
|
||||||
$firstPhotoPath = null;
|
$firstPhotoPath = null;
|
||||||
|
|
||||||
if ($allFiles !== [] && Schema::hasTable('ticket_attachments')) {
|
if ($allFiles !== [] && Schema::hasTable('ticket_attachments')) {
|
||||||
|
|
@ -586,15 +602,15 @@ public function saveRapporto(): void
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->fotoLavoro = [];
|
$this->fotoLavoro = [];
|
||||||
$this->documentiLavoro = [];
|
$this->documentiLavoro = [];
|
||||||
$this->descrizioneFile = [];
|
$this->descrizioneFile = [];
|
||||||
$this->materialiUtilizzati = [];
|
$this->materialiUtilizzati = [];
|
||||||
$this->materialSearch = '';
|
$this->materialSearch = '';
|
||||||
$this->materialSuggestions = [];
|
$this->materialSuggestions = [];
|
||||||
$this->messaggioVeloce = '';
|
$this->messaggioVeloce = '';
|
||||||
$this->proformaCodice = '';
|
$this->proformaCodice = '';
|
||||||
$this->proformaNote = '';
|
$this->proformaNote = '';
|
||||||
$this->dispatch('fornitore-local-previews-clear');
|
$this->dispatch('fornitore-local-previews-clear');
|
||||||
|
|
||||||
$this->reloadIntervento();
|
$this->reloadIntervento();
|
||||||
|
|
@ -693,11 +709,11 @@ public function getRapportinoUploadsProperty(): array
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
|
$name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
|
||||||
$size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0);
|
$size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0);
|
||||||
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
|
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
|
||||||
$isImage = str_starts_with($mime, 'image/');
|
$isImage = str_starts_with($mime, 'image/');
|
||||||
$isPdf = $mime === 'application/pdf' || str_ends_with(strtolower($name), '.pdf');
|
$isPdf = $mime === 'application/pdf' || str_ends_with(strtolower($name), '.pdf');
|
||||||
$previewUrl = null;
|
$previewUrl = null;
|
||||||
|
|
||||||
if ($isImage && method_exists($file, 'temporaryUrl')) {
|
if ($isImage && method_exists($file, 'temporaryUrl')) {
|
||||||
|
|
@ -763,15 +779,17 @@ protected function buildStoredUploadDisplayName(int $index, bool $isImage, strin
|
||||||
{
|
{
|
||||||
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
|
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
|
||||||
$baseName = 'ticket-' . str_pad((string) $this->intervento->ticket_id, 6, '0', STR_PAD_LEFT)
|
$baseName = 'ticket-' . str_pad((string) $this->intervento->ticket_id, 6, '0', STR_PAD_LEFT)
|
||||||
. '-int-' . str_pad((string) $this->intervento->id, 6, '0', STR_PAD_LEFT)
|
. '-int-' . str_pad((string) $this->intervento->id, 6, '0', STR_PAD_LEFT)
|
||||||
. '-' . ($isImage ? 'foto-' : 'doc-') . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT);
|
. '-' . ($isImage ? 'foto-' : 'doc-') . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
return $baseName . ($extension !== '' ? '.' . $extension : '');
|
return $baseName . ($extension !== '' ? '.' . $extension : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function reloadIntervento(): void
|
protected function reloadIntervento(): void
|
||||||
{
|
{
|
||||||
$this->intervento->load([
|
$this->sessionTrackingAvailable = Schema::hasTable('ticket_intervento_sessioni');
|
||||||
|
|
||||||
|
$relations = [
|
||||||
'ticket.stabile',
|
'ticket.stabile',
|
||||||
'ticket.unitaImmobiliare',
|
'ticket.unitaImmobiliare',
|
||||||
'ticket.soggettoRichiedente',
|
'ticket.soggettoRichiedente',
|
||||||
|
|
@ -781,22 +799,16 @@ protected function reloadIntervento(): void
|
||||||
'ticket.assegnatoAUser',
|
'ticket.assegnatoAUser',
|
||||||
'ticket.assegnatoAFornitore',
|
'ticket.assegnatoAFornitore',
|
||||||
'eseguitoDaDipendente',
|
'eseguitoDaDipendente',
|
||||||
'sessioni',
|
];
|
||||||
]);
|
|
||||||
|
if ($this->sessionTrackingAvailable) {
|
||||||
|
$relations[] = 'sessioni';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->intervento->load($relations);
|
||||||
|
|
||||||
$this->intervento->refresh();
|
$this->intervento->refresh();
|
||||||
$this->intervento->load([
|
$this->intervento->load($relations);
|
||||||
'ticket.stabile',
|
|
||||||
'ticket.unitaImmobiliare',
|
|
||||||
'ticket.soggettoRichiedente',
|
|
||||||
'ticket.messages.user',
|
|
||||||
'ticket.attachments.user',
|
|
||||||
'ticket.apertoDaUser',
|
|
||||||
'ticket.assegnatoAUser',
|
|
||||||
'ticket.assegnatoAFornitore',
|
|
||||||
'eseguitoDaDipendente',
|
|
||||||
'sessioni',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->caller = $this->resolveCallerData($this->intervento->ticket);
|
$this->caller = $this->resolveCallerData($this->intervento->ticket);
|
||||||
$this->storicoRows = TicketIntervento::query()
|
$this->storicoRows = TicketIntervento::query()
|
||||||
|
|
@ -832,12 +844,12 @@ protected function reloadIntervento(): void
|
||||||
])
|
])
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$this->dipendenteId = (int) ($this->intervento->eseguito_da_dipendente_id ?? 0) ?: null;
|
$this->dipendenteId = (int) ($this->intervento->eseguito_da_dipendente_id ?? 0) ?: null;
|
||||||
$this->rapportoFornitore = (string) ($this->intervento->rapporto_fornitore ?? '');
|
$this->rapportoFornitore = (string) ($this->intervento->rapporto_fornitore ?? '');
|
||||||
$this->tempoMinuti = $this->calculateTrackedMinutes();
|
$this->tempoMinuti = $this->calculateTrackedMinutes();
|
||||||
$this->articoliUtilizzati = array_pad((array) ($this->intervento->articoli_utilizzati ?? []), 3, '');
|
$this->articoliUtilizzati = array_pad((array) ($this->intervento->articoli_utilizzati ?? []), 3, '');
|
||||||
$this->materialiUtilizzati = collect((array) ($this->intervento->materiali_utilizzati ?? []))
|
$this->materialiUtilizzati = collect((array) ($this->intervento->materiali_utilizzati ?? []))
|
||||||
->map(fn(array $item): array => [
|
->map(fn(array $item): array=> [
|
||||||
'product_id' => isset($item['product_id']) ? (int) $item['product_id'] : null,
|
'product_id' => isset($item['product_id']) ? (int) $item['product_id'] : null,
|
||||||
'descrizione' => (string) ($item['descrizione'] ?? ''),
|
'descrizione' => (string) ($item['descrizione'] ?? ''),
|
||||||
'barcode' => (string) ($item['barcode'] ?? ''),
|
'barcode' => (string) ($item['barcode'] ?? ''),
|
||||||
|
|
@ -850,39 +862,45 @@ protected function reloadIntervento(): void
|
||||||
])
|
])
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
$this->sessionRows = $this->intervento->sessioni
|
$sessionCollection = $this->sessionTrackingAvailable ? $this->intervento->sessioni : collect();
|
||||||
|
|
||||||
|
$this->sessionRows = $sessionCollection
|
||||||
->sortByDesc(fn(TicketInterventoSessione $sessione) => $sessione->started_at?->getTimestamp() ?? 0)
|
->sortByDesc(fn(TicketInterventoSessione $sessione) => $sessione->started_at?->getTimestamp() ?? 0)
|
||||||
->values()
|
->values()
|
||||||
->map(function (TicketInterventoSessione $sessione): array {
|
->map(function (TicketInterventoSessione $sessione): array {
|
||||||
return [
|
return [
|
||||||
'id' => (int) $sessione->id,
|
'id' => (int) $sessione->id,
|
||||||
'type' => (string) $sessione->session_type,
|
'type' => (string) $sessione->session_type,
|
||||||
'note' => (string) ($sessione->note ?? ''),
|
'note' => (string) ($sessione->note ?? ''),
|
||||||
'started_at' => optional($sessione->started_at)->format('d/m/Y H:i:s') ?: '-',
|
'started_at' => optional($sessione->started_at)->format('d/m/Y H:i:s') ?: '-',
|
||||||
'ended_at' => optional($sessione->ended_at)->format('d/m/Y H:i:s') ?: 'Aperta',
|
'ended_at' => optional($sessione->ended_at)->format('d/m/Y H:i:s') ?: 'Aperta',
|
||||||
'duration_minutes' => $sessione->duration_minutes,
|
'duration_minutes' => $sessione->duration_minutes,
|
||||||
'start_geo' => $this->formatGeoPoint($sessione->start_latitude, $sessione->start_longitude, $sessione->start_accuracy_meters),
|
'start_geo' => $this->formatGeoPoint($sessione->start_latitude, $sessione->start_longitude, $sessione->start_accuracy_meters),
|
||||||
'end_geo' => $this->formatGeoPoint($sessione->end_latitude, $sessione->end_longitude, $sessione->end_accuracy_meters),
|
'end_geo' => $this->formatGeoPoint($sessione->end_latitude, $sessione->end_longitude, $sessione->end_accuracy_meters),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->all();
|
->all();
|
||||||
$latestSession = $this->intervento->sessioni
|
$latestSession = $sessionCollection
|
||||||
->sortByDesc(fn(TicketInterventoSessione $sessione) => $sessione->updated_at?->getTimestamp() ?? 0)
|
->sortByDesc(fn(TicketInterventoSessione $sessione) => $sessione->updated_at?->getTimestamp() ?? 0)
|
||||||
->first();
|
->first();
|
||||||
$this->lastGeoSnapshot = $latestSession instanceof TicketInterventoSessione
|
$this->lastGeoSnapshot = $latestSession instanceof TicketInterventoSessione
|
||||||
? [
|
? [
|
||||||
'label' => (string) ($latestSession->session_type === 'checkpoint' ? 'Ultimo checkpoint' : 'Ultimo evento sessione'),
|
'label' => (string) ($latestSession->session_type === 'checkpoint' ? 'Ultimo checkpoint' : 'Ultimo evento sessione'),
|
||||||
'start_geo' => $this->formatGeoPoint($latestSession->start_latitude, $latestSession->start_longitude, $latestSession->start_accuracy_meters),
|
'start_geo' => $this->formatGeoPoint($latestSession->start_latitude, $latestSession->start_longitude, $latestSession->start_accuracy_meters),
|
||||||
'end_geo' => $this->formatGeoPoint($latestSession->end_latitude, $latestSession->end_longitude, $latestSession->end_accuracy_meters),
|
'end_geo' => $this->formatGeoPoint($latestSession->end_latitude, $latestSession->end_longitude, $latestSession->end_accuracy_meters),
|
||||||
'updated_at' => optional($latestSession->updated_at)->format('d/m/Y H:i:s') ?: '-',
|
'updated_at' => optional($latestSession->updated_at)->format('d/m/Y H:i:s') ?: '-',
|
||||||
]
|
]
|
||||||
: null;
|
: null;
|
||||||
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
||||||
$this->qrToken = '';
|
$this->qrToken = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getActiveSession(): ?TicketInterventoSessione
|
protected function getActiveSession(): ?TicketInterventoSessione
|
||||||
{
|
{
|
||||||
|
if (! $this->sessionTrackingAvailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return TicketInterventoSessione::query()
|
return TicketInterventoSessione::query()
|
||||||
->where('ticket_intervento_id', (int) $this->intervento->id)
|
->where('ticket_intervento_id', (int) $this->intervento->id)
|
||||||
->where('session_type', 'work')
|
->where('session_type', 'work')
|
||||||
|
|
@ -893,6 +911,10 @@ protected function getActiveSession(): ?TicketInterventoSessione
|
||||||
|
|
||||||
protected function calculateTrackedMinutes(): ?int
|
protected function calculateTrackedMinutes(): ?int
|
||||||
{
|
{
|
||||||
|
if (! $this->sessionTrackingAvailable) {
|
||||||
|
return $this->intervento->tempo_minuti ? (int) $this->intervento->tempo_minuti : null;
|
||||||
|
}
|
||||||
|
|
||||||
$total = (int) TicketInterventoSessione::query()
|
$total = (int) TicketInterventoSessione::query()
|
||||||
->where('ticket_intervento_id', (int) $this->intervento->id)
|
->where('ticket_intervento_id', (int) $this->intervento->id)
|
||||||
->sum('duration_minutes');
|
->sum('duration_minutes');
|
||||||
|
|
@ -910,23 +932,23 @@ protected function calculateTrackedMinutes(): ?int
|
||||||
*/
|
*/
|
||||||
protected function normalizeGeoPayload(array $geo): array
|
protected function normalizeGeoPayload(array $geo): array
|
||||||
{
|
{
|
||||||
$lat = isset($geo['lat']) && is_numeric($geo['lat']) ? round((float) $geo['lat'], 7) : null;
|
$lat = isset($geo['lat']) && is_numeric($geo['lat']) ? round((float) $geo['lat'], 7) : null;
|
||||||
$lng = isset($geo['lng']) && is_numeric($geo['lng']) ? round((float) $geo['lng'], 7) : null;
|
$lng = isset($geo['lng']) && is_numeric($geo['lng']) ? round((float) $geo['lng'], 7) : null;
|
||||||
$accuracy = isset($geo['accuracy']) && is_numeric($geo['accuracy']) ? max(0, (int) round((float) $geo['accuracy'])) : null;
|
$accuracy = isset($geo['accuracy']) && is_numeric($geo['accuracy']) ? max(0, (int) round((float) $geo['accuracy'])) : null;
|
||||||
$source = trim((string) ($geo['source'] ?? 'browser')) ?: 'browser';
|
$source = trim((string) ($geo['source'] ?? 'browser')) ?: 'browser';
|
||||||
$error = trim((string) ($geo['error'] ?? ''));
|
$error = trim((string) ($geo['error'] ?? ''));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'lat' => $lat,
|
'lat' => $lat,
|
||||||
'lng' => $lng,
|
'lng' => $lng,
|
||||||
'accuracy' => $accuracy,
|
'accuracy' => $accuracy,
|
||||||
'source' => $source,
|
'source' => $source,
|
||||||
'payload' => array_filter([
|
'payload' => array_filter([
|
||||||
'lat' => $lat,
|
'lat' => $lat,
|
||||||
'lng' => $lng,
|
'lng' => $lng,
|
||||||
'accuracy' => $accuracy,
|
'accuracy' => $accuracy,
|
||||||
'source' => $source,
|
'source' => $source,
|
||||||
'error' => $error !== '' ? $error : null,
|
'error' => $error !== '' ? $error : null,
|
||||||
'captured_at' => now()->toDateTimeString(),
|
'captured_at' => now()->toDateTimeString(),
|
||||||
], fn($value) => $value !== null && $value !== ''),
|
], fn($value) => $value !== null && $value !== ''),
|
||||||
];
|
];
|
||||||
|
|
@ -996,18 +1018,16 @@ protected function refreshMaterialSuggestions(): void
|
||||||
|
|
||||||
$this->materialSuggestions = $products->map(function (Product $product): array {
|
$this->materialSuggestions = $products->map(function (Product $product): array {
|
||||||
$identifier = $product->identifiers
|
$identifier = $product->identifiers
|
||||||
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id)
|
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id) ?? $product->identifiers->first();
|
||||||
?? $product->identifiers->first();
|
|
||||||
$offer = $product->offers
|
$offer = $product->offers
|
||||||
->first(fn(ProductOffer $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id)
|
->first(fn(ProductOffer $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id) ?? $product->offers->first();
|
||||||
?? $product->offers->first();
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $product->id,
|
'id' => (int) $product->id,
|
||||||
'label' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))),
|
'label' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))),
|
||||||
'barcode' => (string) ($identifier->code_value ?? ''),
|
'barcode' => (string) ($identifier->code_value ?? ''),
|
||||||
'price' => $offer?->price_amount !== null ? number_format((float) $offer->price_amount, 2, ',', '.') . ' ' . (string) ($offer->currency ?? 'EUR') : null,
|
'price' => $offer?->price_amount !== null ? number_format((float) $offer->price_amount, 2, ',', '.') . ' ' . (string) ($offer->currency ?? 'EUR') : null,
|
||||||
'source' => (string) ($offer?->source_name ?: 'Catalogo'),
|
'source' => (string) ($offer?->source_name ?: 'Catalogo'),
|
||||||
'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)),
|
'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)),
|
||||||
];
|
];
|
||||||
})->all();
|
})->all();
|
||||||
|
|
@ -1037,9 +1057,9 @@ protected function resolveGoogleWorkspaceStatus(): ?array
|
||||||
$oauth = (array) (($amministratore->impostazioni['google']['oauth'] ?? null) ?: []);
|
$oauth = (array) (($amministratore->impostazioni['google']['oauth'] ?? null) ?: []);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'connected' => (bool) ($oauth['connected'] ?? false),
|
'connected' => (bool) ($oauth['connected'] ?? false),
|
||||||
'email' => (string) ($oauth['email'] ?? ''),
|
'email' => (string) ($oauth['email'] ?? ''),
|
||||||
'name' => (string) ($oauth['name'] ?? ''),
|
'name' => (string) ($oauth['name'] ?? ''),
|
||||||
'connected_at' => (string) ($oauth['connected_at'] ?? ''),
|
'connected_at' => (string) ($oauth['connected_at'] ?? ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -645,7 +645,7 @@ public function updatedPendingTicketAttachments(): void
|
||||||
|
|
||||||
public function removeNewTicketFile(int $index): void
|
public function removeNewTicketFile(int $index): void
|
||||||
{
|
{
|
||||||
$file = null;
|
$file = null;
|
||||||
$cameraCount = count($this->newTicketCameraShots);
|
$cameraCount = count($this->newTicketCameraShots);
|
||||||
|
|
||||||
if ($index < $cameraCount) {
|
if ($index < $cameraCount) {
|
||||||
|
|
@ -654,7 +654,7 @@ public function removeNewTicketFile(int $index): void
|
||||||
$this->newTicketCameraShots = array_values($this->newTicketCameraShots);
|
$this->newTicketCameraShots = array_values($this->newTicketCameraShots);
|
||||||
} else {
|
} else {
|
||||||
$docIndex = $index - $cameraCount;
|
$docIndex = $index - $cameraCount;
|
||||||
$file = $this->newTicketAttachments[$docIndex] ?? null;
|
$file = $this->newTicketAttachments[$docIndex] ?? null;
|
||||||
unset($this->newTicketAttachments[$docIndex]);
|
unset($this->newTicketAttachments[$docIndex]);
|
||||||
$this->newTicketAttachments = array_values($this->newTicketAttachments);
|
$this->newTicketAttachments = array_values($this->newTicketAttachments);
|
||||||
}
|
}
|
||||||
|
|
@ -795,13 +795,15 @@ private function resolveAttachmentDescription(int $index, array $metadata = [],
|
||||||
$parts[] = 'Allegato da Ticket Mobile';
|
$parts[] = 'Allegato da Ticket Mobile';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filled($displayName)) {
|
$lat = $metadata['gps_decimal']['lat'] ?? null;
|
||||||
$parts[] = 'File: ' . trim((string) $displayName);
|
$lng = $metadata['gps_decimal']['lng'] ?? null;
|
||||||
}
|
if (is_numeric($lat) && is_numeric($lng)) {
|
||||||
|
$parts[] = 'GPS: ' . number_format((float) $lat, 5, '.', '') . ',' . number_format((float) $lng, 5, '.', '');
|
||||||
|
|
||||||
$sourceName = trim((string) $sourceOriginalName);
|
$mapsUrl = trim((string) ($metadata['google_maps_url'] ?? ''));
|
||||||
if ($sourceName !== '' && $sourceName !== trim((string) $displayName)) {
|
if ($mapsUrl !== '') {
|
||||||
$parts[] = 'Orig: ' . $sourceName;
|
$parts[] = 'Maps: ' . $mapsUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$exifDate = trim((string) ($metadata['exif_datetime'] ?? ''));
|
$exifDate = trim((string) ($metadata['exif_datetime'] ?? ''));
|
||||||
|
|
@ -814,15 +816,13 @@ private function resolveAttachmentDescription(int $index, array $metadata = [],
|
||||||
$parts[] = 'Device: ' . $device;
|
$parts[] = 'Device: ' . $device;
|
||||||
}
|
}
|
||||||
|
|
||||||
$lat = $metadata['gps_decimal']['lat'] ?? null;
|
if (filled($displayName)) {
|
||||||
$lng = $metadata['gps_decimal']['lng'] ?? null;
|
$parts[] = 'File: ' . trim((string) $displayName);
|
||||||
if (is_numeric($lat) && is_numeric($lng)) {
|
}
|
||||||
$parts[] = 'GPS: ' . number_format((float) $lat, 5, '.', '') . ',' . number_format((float) $lng, 5, '.', '');
|
|
||||||
|
|
||||||
$mapsUrl = trim((string) ($metadata['google_maps_url'] ?? ''));
|
$sourceName = trim((string) $sourceOriginalName);
|
||||||
if ($mapsUrl !== '') {
|
if ($sourceName !== '' && $sourceName !== trim((string) $displayName)) {
|
||||||
$parts[] = 'Maps: ' . $mapsUrl;
|
$parts[] = 'Orig: ' . $sourceName;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Str::limit(implode(' | ', array_filter($parts)), 255);
|
return Str::limit(implode(' | ', array_filter($parts)), 255);
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,17 @@
|
||||||
draftSequence: {{ (int) $newTicketDraftSequence }},
|
draftSequence: {{ (int) $newTicketDraftSequence }},
|
||||||
localCameraPreviews: [],
|
localCameraPreviews: [],
|
||||||
localAttachmentPreviews: [],
|
localAttachmentPreviews: [],
|
||||||
|
init() {
|
||||||
|
window.__ticketMobilePreviewState = window.__ticketMobilePreviewState || { camera: [], attachment: [] };
|
||||||
|
this.localCameraPreviews = [...(window.__ticketMobilePreviewState.camera || [])];
|
||||||
|
this.localAttachmentPreviews = [...(window.__ticketMobilePreviewState.attachment || [])];
|
||||||
|
},
|
||||||
|
persist() {
|
||||||
|
window.__ticketMobilePreviewState = {
|
||||||
|
camera: [...this.localCameraPreviews],
|
||||||
|
attachment: [...this.localAttachmentPreviews],
|
||||||
|
};
|
||||||
|
},
|
||||||
revoke(items) {
|
revoke(items) {
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
if (item.url) {
|
if (item.url) {
|
||||||
|
|
@ -285,6 +296,7 @@
|
||||||
const target = kind === 'camera' ? 'localCameraPreviews' : 'localAttachmentPreviews';
|
const target = kind === 'camera' ? 'localCameraPreviews' : 'localAttachmentPreviews';
|
||||||
this.revoke(this[target]);
|
this.revoke(this[target]);
|
||||||
this[target] = this.mapFiles(event?.target?.files || [], kind);
|
this[target] = this.mapFiles(event?.target?.files || [], kind);
|
||||||
|
this.persist();
|
||||||
},
|
},
|
||||||
clearPreviews(kind = null) {
|
clearPreviews(kind = null) {
|
||||||
if (kind === null || kind === 'camera') {
|
if (kind === null || kind === 'camera') {
|
||||||
|
|
@ -295,6 +307,13 @@
|
||||||
this.revoke(this.localAttachmentPreviews);
|
this.revoke(this.localAttachmentPreviews);
|
||||||
this.localAttachmentPreviews = [];
|
this.localAttachmentPreviews = [];
|
||||||
}
|
}
|
||||||
|
this.persist();
|
||||||
|
},
|
||||||
|
previewFor(draftName, originalName) {
|
||||||
|
const pool = [...this.localCameraPreviews, ...this.localAttachmentPreviews];
|
||||||
|
const match = pool.find((item) => item.draftName === draftName || item.name === originalName);
|
||||||
|
|
||||||
|
return match?.url || null;
|
||||||
},
|
},
|
||||||
formatSize(bytes) {
|
formatSize(bytes) {
|
||||||
if (!bytes) {
|
if (!bytes) {
|
||||||
|
|
@ -392,12 +411,25 @@
|
||||||
@if(count($this->selectedUploads) > 0)
|
@if(count($this->selectedUploads) > 0)
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<div class="mb-2 text-xs font-semibold text-gray-700">Coda allegati pronta per il ticket</div>
|
<div class="mb-2 text-xs font-semibold text-gray-700">Coda allegati pronta per il ticket</div>
|
||||||
|
<div class="mb-2 text-[11px] text-gray-500">Le immagini locali restano visibili fino all'invio del ticket. La rinomina definitiva viene applicata solo al momento del salvataggio.</div>
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
@foreach($this->selectedUploads as $upload)
|
@foreach($this->selectedUploads as $upload)
|
||||||
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
|
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
|
||||||
<div class="aspect-[4/3] rounded-lg border bg-gray-50 p-2">
|
<div class="aspect-[4/3] rounded-lg border bg-gray-50 p-2">
|
||||||
@if($upload['is_image'] && !empty($upload['preview_url']))
|
@if($upload['is_image'] && !empty($upload['preview_url']))
|
||||||
<img src="{{ $upload['preview_url'] }}" alt="{{ $upload['name'] }}" class="h-full w-full rounded object-cover" />
|
<img src="{{ $upload['preview_url'] }}" alt="{{ $upload['name'] }}" class="h-full w-full rounded object-cover" />
|
||||||
|
@elseif($upload['is_image'])
|
||||||
|
<template x-if="previewFor(@js($upload['name']), @js($upload['original_name']))">
|
||||||
|
<img :src="previewFor(@js($upload['name']), @js($upload['original_name']))" alt="{{ $upload['name'] }}" class="h-full w-full rounded object-cover" />
|
||||||
|
</template>
|
||||||
|
<template x-if="!previewFor(@js($upload['name']), @js($upload['original_name']))">
|
||||||
|
<div class="flex h-full items-center justify-center text-center text-gray-500">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold">IMG</div>
|
||||||
|
<div class="mt-1 text-[11px]">Anteprima locale in attesa</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@else
|
@else
|
||||||
<div class="flex h-full items-center justify-center text-center text-gray-500">
|
<div class="flex h-full items-center justify-center text-center text-gray-500">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user