Import UI, mobile workflows and ticket context fixes
This commit is contained in:
parent
432f0565ab
commit
478c0a120b
|
|
@ -678,6 +678,43 @@ public function aggiornaAnagraficheDaCondomin(): void
|
|||
->send();
|
||||
}
|
||||
|
||||
public function risincronizzaRelazioniImportate(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$state = $this->getSchema('form')?->getState() ?? [];
|
||||
$code = trim((string) ($state['stabile_code'] ?? ''));
|
||||
if ($code === '') {
|
||||
Notification::make()->title('Seleziona uno stabile')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$stabile = $this->resolveSelectedStabileByCode($code);
|
||||
if (! $stabile instanceof Stabile) {
|
||||
Notification::make()->title('Stabile locale non trovato')->body('Prima crea o apri lo stabile selezionato.')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) ($state['dry_run'] ?? false));
|
||||
if (! $result['ok']) {
|
||||
Notification::make()
|
||||
->title('Risincronizzazione relazioni fallita')
|
||||
->body((string) ($result['message'] ?? 'Errore non specificato.'))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title((bool) ($state['dry_run'] ?? false) ? 'Dry-run relazioni completato' : 'Risincronizzazione relazioni completata')
|
||||
->body((string) ($result['message'] ?? 'Operazione completata'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function aggiornaFornitoriGescon(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
|
@ -1134,6 +1171,28 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
|
|||
$outputs[] = trim((string) Artisan::output());
|
||||
}
|
||||
|
||||
$shouldSyncRelations = ! empty($options['with_unita'])
|
||||
|| ! empty($options['with_soggetti'])
|
||||
|| ! empty($options['with_diritti']);
|
||||
|
||||
if ($shouldSyncRelations) {
|
||||
$relationSync = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) $options['dry']);
|
||||
if (! $relationSync['ok']) {
|
||||
Notification::make()
|
||||
->title('Import completato con errore nel re-sync relazioni')
|
||||
->body((string) ($relationSync['message'] ?? 'Errore non specificato.'))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ((array) ($relationSync['outputs'] ?? []) as $syncOutput) {
|
||||
if (is_string($syncOutput) && trim($syncOutput) !== '') {
|
||||
$outputs[] = trim($syncOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tail = collect($outputs)
|
||||
->filter()
|
||||
->map(fn(string $o) => trim((string) collect(preg_split('/\R/', $o) ?: [])->last()))
|
||||
|
|
@ -1250,6 +1309,100 @@ public function caricaAnniDaGenerale(): void
|
|||
->send();
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function getCommandPreviewData(): array
|
||||
{
|
||||
$state = is_array($this->data) ? $this->data : [];
|
||||
|
||||
$code = trim((string) ($state['stabile_code'] ?? ''));
|
||||
if ($code !== '') {
|
||||
$code = str_pad($code, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$path = trim((string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'));
|
||||
$annoDir = trim((string) ($state['anno'] ?? ''));
|
||||
$years = implode(',', array_values(array_filter(array_map(
|
||||
fn($value) => trim((string) $value),
|
||||
(array) ($state['align_years_selected'] ?? [])
|
||||
))));
|
||||
$waterYears = implode(',', array_values(array_filter(array_map(
|
||||
fn($value) => trim((string) $value),
|
||||
(array) ($state['align_water_years_selected'] ?? [])
|
||||
))));
|
||||
$openYears = implode(',', array_values(array_filter(array_map(
|
||||
fn($value) => trim((string) $value),
|
||||
(array) ($state['align_open_years_selected'] ?? [])
|
||||
))));
|
||||
|
||||
$adminId = (int) ($state['amministratore_id'] ?? 0);
|
||||
$stabile = $this->resolveSelectedStabileByCode($code);
|
||||
$stabileId = $stabile instanceof Stabile ? (int) $stabile->id : null;
|
||||
|
||||
$fornitoriMdb = trim((string) ($state['fornitori_mdb'] ?? ''));
|
||||
if ($fornitoriMdb === '' && $path !== '') {
|
||||
$fornitoriMdb = rtrim($path, '/') . '/dbc/Fornitori.mdb';
|
||||
}
|
||||
|
||||
$stabiliMdb = trim((string) ($state['stabili_mdb'] ?? ''));
|
||||
if ($stabiliMdb === '' && $path !== '') {
|
||||
$stabiliMdb = rtrim($path, '/') . '/dbc/Stabili.mdb';
|
||||
}
|
||||
|
||||
$generaleMdb = ($path !== '' && $code !== '')
|
||||
? rtrim($path, '/') . '/' . $code . '/generale_stabile.mdb'
|
||||
: '';
|
||||
$singoloMdb = ($path !== '' && $code !== '' && $annoDir !== '')
|
||||
? rtrim($path, '/') . '/' . $code . '/' . $annoDir . '/singolo_anno.mdb'
|
||||
: '';
|
||||
|
||||
return [
|
||||
'source_mode' => (string) ($state['source_mode'] ?? 'linux_server'),
|
||||
'stabile_code' => $code,
|
||||
'stabile_id' => $stabileId,
|
||||
'amministratore_id' => $adminId > 0 ? $adminId : null,
|
||||
'anno_dir' => $annoDir,
|
||||
'years_csv' => $years,
|
||||
'water_years_csv' => $waterYears,
|
||||
'open_years_csv' => $openYears,
|
||||
'paths' => [
|
||||
'root' => $path,
|
||||
'stabili_mdb' => $stabiliMdb,
|
||||
'fornitori_mdb' => $fornitoriMdb,
|
||||
'generale_mdb' => $generaleMdb,
|
||||
'singolo_mdb' => $singoloMdb,
|
||||
],
|
||||
'commands' => [
|
||||
'load_stabili' => $stabiliMdb !== ''
|
||||
? 'php artisan import:gescon-fornitori ' . max(1, $adminId) . ' --mdb="' . $fornitoriMdb . '" --preview --limit=5'
|
||||
: null,
|
||||
'fornitori_sync' => ($adminId > 0 && $fornitoriMdb !== '')
|
||||
? 'php artisan gescon:sync-fornitori-legacy ' . $adminId . ' --mdb="' . $fornitoriMdb . '"'
|
||||
: null,
|
||||
'allineamento' => ($code !== '' && $path !== '' && $years !== '')
|
||||
? 'php artisan gescon:import-align --stabile=' . $code . ' --path="' . $path . '" --years=' . $years . ' --water-years=' . ($waterYears !== '' ? $waterYears : $years) . ' --open-years=' . ($openYears !== '' ? $openYears : $years)
|
||||
: null,
|
||||
'unita' => ($code !== '' && $path !== '' && $annoDir !== '')
|
||||
? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=unita --anno=' . $annoDir . ' --path="' . $path . '"'
|
||||
: null,
|
||||
'soggetti' => ($code !== '' && $path !== '' && $annoDir !== '')
|
||||
? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=soggetti --anno=' . $annoDir . ' --path="' . $path . '"'
|
||||
: null,
|
||||
'diritti' => ($code !== '' && $path !== '' && $annoDir !== '')
|
||||
? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=diritti --anno=' . $annoDir . ' --path="' . $path . '"'
|
||||
: null,
|
||||
'anagrafiche_sync' => ($code !== '')
|
||||
? 'php artisan gescon:auto-sync-anagrafiche --stabile=' . $code . ' --only=both --link-ruolo --include-comproprietari --apply'
|
||||
: null,
|
||||
'ruoli_sync' => ($stabileId !== null)
|
||||
? 'php artisan gescon:sync-rubrica-ruoli --stabile=' . $stabileId . ' --apply'
|
||||
: null,
|
||||
'repair_contacts' => ($code !== '')
|
||||
? 'php artisan gescon:repair-nominativi-contacts --stabile=' . $code . ' --apply'
|
||||
: null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function refreshAnniOptionsForStabile(string $code, string $root, ?Set $set = null, mixed $get = null): int
|
||||
{
|
||||
$code = trim($code);
|
||||
|
|
@ -1348,6 +1501,88 @@ private function refreshAnniOptionsForStabile(string $code, string $root, ?Set $
|
|||
return count($opts);
|
||||
}
|
||||
|
||||
private function resolveSelectedStabileByCode(?string $code): ?Stabile
|
||||
{
|
||||
$normalized = trim((string) $code);
|
||||
if ($normalized === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$trimmed = ltrim($normalized, '0');
|
||||
|
||||
return Stabile::query()
|
||||
->where(function ($query) use ($normalized, $trimmed): void {
|
||||
$query->where('codice_stabile', $normalized);
|
||||
if ($trimmed !== '' && $trimmed !== $normalized) {
|
||||
$query->orWhere('codice_stabile', $trimmed);
|
||||
}
|
||||
})
|
||||
->first();
|
||||
}
|
||||
|
||||
/** @return array{ok:bool,message:string,outputs:array<int,string>} */
|
||||
private function runRelationSyncSequence(string $code, int $stabileId, bool $dryRun): array
|
||||
{
|
||||
$outputs = [];
|
||||
$statuses = [];
|
||||
|
||||
try {
|
||||
$syncParams = [
|
||||
'--stabile' => $code,
|
||||
'--limit' => 20000,
|
||||
'--only' => 'both',
|
||||
'--link-ruolo' => true,
|
||||
'--include-comproprietari' => true,
|
||||
];
|
||||
if (! $dryRun) {
|
||||
$syncParams['--apply'] = true;
|
||||
}
|
||||
|
||||
$statuses[] = Artisan::call('gescon:auto-sync-anagrafiche', $syncParams);
|
||||
$outputs[] = trim((string) Artisan::output());
|
||||
|
||||
$ruoliParams = [
|
||||
'--stabile' => (string) $stabileId,
|
||||
'--limit' => 5000,
|
||||
];
|
||||
if (! $dryRun) {
|
||||
$ruoliParams['--apply'] = true;
|
||||
}
|
||||
|
||||
$statuses[] = Artisan::call('gescon:sync-rubrica-ruoli', $ruoliParams);
|
||||
$outputs[] = trim((string) Artisan::output());
|
||||
|
||||
$repairParams = [
|
||||
'--stabile' => $code,
|
||||
'--limit' => 5000,
|
||||
];
|
||||
if (! $dryRun) {
|
||||
$repairParams['--apply'] = true;
|
||||
}
|
||||
|
||||
$statuses[] = Artisan::call('gescon:repair-nominativi-contacts', $repairParams);
|
||||
$outputs[] = trim((string) Artisan::output());
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'outputs' => $outputs,
|
||||
];
|
||||
}
|
||||
|
||||
$tail = collect($outputs)
|
||||
->filter(fn($value) => is_string($value) && trim($value) !== '')
|
||||
->map(fn(string $output) => trim((string) collect(preg_split('/\R/', $output) ?: [])->last()))
|
||||
->filter(fn($value) => is_string($value) && $value !== '')
|
||||
->implode("\n");
|
||||
|
||||
return [
|
||||
'ok' => collect($statuses)->every(fn($status) => (int) $status === 0),
|
||||
'message' => $tail !== '' ? $tail : 'Operazione completata.',
|
||||
'outputs' => $outputs,
|
||||
];
|
||||
}
|
||||
|
||||
private function firstNonEmpty(array $row, array $keys): mixed
|
||||
{
|
||||
foreach ($keys as $k) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
|
@ -1108,6 +1109,16 @@ protected function getHeaderActions(): array
|
|||
$this->startInlineEdit();
|
||||
}),
|
||||
|
||||
Action::make('abilita_accesso_netgescon')
|
||||
->label('Abilita accesso')
|
||||
->icon('heroicon-o-key')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Abilita accesso NetGescon')
|
||||
->modalDescription('Crea o collega un utente per questo nominativo e invia il link impostazione password all\'email disponibile in rubrica.')
|
||||
->action(function (): void {
|
||||
$this->abilitaAccessoRubrica();
|
||||
}),
|
||||
|
||||
Action::make('email_multiple')
|
||||
->label('Email aggiuntive')
|
||||
->icon('heroicon-o-envelope')
|
||||
|
|
@ -1859,6 +1870,61 @@ private function getStudioCollaboratoreEmail(): ?string
|
|||
return null;
|
||||
}
|
||||
|
||||
private function getRubricaAccessEmail(): ?string
|
||||
{
|
||||
return $this->getStudioCollaboratoreEmail();
|
||||
}
|
||||
|
||||
public function abilitaAccessoRubrica(): void
|
||||
{
|
||||
$email = $this->getRubricaAccessEmail();
|
||||
if ($email === null) {
|
||||
Notification::make()
|
||||
->title('Email rubrica mancante')
|
||||
->warning()
|
||||
->body('Per abilitare l\'accesso serve almeno un indirizzo email principale o aggiuntivo attivo nella scheda rubrica.')
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$name = trim((string) ($this->rubrica->nome_completo ?: $this->rubrica->ragione_sociale ?: 'Utente NetGescon'));
|
||||
$user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first();
|
||||
|
||||
if (! $user) {
|
||||
$user = User::query()->create([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password' => Hash::make(Str::random(32)),
|
||||
'email_verified_at' => null,
|
||||
'is_active' => true,
|
||||
'registration_status' => 'approved',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore', 'condomino'])) {
|
||||
$user->assignRole('condomino');
|
||||
} elseif (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore']) && ! $user->hasRole('condomino')) {
|
||||
$user->assignRole('condomino');
|
||||
}
|
||||
|
||||
$status = Password::sendResetLink(['email' => $email]);
|
||||
|
||||
if ($status === Password::RESET_LINK_SENT) {
|
||||
Notification::make()
|
||||
->title('Accesso abilitato')
|
||||
->success()
|
||||
->body('Utente collegato alla rubrica. Link impostazione password inviato a ' . $email . '.')
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Utente creato o collegato, ma invio mail non confermato')
|
||||
->warning()
|
||||
->body('Controlla il mailer attivo. Stato Laravel: ' . __($status))
|
||||
->send();
|
||||
}
|
||||
|
||||
public function canRequestClickToCall(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
|
|
|||
33
app/Filament/Pages/Impostazioni/GoogleDashboard.php
Normal file
33
app/Filament/Pages/Impostazioni/GoogleDashboard.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
namespace App\Filament\Pages\Impostazioni;
|
||||
|
||||
use App\Models\User;
|
||||
use BackedEnum;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use UnitEnum;
|
||||
|
||||
class GoogleDashboard extends Page
|
||||
{
|
||||
protected static ?string $navigationLabel = 'Google';
|
||||
|
||||
protected static ?string $title = 'Google';
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-cloud';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Impostazioni';
|
||||
|
||||
protected static ?int $navigationSort = 35;
|
||||
|
||||
protected static ?string $slug = 'impostazioni/google';
|
||||
|
||||
protected string $view = 'filament.pages.impostazioni.google-dashboard';
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}
|
||||
}
|
||||
33
app/Filament/Pages/Mobile/MobileDashboard.php
Normal file
33
app/Filament/Pages/Mobile/MobileDashboard.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
namespace App\Filament\Pages\Mobile;
|
||||
|
||||
use App\Models\User;
|
||||
use BackedEnum;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use UnitEnum;
|
||||
|
||||
class MobileDashboard extends Page
|
||||
{
|
||||
protected static ?string $navigationLabel = 'Dashboard Mobile';
|
||||
|
||||
protected static ?string $title = 'Dashboard Mobile';
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-device-phone-mobile';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Mobile';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static ?string $slug = 'mobile/dashboard';
|
||||
|
||||
protected string $view = 'filament.pages.mobile.dashboard';
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ class TicketMobile extends Page
|
|||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-device-phone-mobile';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Supporto';
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Mobile';
|
||||
|
||||
protected static ?int $navigationSort = 25;
|
||||
|
||||
|
|
@ -1131,14 +1131,14 @@ private function selectCallerRepresentative(Collection $group, int $selectedCall
|
|||
$representative = $selected instanceof RubricaUniversale
|
||||
? $selected
|
||||
: $group
|
||||
->sortByDesc(fn(RubricaUniversale $match): array=> [
|
||||
(int) (! empty($match->amministratore_id)),
|
||||
(int) (! empty($match->riferimento_stabile)),
|
||||
(int) (! empty($match->riferimento_unita)),
|
||||
(int) (($match->categoria ?? '') === 'condomino'),
|
||||
-1 * (int) $match->id,
|
||||
])
|
||||
->first();
|
||||
->sortByDesc(fn(RubricaUniversale $match): array => [
|
||||
(int) (! empty($match->amministratore_id)),
|
||||
(int) (! empty($match->riferimento_stabile)),
|
||||
(int) (! empty($match->riferimento_unita)),
|
||||
(int) (($match->categoria ?? '') === 'condomino'),
|
||||
-1 * (int) $match->id,
|
||||
])
|
||||
->first();
|
||||
|
||||
$duplicateCategories = $group
|
||||
->pluck('categoria')
|
||||
|
|
@ -1195,20 +1195,33 @@ public function getCallerStabileLabel(int $rubricaId): ?string
|
|||
*/
|
||||
private function resolveCallerTicketContext(?RubricaUniversale $caller, ?int $fallbackStabileId = null): array
|
||||
{
|
||||
$stabile = $caller instanceof RubricaUniversale
|
||||
? $this->resolveCallerStabile($caller, $fallbackStabileId)
|
||||
: ($fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null);
|
||||
$fallbackStabile = $fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null;
|
||||
|
||||
$callerStabile = $caller instanceof RubricaUniversale
|
||||
? $this->resolveCallerStabile($caller)
|
||||
: null;
|
||||
|
||||
$soggetto = $caller instanceof RubricaUniversale
|
||||
? $this->resolveCallerSoggetto($caller, $stabile?->id)
|
||||
? $this->resolveCallerSoggetto($caller, $callerStabile?->id)
|
||||
: null;
|
||||
|
||||
$unita = $soggetto instanceof Soggetto
|
||||
? $this->resolveCallerUnita($soggetto, $stabile?->id, (string) ($caller?->riferimento_unita ?? ''))
|
||||
? $this->resolveCallerUnita($soggetto, $callerStabile?->id, (string) ($caller?->riferimento_unita ?? ''))
|
||||
: null;
|
||||
|
||||
if (! $stabile && $unita instanceof UnitaImmobiliare) {
|
||||
$stabile = $unita->stabile;
|
||||
$stabile = $unita instanceof UnitaImmobiliare
|
||||
? $unita->stabile
|
||||
: $callerStabile;
|
||||
|
||||
if (! $stabile && $soggetto instanceof Soggetto) {
|
||||
$unita = $this->resolveCallerUnita($soggetto, null, (string) ($caller?->riferimento_unita ?? ''));
|
||||
if ($unita instanceof UnitaImmobiliare) {
|
||||
$stabile = $unita->stabile;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $stabile && ! $unita && ! $soggetto && $fallbackStabile instanceof Stabile) {
|
||||
$stabile = $fallbackStabile;
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
@ -1261,6 +1274,7 @@ private function resolveCallerStabile(RubricaUniversale $caller, ?int $fallbackS
|
|||
return $fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null;
|
||||
}
|
||||
|
||||
|
||||
private function resolveCallerSoggetto(RubricaUniversale $caller, ?int $stabileId = null): ?Soggetto
|
||||
{
|
||||
$queries = [];
|
||||
|
|
|
|||
102
app/Filament/Widgets/TicketMobileOverview.php
Normal file
102
app/Filament/Widgets/TicketMobileOverview.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Filament\Pages\Supporto\TicketGestione;
|
||||
use App\Filament\Pages\Supporto\TicketMobile;
|
||||
use App\Filament\Pages\Supporto\TicketMobileHelp;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
use App\Support\StabileContext;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TicketMobileOverview extends Widget
|
||||
{
|
||||
protected string $view = 'filament.widgets.ticket-mobile-overview';
|
||||
|
||||
protected int|string|array $columnSpan = 'full';
|
||||
|
||||
/** @var array<string,int> */
|
||||
public array $ticketCounters = [
|
||||
'open' => 0,
|
||||
'urgent' => 0,
|
||||
'closed' => 0,
|
||||
'all' => 0,
|
||||
];
|
||||
|
||||
public static function canView(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stabileIds = StabileContext::accessibleStabili($user)
|
||||
->pluck('id')
|
||||
->map(fn($value) => (int) $value)
|
||||
->filter(fn(int $value) => $value > 0)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($stabileIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$base = Ticket::query()->whereIn('stabile_id', $stabileIds);
|
||||
|
||||
$this->ticketCounters = [
|
||||
'open' => (clone $base)->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
|
||||
'urgent' => (clone $base)->where('priorita', 'Urgente')->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
|
||||
'closed' => (clone $base)->whereIn('stato', ['Risolto', 'Chiuso'])->count(),
|
||||
'all' => (clone $base)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getDashboardFilamentUrl(): string
|
||||
{
|
||||
return route('filament.admin-filament.pages.dashboard');
|
||||
}
|
||||
|
||||
public function getRubricaFilamentUrl(): string
|
||||
{
|
||||
return route('filament.admin-filament.pages.gescon.anagrafica.rubrica-universale');
|
||||
}
|
||||
|
||||
public function getAdminMobileHubUrl(): string
|
||||
{
|
||||
return route('admin.mobile');
|
||||
}
|
||||
|
||||
public function getCondominoMobileTicketUrl(): string
|
||||
{
|
||||
return route('condomino.tickets.mobile');
|
||||
}
|
||||
|
||||
public function getTicketArchivioUrl(): string
|
||||
{
|
||||
return TicketGestione::getUrl(panel: 'admin-filament', parameters: ['status' => 'all']);
|
||||
}
|
||||
|
||||
public function getTicketMobileUrl(): string
|
||||
{
|
||||
return TicketMobile::getUrl(panel: 'admin-filament');
|
||||
}
|
||||
|
||||
public function getGestioneTicketUrl(): string
|
||||
{
|
||||
return TicketGestione::getUrl(panel: 'admin-filament');
|
||||
}
|
||||
|
||||
public function getHelpUrl(): string
|
||||
{
|
||||
return TicketMobileHelp::getUrl(panel: 'admin-filament');
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
use App\Filament\Pages\Supporto\SupportoDashboard;
|
||||
use App\Filament\Pages\Supporto\TicketMobile;
|
||||
use App\Filament\Widgets\GoogleScadenziarioOverview;
|
||||
use App\Filament\Widgets\TicketMobileOverview;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\FornitoreDipendente;
|
||||
use App\Models\User;
|
||||
|
|
@ -116,6 +117,7 @@ public function panel(Panel $panel): Panel
|
|||
->widgets([
|
||||
AccountWidget::class,
|
||||
GoogleScadenziarioOverview::class,
|
||||
TicketMobileOverview::class,
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ public function store(object $file, string $directory, array $options = []): arr
|
|||
$displayName = $originalName !== '' ? $originalName : ($storedName !== '' ? $storedName : 'file');
|
||||
}
|
||||
|
||||
$sourceMetadata = $this->extractImageMetadataFromFileObject($file);
|
||||
|
||||
$mime = TicketAttachment::normalizeMimeType(
|
||||
method_exists($file, 'getClientMimeType') ? (string) $file->getClientMimeType() : '',
|
||||
$originalName,
|
||||
|
|
@ -46,7 +48,10 @@ public function store(object $file, string $directory, array $options = []): arr
|
|||
$metadata = [];
|
||||
|
||||
if (str_starts_with($mime, 'image/')) {
|
||||
$metadata = $this->extractImageMetadata($path);
|
||||
$metadata = $this->mergeImageMetadata(
|
||||
$this->extractImageMetadata($path),
|
||||
$sourceMetadata,
|
||||
);
|
||||
if ($this->shouldOptimizeImage($metadata, $options)) {
|
||||
$this->optimizeImage($path, $mime, $metadata);
|
||||
}
|
||||
|
|
@ -80,6 +85,30 @@ private function shouldOptimizeImage(array $metadata, array $options = []): bool
|
|||
return ! $this->hasEmbeddedImageMetadata($metadata);
|
||||
}
|
||||
|
||||
private function mergeImageMetadata(array $storedMetadata, array $sourceMetadata): array
|
||||
{
|
||||
$merged = $storedMetadata;
|
||||
|
||||
foreach ($sourceMetadata as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
if ($value === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$merged[$key] = array_replace(is_array($merged[$key] ?? null) ? $merged[$key] : [], $value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
private function hasEmbeddedImageMetadata(array $metadata): bool
|
||||
{
|
||||
return filled($metadata['exif_datetime'] ?? null)
|
||||
|
|
@ -183,6 +212,30 @@ private function applyOrientation($source, int $orientation)
|
|||
private function extractImageMetadata(string $path): array
|
||||
{
|
||||
$absolutePath = Storage::disk('public')->path($path);
|
||||
return $this->extractImageMetadataFromAbsolutePath($absolutePath);
|
||||
}
|
||||
|
||||
private function extractImageMetadataFromFileObject(object $file): array
|
||||
{
|
||||
$absolutePath = null;
|
||||
|
||||
if (method_exists($file, 'getRealPath')) {
|
||||
$absolutePath = $file->getRealPath();
|
||||
}
|
||||
|
||||
if ((! is_string($absolutePath) || $absolutePath === '') && method_exists($file, 'path')) {
|
||||
$absolutePath = $file->path();
|
||||
}
|
||||
|
||||
if (! is_string($absolutePath) || $absolutePath === '' || ! is_file($absolutePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->extractImageMetadataFromAbsolutePath($absolutePath);
|
||||
}
|
||||
|
||||
private function extractImageMetadataFromAbsolutePath(string $absolutePath): array
|
||||
{
|
||||
$metadata = [];
|
||||
|
||||
$imageSize = @getimagesize($absolutePath);
|
||||
|
|
|
|||
|
|
@ -1,66 +1,98 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="space-y-4">
|
||||
@php
|
||||
$sourceMode = data_get($data ?? [], 'source_mode', 'linux_server');
|
||||
@endphp
|
||||
@php
|
||||
$sourceMode = data_get($data ?? [], 'source_mode', 'linux_server');
|
||||
$preview = $this->getCommandPreviewData();
|
||||
$selectedLabel = data_get($stabiliOptions ?? [], data_get($preview, 'stabile_code', ''), data_get($preview, 'stabile_code', ''));
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
:color="$sourceMode === 'linux_server' ? 'primary' : 'gray'"
|
||||
wire:click="$set('data.source_mode','linux_server')"
|
||||
>Linux / Server</x-filament::button>
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
:color="$sourceMode === 'windows_upload' ? 'primary' : 'gray'"
|
||||
wire:click="$set('data.source_mode','windows_upload')"
|
||||
>Windows / Upload</x-filament::button>
|
||||
</div>
|
||||
<div class="space-y-6" x-data="{ tab: @js($sourceMode === 'windows_upload' ? 'windows' : 'linux') }">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-slate-900">Configurazione sorgente e import</h2>
|
||||
<p class="mt-1 text-xs text-slate-600">Seleziona la sorgente, imposta stabile e anni, poi usa la tab operativa per import, aggiornamenti e re-sync.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
:color="$sourceMode === 'linux_server' ? 'primary' : 'gray'"
|
||||
x-on:click="tab = 'linux'"
|
||||
wire:click="$set('data.source_mode','linux_server')"
|
||||
>Linux / Server</x-filament::button>
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
:color="$sourceMode === 'windows_upload' ? 'primary' : 'gray'"
|
||||
x-on:click="tab = 'windows'"
|
||||
wire:click="$set('data.source_mode','windows_upload')"
|
||||
>Windows / Upload</x-filament::button>
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
color="gray"
|
||||
x-on:click="tab = 'operativa'"
|
||||
>Tab operativa</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
{{ $this->getSchema('form') }}
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-slate-200 bg-slate-50 p-3">
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Stabile selezionato</div>
|
||||
<div class="mt-1 text-sm text-slate-800">{{ $selectedLabel !== '' ? $selectedLabel : 'Nessuno selezionato' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Amministratore</div>
|
||||
<div class="mt-1 text-sm text-slate-800">{{ data_get($preview, 'amministratore_id') ?: '-' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Anno legacy diretto</div>
|
||||
<div class="mt-1 text-sm text-slate-800">{{ data_get($preview, 'anno_dir') ?: '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<x-filament::button type="button" wire:click="loadStabili">
|
||||
Carica elenco stabili da Stabili.mdb
|
||||
</x-filament::button>
|
||||
<div class="mt-4">
|
||||
{{ $this->getSchema('form') }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<x-filament::button type="button" color="gray" wire:click="caricaAnniDaGenerale" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">
|
||||
Carica anni da generale_stabile.mdb
|
||||
</x-filament::button>
|
||||
<section class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<div class="border-b border-slate-200 px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button" class="rounded-lg px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="tab === 'linux' ? 'bg-sky-600 text-white' : 'bg-slate-100 text-slate-700'"
|
||||
x-on:click="tab = 'linux'">
|
||||
Connessione Linux
|
||||
</button>
|
||||
<button type="button" class="rounded-lg px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="tab === 'windows' ? 'bg-sky-600 text-white' : 'bg-slate-100 text-slate-700'"
|
||||
x-on:click="tab = 'windows'">
|
||||
Caricamento Windows
|
||||
</button>
|
||||
<button type="button" class="rounded-lg px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="tab === 'operativa' ? 'bg-sky-600 text-white' : 'bg-slate-100 text-slate-700'"
|
||||
x-on:click="tab = 'operativa'">
|
||||
Operativa / Re-sync
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-filament::button type="button" color="success" wire:click="creaStabileDaMdb" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">
|
||||
Crea / apri stabile selezionato
|
||||
</x-filament::button>
|
||||
<div class="p-4">
|
||||
<div x-show="tab === 'linux'">
|
||||
@include('filament.pages.gescon.partials.importazione-archivi-linux', ['preview' => $preview])
|
||||
</div>
|
||||
|
||||
<x-filament::button type="button" color="gray" wire:click="eseguiImportSelezionato" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">
|
||||
Importa solo dati selezionati
|
||||
</x-filament::button>
|
||||
<div x-show="tab === 'windows'">
|
||||
@include('filament.pages.gescon.partials.importazione-archivi-windows', ['preview' => $preview])
|
||||
</div>
|
||||
|
||||
<x-filament::button type="button" color="primary" wire:click="eseguiImportAllineamentoCompleto" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">
|
||||
Import + allineamento (comando unico)
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button type="button" color="warning" wire:click="applicaEnrichmentStabile" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">
|
||||
Applica enrichment (banca/posta/rate/catasto)
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button type="button" color="info" wire:click="aggiornaFornitoriGescon" :disabled="blank(data_get($data ?? [], 'path_root'))">
|
||||
Aggiorna fornitori (GESCON)
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button type="button" color="warning" wire:click="refreshImportTagLegacyFornitoriGlobale" :disabled="blank(data_get($data ?? [], 'path_root'))">
|
||||
Refresh + Import TAG legacy (tutti i fornitori)
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button type="button" color="info" wire:click="aggiornaAnagraficheDaCondomin" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">
|
||||
Aggiorna anagrafiche (condomin → Rubrica)
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button type="button" color="danger" wire:click="applicaEnrichmentTuttiStabili" :disabled="blank(data_get($data ?? [], 'path_root'))">
|
||||
Enrichment massivo stabili
|
||||
</x-filament::button>
|
||||
</div>
|
||||
<div x-show="tab === 'operativa'">
|
||||
@include('filament.pages.gescon.partials.importazione-archivi-operativa', [
|
||||
'preview' => $preview,
|
||||
'data' => $data ?? [],
|
||||
'stabiliOptions' => $stabiliOptions ?? [],
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Percorsi attesi sul server Linux</div>
|
||||
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Root archivio</div>
|
||||
<pre class="mt-1 overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'paths.root', '/mnt/gescon-archives/gescon') }}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Stabili.mdb / Fornitori.mdb</div>
|
||||
<pre class="mt-1 overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'paths.stabili_mdb', '-') }}
|
||||
{{ data_get($preview, 'paths.fornitori_mdb', '-') }}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">generale_stabile.mdb</div>
|
||||
<pre class="mt-1 overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'paths.generale_mdb', '-') }}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">singolo_anno.mdb</div>
|
||||
<pre class="mt-1 overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'paths.singolo_mdb', '-') }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Verifica rapida archivio</div>
|
||||
<p class="mt-1 text-xs text-slate-600">Usa questi comandi se vuoi controllare a terminale che i file legacy siano leggibili dal server staging.</p>
|
||||
<pre class="mt-3 overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">ls -la "{{ data_get($preview, 'paths.root', '/mnt/gescon-archives/gescon') }}"
|
||||
ls -la "{{ data_get($preview, 'paths.generale_mdb', '') }}"
|
||||
ls -la "{{ data_get($preview, 'paths.singolo_mdb', '') }}"
|
||||
ls -la "{{ data_get($preview, 'paths.fornitori_mdb', '') }}"</pre>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Comandi rapidi legacy</div>
|
||||
<p class="mt-1 text-xs text-slate-600">Comandi compatti già valorizzati con stabile, anni e path selezionati nella pagina.</p>
|
||||
<pre class="mt-3 overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'commands.allineamento', 'Seleziona stabile e anni per generare il comando.') }}
|
||||
|
||||
{{ data_get($preview, 'commands.fornitori_sync', 'Seleziona amministratore e Fornitori.mdb per generare il comando.') }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
@php
|
||||
$selectedLabel = data_get($stabiliOptions ?? [], data_get($preview, 'stabile_code', ''), data_get($preview, 'stabile_code', ''));
|
||||
@endphp
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900">
|
||||
Se importi `unità`, `soggetti` o `diritti`, la pagina ora lancia anche il re-sync di anagrafiche, ruoli rubrica e bonifica recapiti/contatti collegati allo stabile selezionato.
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-3">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">1. Setup archivio</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<x-filament::button type="button" size="sm" wire:click="loadStabili">Carica elenco stabili</x-filament::button>
|
||||
<x-filament::button type="button" size="sm" color="gray" wire:click="caricaAnniDaGenerale" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Carica anni</x-filament::button>
|
||||
<x-filament::button type="button" size="sm" color="success" wire:click="creaStabileDaMdb" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Crea / apri stabile</x-filament::button>
|
||||
</div>
|
||||
<div class="mt-3 text-xs text-slate-600">Stabile attuale: {{ $selectedLabel !== '' ? $selectedLabel : 'nessuno' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">2. Import / aggiornamento</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<x-filament::button type="button" size="sm" color="gray" wire:click="eseguiImportSelezionato" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Importa dati selezionati</x-filament::button>
|
||||
<x-filament::button type="button" size="sm" color="primary" wire:click="eseguiImportAllineamentoCompleto" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Import + allineamento</x-filament::button>
|
||||
<x-filament::button type="button" size="sm" color="info" wire:click="aggiornaFornitoriGescon" :disabled="blank(data_get($data ?? [], 'path_root'))">Aggiorna fornitori</x-filament::button>
|
||||
</div>
|
||||
<div class="mt-3 text-xs text-slate-600">Usa `Import + allineamento` come flusso principale; `Importa dati selezionati` serve per slice mirate.</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">3. Re-sync relazioni</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<x-filament::button type="button" size="sm" color="warning" wire:click="risincronizzaRelazioniImportate" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Risincronizza relazioni</x-filament::button>
|
||||
<x-filament::button type="button" size="sm" color="info" wire:click="aggiornaAnagraficheDaCondomin" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Sync anagrafiche</x-filament::button>
|
||||
<x-filament::button type="button" size="sm" color="warning" wire:click="refreshImportTagLegacyFornitoriGlobale" :disabled="blank(data_get($data ?? [], 'path_root'))">Refresh TAG fornitori</x-filament::button>
|
||||
</div>
|
||||
<div class="mt-3 text-xs text-slate-600">Serve quando i dati locali esistono già e vuoi riallineare rubrica, ruoli e recapiti senza reimportare tutto.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-2">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Comandi pronti</div>
|
||||
<div class="mt-3 space-y-3 text-xs text-slate-600">
|
||||
<div>
|
||||
<div class="mb-1 font-semibold uppercase tracking-wide text-slate-500">Flusso principale</div>
|
||||
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'commands.allineamento', 'Seleziona stabile e anni per generare il comando.') }}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 font-semibold uppercase tracking-wide text-slate-500">Fornitori</div>
|
||||
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'commands.fornitori_sync', 'Seleziona amministratore e Fornitori.mdb per generare il comando.') }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Relazioni e riallineamento</div>
|
||||
<div class="mt-3 space-y-3 text-xs text-slate-600">
|
||||
<div>
|
||||
<div class="mb-1 font-semibold uppercase tracking-wide text-slate-500">Sync anagrafiche</div>
|
||||
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'commands.anagrafiche_sync', 'Seleziona uno stabile per generare il comando.') }}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 font-semibold uppercase tracking-wide text-slate-500">Sync ruoli / repair contatti</div>
|
||||
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'commands.ruoli_sync', 'Crea o apri prima lo stabile locale per ottenere lo stabile_id.') }}
|
||||
|
||||
{{ data_get($preview, 'commands.repair_contacts', 'Seleziona uno stabile per generare il comando.') }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Import modulare</div>
|
||||
<div class="mt-3 grid gap-3 xl:grid-cols-3">
|
||||
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'commands.unita', 'Serve stabile + anno legacy per generare il comando.') }}</pre>
|
||||
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'commands.soggetti', 'Serve stabile + anno legacy per generare il comando.') }}</pre>
|
||||
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ data_get($preview, 'commands.diritti', 'Serve stabile + anno legacy per generare il comando.') }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
|
||||
In modalità Windows la pagina non legge i file direttamente dal PC: carichi ZIP, Stabili.mdb e Fornitori.mdb, poi il server staging li usa come sorgente temporanea.
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Cosa caricare</div>
|
||||
<ul class="mt-3 space-y-2 text-sm text-slate-700">
|
||||
<li>ZIP dell'archivio GESCON con cartella `dbc` e cartelle stabili.</li>
|
||||
<li>`Stabili.mdb` se vuoi caricare o rileggere l'elenco stabili da file singolo.</li>
|
||||
<li>`Fornitori.mdb` se vuoi aggiornare solo i fornitori dal legacy.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">PowerShell rapido</div>
|
||||
<p class="mt-1 text-xs text-slate-600">Se devi preparare uno ZIP dal PC Windows.</p>
|
||||
<pre class="mt-3 overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">Compress-Archive -Path "C:\\GESCON\\*" -DestinationPath "C:\\temp\\gescon-archive.zip" -Force</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Uso consigliato</div>
|
||||
<ol class="mt-3 space-y-2 text-sm text-slate-700">
|
||||
<li>Carica ZIP o file MDB nella parte alta della pagina.</li>
|
||||
<li>Premi `Carica elenco stabili da Stabili.mdb`.</li>
|
||||
<li>Seleziona stabile e anni, poi vai in `Operativa / Re-sync`.</li>
|
||||
<li>Esegui prima `Import + allineamento` oppure `Aggiorna fornitori` a seconda del blocco dati che vuoi riallineare.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-base font-semibold text-slate-900">Google</div>
|
||||
<div class="mt-1 text-xs text-slate-600">Area dedicata per collegamento Google, sincronizzazioni e template Drive per stabile.</div>
|
||||
</div>
|
||||
|
||||
@livewire(\App\Filament\Widgets\GoogleWorkspaceOverview::class)
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
26
resources/views/filament/pages/mobile/dashboard.blade.php
Normal file
26
resources/views/filament/pages/mobile/dashboard.blade.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-base font-semibold text-slate-900">Mobile</div>
|
||||
<div class="mt-1 text-xs text-slate-600">Contenitore unico per ticket, letture e prossimi flussi mobile ottimizzati.</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Ticket Mobile</div>
|
||||
<div class="mt-1 text-xs text-slate-600">Inserimento rapido ticket dal telefono con foto, contesto chiamata e nominativo rubrica.</div>
|
||||
<div class="mt-3">
|
||||
<a href="{{ \App\Filament\Pages\Supporto\TicketMobile::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-sky-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-600">Apri Ticket Mobile</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Letture servizi</div>
|
||||
<div class="mt-1 text-xs text-slate-600">Base amministrativa per le letture. Qui agganceremo il flusso dedicato lettura acqua con GPS, EXIF e accesso nominativo.</div>
|
||||
<div class="mt-3">
|
||||
<a href="{{ \App\Filament\Pages\Condomini\LettureServiziArchivio::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">Apri Letture Servizi</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
@ -5,6 +5,18 @@
|
|||
<div class="mt-1 text-xs text-gray-500">Area tecnica per manutenzione, sincronizzazione Google e debug operativo.</div>
|
||||
</div>
|
||||
|
||||
@livewire(\App\Filament\Widgets\GoogleWorkspaceOverview::class)
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">Google e scadenzario</div>
|
||||
<div class="mt-1 text-xs text-slate-600">Il pannello completo Google ora ha una pagina dedicata sotto Impostazioni.</div>
|
||||
</div>
|
||||
<a href="{{ \App\Filament\Pages\Impostazioni\GoogleDashboard::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-slate-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-800">
|
||||
Apri pagina Google
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@livewire(\App\Filament\Widgets\GoogleScadenziarioOverview::class)
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
|
|
@ -56,20 +56,8 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{{ $this->getDashboardFilamentUrl() }}" class="inline-flex items-center rounded-md bg-gray-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Dashboard Filament</a>
|
||||
<a href="{{ $this->getRubricaFilamentUrl() }}" 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">Rubrica Unica Filament</a>
|
||||
<a href="{{ $this->getAdminMobileHubUrl() }}" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">Hub Cellulare / WebApp</a>
|
||||
<a href="{{ $this->getCondominoMobileTicketUrl() }}" class="inline-flex items-center rounded-md bg-amber-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-500" target="_blank" rel="noopener noreferrer">WebApp Ticket Condomino</a>
|
||||
<a href="{{ $this->getTicketArchivioUrl() }}" 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">Archivio Ticket Completo</a>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Aperti: <span class="font-semibold">{{ $ticketCounters['open'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Urgenti: <span class="font-semibold">{{ $ticketCounters['urgent'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Chiusi: <span class="font-semibold">{{ $ticketCounters['closed'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Totali: <span class="font-semibold">{{ $ticketCounters['all'] ?? 0 }}</span></div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm text-slate-700">
|
||||
Il riepilogo operativo Ticket Mobile ora e nella dashboard principale, cosi i link rapidi e lo stato ticket restano visibili appena entri in Filament.
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
|
|
@ -292,11 +280,31 @@
|
|||
url: (file.type || '').startsWith('image/') ? URL.createObjectURL(file) : null,
|
||||
}));
|
||||
},
|
||||
appendPreviews(target, items) {
|
||||
const existing = new Map((this[target] || []).map((item) => [item.key, item]));
|
||||
|
||||
items.forEach((item) => {
|
||||
existing.set(item.key, item);
|
||||
});
|
||||
|
||||
this[target] = Array.from(existing.values());
|
||||
},
|
||||
setPreviews(event, kind) {
|
||||
const target = kind === 'camera' ? 'localCameraPreviews' : 'localAttachmentPreviews';
|
||||
this.revoke(this[target]);
|
||||
this[target] = this.mapFiles(event?.target?.files || [], kind);
|
||||
const batchId = Date.now();
|
||||
const mapped = Array.from(event?.target?.files || []).map((file, index) => ({
|
||||
key: `${kind}-${batchId}-${index}-${file.name}-${file.size}`,
|
||||
name: file.name,
|
||||
draftName: this.buildDraftName(file, this.draftSequence + index),
|
||||
size: file.size || 0,
|
||||
isImage: (file.type || '').startsWith('image/'),
|
||||
url: (file.type || '').startsWith('image/') ? URL.createObjectURL(file) : null,
|
||||
}));
|
||||
this.appendPreviews(target, mapped);
|
||||
this.persist();
|
||||
if (event?.target) {
|
||||
event.target.value = null;
|
||||
}
|
||||
},
|
||||
clearPreviews(kind = null) {
|
||||
if (kind === null || kind === 'camera') {
|
||||
|
|
@ -415,6 +423,11 @@
|
|||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
@foreach($this->selectedUploads as $upload)
|
||||
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
|
||||
@if($upload['is_image'] && $upload['preview_url'])
|
||||
<div class="mb-2 aspect-[4/3] overflow-hidden rounded-lg border bg-slate-50">
|
||||
<img src="{{ $upload['preview_url'] }}" alt="{{ $upload['name'] }}" class="h-full w-full object-cover" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-wrap items-center gap-2 rounded-lg border bg-gray-50 px-3 py-2">
|
||||
<span class="inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold {{ $upload['is_image'] ? 'bg-sky-100 text-sky-700' : 'bg-slate-200 text-slate-700' }}">
|
||||
{{ $upload['is_image'] ? 'Immagine' : (strtoupper(pathinfo($upload['name'], PATHINFO_EXTENSION) ?: 'FILE')) }}
|
||||
|
|
@ -450,15 +463,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold">Gestione operativa separata</div>
|
||||
<div class="mt-1 text-xs text-gray-500">Per lavorazione, presa in carico, risoluzione e chiusura usa la pagina dedicata.</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-indigo-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-600">Vai a Gestione Ticket</a>
|
||||
<a href="{{ $this->getHelpUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Apri Help Ticket Mobile</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
Suggerimento: aggiungi questa pagina ai preferiti del browser sul telefono per usarla come webapp rapida.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
<x-filament-widgets::widget>
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Ticket e Hub Mobile</x-slot>
|
||||
<x-slot name="description">Accesso rapido a inserimento ticket, archivio operativo e flusso mobile/webapp.</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{{ $this->getDashboardFilamentUrl() }}" class="inline-flex items-center rounded-md bg-gray-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Dashboard Filament</a>
|
||||
<a href="{{ $this->getRubricaFilamentUrl() }}" 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">Rubrica Unica Filament</a>
|
||||
<a href="{{ $this->getAdminMobileHubUrl() }}" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">Hub Cellulare / WebApp</a>
|
||||
<a href="{{ $this->getCondominoMobileTicketUrl() }}" class="inline-flex items-center rounded-md bg-amber-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-500" target="_blank" rel="noopener noreferrer">WebApp Ticket Condomino</a>
|
||||
<a href="{{ $this->getTicketMobileUrl() }}" class="inline-flex items-center rounded-md bg-sky-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-600">Apri Ticket Mobile</a>
|
||||
<a href="{{ $this->getTicketArchivioUrl() }}" 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">Archivio Ticket Completo</a>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Aperti: <span class="font-semibold">{{ $ticketCounters['open'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Urgenti: <span class="font-semibold">{{ $ticketCounters['urgent'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Chiusi: <span class="font-semibold">{{ $ticketCounters['closed'] ?? 0 }}</span></div>
|
||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs">Totali: <span class="font-semibold">{{ $ticketCounters['all'] ?? 0 }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold">Gestione operativa separata</div>
|
||||
<div class="mt-1 text-xs text-gray-500">Per lavorazione, presa in carico, risoluzione e chiusura usa la pagina dedicata.</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<a href="{{ $this->getGestioneTicketUrl() }}" class="inline-flex items-center rounded-md bg-indigo-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-600">Vai a Gestione Ticket</a>
|
||||
<a href="{{ $this->getHelpUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Apri Help Ticket Mobile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-filament-widgets::widget>
|
||||
Loading…
Reference in New Issue
Block a user