Add archive QR XML payloads and staging support updates
This commit is contained in:
parent
d7e9a1c39d
commit
e8440e873c
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class NetgesconNodeRefreshCommand extends Command
|
||||
|
|
@ -215,8 +214,8 @@ public function handle(): int
|
|||
'event:cache' => 'NODE REFRESH: php artisan event:cache',
|
||||
'queue:restart' => 'NODE REFRESH: php artisan queue:restart',
|
||||
] as $artisanCommand => $label) {
|
||||
$exit = Artisan::call($artisanCommand);
|
||||
$output = trim((string) Artisan::output());
|
||||
$exit = Artisan::call($artisanCommand);
|
||||
$output = trim((string) Artisan::output());
|
||||
$fullOutput[] = $label;
|
||||
$fullOutput[] = $output;
|
||||
|
||||
|
|
|
|||
109
app/Filament/Pages/Condomini/AssembleeHub.php
Normal file
109
app/Filament/Pages/Condomini/AssembleeHub.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Condomini;
|
||||
|
||||
use App\Filament\Pages\Strumenti\DocumentiArchivio;
|
||||
use App\Models\Assemblea;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\User;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use UnitEnum;
|
||||
|
||||
class AssembleeHub extends Page
|
||||
{
|
||||
protected static ?string $navigationLabel = 'HUB Assemblee';
|
||||
|
||||
protected static ?string $title = 'HUB Assemblee';
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-megaphone';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
|
||||
|
||||
protected static ?int $navigationSort = 35;
|
||||
|
||||
protected static ?string $slug = 'condomini/assemblee-hub';
|
||||
|
||||
protected string $view = 'filament.pages.condomini.assemblee-hub';
|
||||
|
||||
public ?Stabile $stabileAttivo = null;
|
||||
|
||||
public static function canAccess(): 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;
|
||||
}
|
||||
|
||||
$activeId = StabileContext::resolveActiveStabileId($user);
|
||||
if (! $activeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->stabileAttivo = StabileContext::accessibleStabili($user)->firstWhere('id', $activeId);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('apri_documenti')
|
||||
->label('Archivio documenti')
|
||||
->icon('heroicon-o-folder-open')
|
||||
->url(fn(): string => DocumentiArchivio::getUrl(panel: 'admin-filament')),
|
||||
];
|
||||
}
|
||||
|
||||
public function getStatsProperty(): array
|
||||
{
|
||||
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
|
||||
if ($stabileId <= 0) {
|
||||
return ['totale' => 0, 'convocate' => 0, 'in_calendario' => 0, 'con_verbale' => 0];
|
||||
}
|
||||
|
||||
$query = Assemblea::query()->where('stabile_id', $stabileId);
|
||||
|
||||
return [
|
||||
'totale' => (clone $query)->count(),
|
||||
'convocate' => (clone $query)->where('stato', 'convocata')->count(),
|
||||
'in_calendario'=> (clone $query)
|
||||
->where(function ($builder): void {
|
||||
$today = now();
|
||||
|
||||
$builder->whereDate('data_prima_convocazione', '>=', $today->toDateString())
|
||||
->orWhereDate('data_seconda_convocazione', '>=', $today->toDateString());
|
||||
})
|
||||
->count(),
|
||||
'con_verbale' => (clone $query)->whereHas('verbale')->count(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getAssembleeRowsProperty(): Collection
|
||||
{
|
||||
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
|
||||
if ($stabileId <= 0) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return Assemblea::query()
|
||||
->with(['verbale:id,assemblea_id'])
|
||||
->withCount(['ordineGiorno', 'convocazioni', 'presenze', 'documenti'])
|
||||
->where('stabile_id', $stabileId)
|
||||
->orderByDesc('data_seconda_convocazione')
|
||||
->orderByDesc('data_prima_convocazione')
|
||||
->orderByDesc('id')
|
||||
->limit(16)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use UnitEnum;
|
||||
|
||||
|
|
@ -229,4 +230,33 @@ protected function buildAmazonSearchUrl(string $term): string
|
|||
|
||||
return 'https://www.amazon.it/s?' . http_build_query($query);
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
public function getAmazonSetupChecklistProperty(): array
|
||||
{
|
||||
$items = [
|
||||
['key' => 'AMAZON_CREATORS_ENABLED', 'required' => true, 'value' => (bool) config('services.amazon.creators_enabled', false) ? 'true' : 'false'],
|
||||
['key' => 'AMAZON_CREATORS_API_BASE_URL', 'required' => true, 'value' => (string) config('services.amazon.creators_api_base_url', '')],
|
||||
['key' => 'AMAZON_CREATORS_TOKEN_URL', 'required' => true, 'value' => (string) config('services.amazon.creators_token_url', '')],
|
||||
['key' => 'AMAZON_CREATORS_CREDENTIAL_ID', 'required' => true, 'value' => (string) config('services.amazon.creators_credential_id', '')],
|
||||
['key' => 'AMAZON_CREATORS_CREDENTIAL_SECRET', 'required' => true, 'value' => (string) config('services.amazon.creators_credential_secret', '')],
|
||||
['key' => 'AMAZON_CREATORS_SEARCH_PATH', 'required' => true, 'value' => (string) data_get(config('services.amazon.creators_paths', []), 'searchItems', '')],
|
||||
['key' => 'AMAZON_CREATORS_GET_ITEMS_PATH', 'required' => true, 'value' => (string) data_get(config('services.amazon.creators_paths', []), 'getItems', '')],
|
||||
['key' => 'AMAZON_MARKETPLACE', 'required' => false, 'value' => (string) config('services.amazon.marketplace', 'www.amazon.it')],
|
||||
['key' => 'AMAZON_REFERRAL_TAG', 'required' => false, 'value' => (string) config('services.amazon.referral_tag', '')],
|
||||
];
|
||||
|
||||
return array_map(static function (array $item): array {
|
||||
$rawValue = trim((string) ($item['value'] ?? ''));
|
||||
$isBoolean = ($item['key'] ?? '') === 'AMAZON_CREATORS_ENABLED';
|
||||
$configured = $isBoolean ? $rawValue === 'true' : $rawValue !== '';
|
||||
|
||||
return [
|
||||
'key' => (string) ($item['key'] ?? ''),
|
||||
'required' => (bool) ($item['required'] ?? false),
|
||||
'configured' => $configured,
|
||||
'display' => $rawValue !== '' ? Str::limit($rawValue, 54) : 'non valorizzata',
|
||||
];
|
||||
}, $items);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
use App\Models\User;
|
||||
use App\Services\Documenti\DocumentoArchivioVisibilityService;
|
||||
use App\Services\Documenti\PdfTextExtractionService;
|
||||
use App\Support\Archivio\ArchiveQrXmlPayload;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
|
|
@ -909,6 +910,23 @@ public function getGenericLabelPreviewProperty(): array
|
|||
|
||||
$amazonUrl = trim((string) ($this->normalizeAmazonPurchaseUrl($this->genericLabelForm['amazon_url'] ?? null) ?? ''));
|
||||
|
||||
$qrProfiles = ArchiveQrXmlPayload::buildProfiles([
|
||||
'id' => 'AF-QR-PREVIEW',
|
||||
'legacy' => 'AF:AF-QR-PREVIEW',
|
||||
'type' => Str::upper((string) ($this->genericLabelForm['tipo_item'] ?? 'SCATOLA')),
|
||||
'stabile_id' => (string) ((int) ($this->resolveActiveStabile()?->id ?? 0)),
|
||||
'parent' => trim((string) ($this->genericLabelForm['parent_id'] ?? '')) !== '' ? 'COLLEGATO' : 'RADICE',
|
||||
'code' => $mnemonicCode,
|
||||
'title' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
|
||||
'archive_class' => 'GENERIC_LABEL',
|
||||
'location' => trim(implode('/', array_filter([
|
||||
$this->genericLabelForm['magazzino'] ?? null,
|
||||
$this->genericLabelForm['scaffale'] ?? null,
|
||||
$this->genericLabelForm['ripiano'] ?? null,
|
||||
$this->genericLabelForm['ubicazione_dettaglio'] ?? null,
|
||||
]))),
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'mnemonic_code' => $mnemonicCode,
|
||||
'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
|
||||
|
|
@ -936,14 +954,11 @@ public function getGenericLabelPreviewProperty(): array
|
|||
$stabileAddress,
|
||||
]))),
|
||||
'codice_univoco' => 'AF-QR-PREVIEW',
|
||||
'qr_payload' => implode('|', array_filter([
|
||||
'NGDOC',
|
||||
'AF:AF-QR-PREVIEW',
|
||||
'MN:' . $mnemonicCode,
|
||||
'TIPO:' . Str::upper((string) ($this->genericLabelForm['tipo_item'] ?? 'SCATOLA')),
|
||||
'STB:' . ((int) ($this->resolveActiveStabile()?->id ?? 0)),
|
||||
trim((string) ($this->genericLabelForm['parent_id'] ?? '')) !== '' ? 'PARENT:COLLEGATO' : 'PARENT:RADICE',
|
||||
])),
|
||||
'qr_payload' => (string) ($qrProfiles['xml_open'] ?? ''),
|
||||
'qr_payload_zip' => (string) ($qrProfiles['xml_zip'] ?? ''),
|
||||
'nfc_payload' => (string) ($qrProfiles['xml_nfc'] ?? ''),
|
||||
'qr_payload_length' => (int) ($qrProfiles['xml_open_length'] ?? 0),
|
||||
'nfc_payload_length'=> (int) ($qrProfiles['xml_nfc_length'] ?? 0),
|
||||
'amazon_url' => $amazonUrl,
|
||||
];
|
||||
|
||||
|
|
@ -1978,7 +1993,7 @@ private function initializeScannerAcquisitionForm(): void
|
|||
'data_scadenza' => null,
|
||||
'importo' => null,
|
||||
'faldone' => '',
|
||||
'supporto_fisico' => '',
|
||||
'supporto_fisico' => 'Busta trasparente con anelli da archiviare',
|
||||
'modello_etichetta' => '11354',
|
||||
'busta_numero' => '',
|
||||
'fascicolo_numero' => '',
|
||||
|
|
@ -1993,6 +2008,49 @@ private function initializeScannerAcquisitionForm(): void
|
|||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function getScannerQrPreviewProperty(): array
|
||||
{
|
||||
$title = trim((string) ($this->scannerAcquisitionForm['titolo'] ?? ''));
|
||||
$supporto = trim((string) ($this->scannerAcquisitionForm['supporto_fisico'] ?? ''));
|
||||
$stabileId = (int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0);
|
||||
$categoria = trim((string) ($this->scannerAcquisitionForm['categoria'] ?? 'altro'));
|
||||
$classe = trim((string) ($this->scannerAcquisitionForm['archivio_classe'] ?? 'altro'));
|
||||
$busta = trim((string) ($this->scannerAcquisitionForm['busta_numero'] ?? ''));
|
||||
$fascicolo = trim((string) ($this->scannerAcquisitionForm['fascicolo_numero'] ?? ''));
|
||||
$contenitore = trim((string) ($this->scannerAcquisitionForm['contenitore_numero'] ?? ''));
|
||||
|
||||
$archiveCode = trim(implode('-', array_filter([
|
||||
strtoupper($categoria !== '' ? $categoria : 'DOC'),
|
||||
now()->format('Y'),
|
||||
$busta !== '' ? 'B' . $busta : null,
|
||||
$fascicolo !== '' ? 'F' . $fascicolo : null,
|
||||
$contenitore !== '' ? 'C' . $contenitore : null,
|
||||
])));
|
||||
|
||||
$profiles = ArchiveQrXmlPayload::buildProfiles([
|
||||
'id' => $archiveCode !== '' ? $archiveCode : 'DOC-PREVIEW',
|
||||
'legacy' => 'DOC:' . ($archiveCode !== '' ? $archiveCode : 'DOC-PREVIEW'),
|
||||
'type' => 'ATTO',
|
||||
'stabile_id' => (string) $stabileId,
|
||||
'parent' => $supporto !== '' ? strtoupper(Str::slug($supporto, '-')) : 'RADICE',
|
||||
'code' => $archiveCode,
|
||||
'title' => $title,
|
||||
'document_code' => $archiveCode,
|
||||
'archive_class' => strtoupper($classe),
|
||||
'location' => $supporto,
|
||||
]);
|
||||
|
||||
return [
|
||||
'code' => $archiveCode,
|
||||
'xml_open' => (string) ($profiles['xml_open'] ?? ''),
|
||||
'xml_zip' => (string) ($profiles['xml_zip'] ?? ''),
|
||||
'xml_nfc' => (string) ($profiles['xml_nfc'] ?? ''),
|
||||
'xml_open_length' => (int) ($profiles['xml_open_length'] ?? 0),
|
||||
'xml_nfc_length' => (int) ($profiles['xml_nfc_length'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function initializeDriveTemplateForm(): void
|
||||
{
|
||||
$settings = $this->getArchiveHubSettings();
|
||||
|
|
@ -3037,6 +3095,18 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
|
|||
$riferimentoAcquisizione = trim((string) ($data['riferimento_acquisizione'] ?? ''));
|
||||
$scannerNote = trim((string) ($data['scanner_note'] ?? ''));
|
||||
$origineDocumento = trim((string) ($data['origine_documento'] ?? ''));
|
||||
$documentQrProfiles = ArchiveQrXmlPayload::buildProfiles([
|
||||
'id' => $archiveReference,
|
||||
'legacy' => 'DOC:' . $archiveReference,
|
||||
'type' => 'ATTO',
|
||||
'stabile_id' => (string) $stabileId,
|
||||
'parent' => trim((string) ($data['supporto_fisico'] ?? '')) !== '' ? strtoupper(Str::slug((string) $data['supporto_fisico'], '-')) : 'RADICE',
|
||||
'code' => $archiveReference,
|
||||
'title' => $titolo,
|
||||
'document_code' => $protocollo,
|
||||
'archive_class' => strtoupper($archivioClasse !== '' ? $archivioClasse : $categoria),
|
||||
'location' => trim((string) ($data['supporto_fisico'] ?? '')),
|
||||
]);
|
||||
|
||||
if ($scannerNote !== '') {
|
||||
$note = trim($note . ($note !== '' ? "\n" : '') . 'Scanner: ' . $scannerNote);
|
||||
|
|
@ -3083,6 +3153,9 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
|
|||
'origine_documento' => $origineDocumento !== '' ? $origineDocumento : null,
|
||||
'qr_ready' => true,
|
||||
'nfc_ready' => true,
|
||||
'qr_payload_xml' => (string) ($documentQrProfiles['xml_open'] ?? ''),
|
||||
'qr_payload_zip' => (string) ($documentQrProfiles['xml_zip'] ?? ''),
|
||||
'nfc_payload_xml' => (string) ($documentQrProfiles['xml_nfc'] ?? ''),
|
||||
],
|
||||
'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'),
|
||||
'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Archivio\ArchiveQrXmlPayload;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
|
@ -178,15 +179,17 @@ public function buildQrPayload(): string
|
|||
trim((string) ($this->ubicazione_dettaglio ?? '')),
|
||||
])->filter()->implode('/');
|
||||
|
||||
return implode('|', array_filter([
|
||||
'NGDOC',
|
||||
'AF:' . trim((string) $this->codice_univoco),
|
||||
'TIPO:' . Str::upper((string) $this->tipo_item),
|
||||
'STB:' . (int) $this->stabile_id,
|
||||
'PARENT:' . ($parentCode !== '' ? $parentCode : 'RADICE'),
|
||||
$documentCode !== '' ? 'DOC:' . $documentCode : null,
|
||||
$location !== '' ? 'LOC:' . $location : null,
|
||||
]));
|
||||
return ArchiveQrXmlPayload::build([
|
||||
'id' => trim((string) $this->codice_univoco),
|
||||
'legacy' => 'AF:' . trim((string) $this->codice_univoco),
|
||||
'type' => Str::upper((string) $this->tipo_item),
|
||||
'stabile_id' => (string) ((int) $this->stabile_id),
|
||||
'parent' => $parentCode !== '' ? $parentCode : 'RADICE',
|
||||
'document_code' => $documentCode,
|
||||
'location' => $location,
|
||||
'title' => trim((string) $this->titolo),
|
||||
'archive_class' => 'ARCHIVIO_FISICO',
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildPublicQrUrl(): string
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ public function panel(Panel $panel): Panel
|
|||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}),
|
||||
NavigationItem::make('Cellulare / WebApp')
|
||||
->group('NetGescon')
|
||||
|
|
@ -70,7 +70,7 @@ public function panel(Panel $panel): Panel
|
|||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}),
|
||||
NavigationItem::make('Ticket Mobile')
|
||||
->group('NetGescon')
|
||||
|
|
@ -80,7 +80,7 @@ public function panel(Panel $panel): Panel
|
|||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}),
|
||||
NavigationItem::make('Scheda Stabile')
|
||||
->group('NetGescon')
|
||||
|
|
@ -90,7 +90,7 @@ public function panel(Panel $panel): Panel
|
|||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}),
|
||||
NavigationItem::make('Catalogo Fornitore')
|
||||
->group('Fornitore')
|
||||
|
|
|
|||
127
app/Support/Archivio/ArchiveQrXmlPayload.php
Normal file
127
app/Support/Archivio/ArchiveQrXmlPayload.php
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace App\Support\Archivio;
|
||||
|
||||
final class ArchiveQrXmlPayload
|
||||
{
|
||||
public const VERSION = '1';
|
||||
|
||||
public const PROFILE_LITE = 'MASTER-XML-LITE';
|
||||
|
||||
public const PROFILE_NFC = 'MASTER-NFC-LITE';
|
||||
|
||||
public const COMPRESSED_PREFIX = 'NGXZ1:';
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
public static function build(array $data, string $profile = self::PROFILE_LITE): string
|
||||
{
|
||||
$normalized = self::normalize($data);
|
||||
$checksum = self::checksum($normalized + ['profile' => $profile]);
|
||||
|
||||
$fields = [
|
||||
'id' => $normalized['id'],
|
||||
'legacy' => $normalized['legacy'],
|
||||
'type' => $normalized['type'],
|
||||
'sid' => $normalized['stabile_id'],
|
||||
'parent' => $normalized['parent'],
|
||||
'code' => $normalized['code'],
|
||||
'title' => $normalized['title'],
|
||||
'doc' => $normalized['document_code'],
|
||||
'class' => $normalized['archive_class'],
|
||||
'loc' => $normalized['location'],
|
||||
'cs' => $checksum,
|
||||
];
|
||||
|
||||
$xml = '<ngxa v="' . self::VERSION . '" p="' . self::escape($profile) . '">';
|
||||
|
||||
foreach ($fields as $tag => $value) {
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$xml .= '<' . $tag . '>' . self::escape($value) . '</' . $tag . '>';
|
||||
}
|
||||
|
||||
$xml .= '</ngxa>';
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
public static function buildCompressed(array $data, string $profile = self::PROFILE_LITE): string
|
||||
{
|
||||
$xml = self::build($data, $profile);
|
||||
$compressed = gzcompress($xml, 9);
|
||||
|
||||
if ($compressed === false) {
|
||||
return self::COMPRESSED_PREFIX . base64_encode($xml);
|
||||
}
|
||||
|
||||
return self::COMPRESSED_PREFIX . base64_encode($compressed);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
public static function buildNfcLite(array $data): string
|
||||
{
|
||||
return self::build([
|
||||
'id' => $data['id'] ?? '',
|
||||
'legacy' => $data['legacy'] ?? '',
|
||||
'type' => $data['type'] ?? '',
|
||||
'stabile_id' => $data['stabile_id'] ?? '',
|
||||
'parent' => $data['parent'] ?? 'RADICE',
|
||||
'code' => $data['code'] ?? '',
|
||||
'archive_class' => $data['archive_class'] ?? '',
|
||||
], self::PROFILE_NFC);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function buildProfiles(array $data): array
|
||||
{
|
||||
$xmlOpen = self::build($data, self::PROFILE_LITE);
|
||||
$xmlNfc = self::buildNfcLite($data);
|
||||
|
||||
return [
|
||||
'xml_open' => $xmlOpen,
|
||||
'xml_open_length' => strlen($xmlOpen),
|
||||
'xml_zip' => self::buildCompressed($data, self::PROFILE_LITE),
|
||||
'xml_nfc' => $xmlNfc,
|
||||
'xml_nfc_length' => strlen($xmlNfc),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function normalize(array $data): array
|
||||
{
|
||||
$id = trim((string) ($data['id'] ?? ''));
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'legacy' => trim((string) ($data['legacy'] ?? ($id !== '' ? 'AF:' . $id : ''))),
|
||||
'type' => trim((string) ($data['type'] ?? '')),
|
||||
'stabile_id' => trim((string) ($data['stabile_id'] ?? '')),
|
||||
'parent' => trim((string) ($data['parent'] ?? 'RADICE')),
|
||||
'code' => trim((string) ($data['code'] ?? '')),
|
||||
'title' => trim((string) ($data['title'] ?? '')),
|
||||
'document_code'=> trim((string) ($data['document_code'] ?? '')),
|
||||
'archive_class'=> trim((string) ($data['archive_class'] ?? '')),
|
||||
'location' => trim((string) ($data['location'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, string> $data */
|
||||
private static function checksum(array $data): string
|
||||
{
|
||||
ksort($data);
|
||||
|
||||
return strtoupper(substr(sha1(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''), 0, 12));
|
||||
}
|
||||
|
||||
private static function escape(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,44 +2,130 @@ # Documenti Archivio DAFARE
|
|||
|
||||
## Obiettivo operativo
|
||||
|
||||
Costruire un flusso unico che parta dal file digitale, generi il codice mnemonico/QR, stampi l'etichetta corretta e accompagni il documento dentro la catena fisica fino alla collocazione finale.
|
||||
Costruire un flusso unico che parta dal file digitale, generi una codifica master stabile, stampi etichette QR leggibili, colleghi eventuali tag NFC o RFID e accompagni il documento dentro la catena fisica fino alla collocazione finale o al passaggio consegne tra amministratori.
|
||||
|
||||
## Slice da fare subito
|
||||
## Stato reale da cui partiamo
|
||||
|
||||
1. Rifinire il builder etichette su `etichette-generiche`.
|
||||
2. Tenere le impostazioni accanto alla riga o al campo che controllano.
|
||||
3. Rendere piu esplicita la modifica e la ristampa delle etichette gia create.
|
||||
4. Portare la gestione di supporto e ubicazione nella tab `movimentazione-codici`.
|
||||
5. Preparare il flusso `file base -> primo supporto -> contenitori successivi`.
|
||||
- Nel modulo documenti esistono gia tre concetti separati da unificare: Categoria, Classe archivio e Voce archivio.
|
||||
- Nel codice attuale il punto piu vicino e app/Filament/Pages/Strumenti/DocumentiArchivio.php, dove la tab scansione usa categoria e archivio_classe, mentre l'archivio fisico registra la voce separatamente.
|
||||
- Per le assemblee esiste gia una base dati con Assemblea, OrdineGiorno, Convocazione, PresenzaAssemblea, Verbale, DocumentoAssemblea e collegamenti allo stabile.
|
||||
- Il lavoro da fare ora non e inventare concetti nuovi, ma unificare nomenclatura, codifica e flussi sopra strutture gia presenti.
|
||||
|
||||
## Convenzioni archivio da fissare
|
||||
## Decisione guida
|
||||
|
||||
### Codice categoria archivio
|
||||
Tutto deve convergere su una sola codifica master archivistica, usata in quattro contesti:
|
||||
|
||||
- `CONTR-ANNO-###`: contratti di servizio
|
||||
- `ASSIC-ANNO-###`: polizze assicurative
|
||||
- `CERT-ANNO-###`: certificati e autorizzazioni
|
||||
- `PREV-ANNO-###`: preventivi e offerte
|
||||
- `FATT-ANNO-###`: fatture e documenti fiscali
|
||||
- `VERB-ANNO-###`: verbali assemblee
|
||||
- `DOC-ANNO-###`: altri documenti
|
||||
1. Nome logico del documento e riferimento archivistico umano.
|
||||
2. Payload QR o tag NFC.
|
||||
3. Chiave di import/export per passaggio consegne tra amministratori.
|
||||
4. Ancoraggio comune per archivio digitale, archivio fisico, assemblee e future automazioni hardware.
|
||||
|
||||
### Regole comuni
|
||||
## Gruppo A - Codifica master unificata archivio
|
||||
|
||||
- Il prefisso categoria va salvato anche nella voce archivio su database.
|
||||
- Il valore deve essere in formato Linux-safe: maiuscolo, senza spazi, uniforme.
|
||||
### Obiettivo
|
||||
|
||||
Sostituire i tre campi disallineati con un unico concetto di classificazione archivistica master, mantenendo compatibilita con le schermate esistenti.
|
||||
|
||||
### Modello logico proposto
|
||||
|
||||
- codice_master: identificatore archivistico stabile e stampabile
|
||||
- famiglia_archivio: macro famiglia funzionale
|
||||
- classe_archivio: classe normalizzata interna
|
||||
- voce_archivio: descrizione umana operativa
|
||||
- versione_schema: versione della codifica XML, QR e NFC
|
||||
|
||||
### Convenzioni archivio da fissare
|
||||
|
||||
#### Famiglie iniziali
|
||||
|
||||
- CONTR: contratti di servizio
|
||||
- ASSIC: polizze assicurative
|
||||
- CERT: certificati e autorizzazioni
|
||||
- PREV: preventivi e offerte
|
||||
- FATT: fatture e documenti fiscali
|
||||
- VERB: verbali assemblee
|
||||
- ASSEMB: convocazioni, allegati, presenze e verbali assembleari
|
||||
- DOC: altri documenti
|
||||
|
||||
#### Regole comuni
|
||||
|
||||
- Il codice deve essere Linux-safe: maiuscolo, senza spazi, solo caratteri stabili.
|
||||
- Il codice mnemonico deve diventare sia nome logico del file digitale sia fallback umano dell'etichetta stampata.
|
||||
- Il QR o TAG deve contenere l'identificatore unico universale del nodo archivistico, leggibile da smartphone o lettore fisico.
|
||||
- Il passaggio consegne tra amministratori deve riusare lo stesso codice mnemonico/QR senza ricodifiche locali.
|
||||
- Il QR o tag deve contenere sempre almeno schema, codice_master, tipo_record, stabile, hash o checksum.
|
||||
- Il passaggio consegne tra amministratori deve riusare lo stesso codice senza ricodifiche locali.
|
||||
- La descrizione umana puo cambiare, il codice master no.
|
||||
|
||||
### Formato consigliato del codice master
|
||||
|
||||
- FAMIGLIA-ANNO-SEQ
|
||||
- esempi:
|
||||
- CONTR-2026-001
|
||||
- VERB-2026-014
|
||||
- ASSEMB-2026-005
|
||||
|
||||
### Data nei file
|
||||
|
||||
- Inserire la data nel nome file con formato `YYMMGG`.
|
||||
- Inserire la data nel nome file con formato YYMMGG.
|
||||
- Prevedere una futura impostazione generale dell'amministratore per cambiare il formato globale della data.
|
||||
|
||||
## Flussi da realizzare
|
||||
## Gruppo B - XML master per QR, NFC e passaggio consegne
|
||||
|
||||
### 1. Gestione file base
|
||||
### Obiettivo
|
||||
|
||||
Usare XML come formato di trasporto e scambio per non dipendere da link esterni, URL o host specifici.
|
||||
|
||||
### Vincolo tecnico da non perdere di vista
|
||||
|
||||
- Un QR standard non e affidabile per 6000 byte in lettura smartphone generalista.
|
||||
- Per uso reale conviene progettare un XML compatto e mantenere un payload sicuro molto piu basso.
|
||||
- Un NTAG215 offre circa 504 byte utili: questo obbliga a usare un XML molto compatto o una variante abbreviata dedicata al tag NFC.
|
||||
- Quindi servono due profili dello stesso schema:
|
||||
- MASTER-XML-FULL per esportazioni, passaggi consegne e QR ad alta capienza
|
||||
- MASTER-XML-LITE per QR rapidi e soprattutto per NTAG215
|
||||
|
||||
### Campi minimi del profilo full
|
||||
|
||||
- schema
|
||||
- versione
|
||||
- tipo_record
|
||||
- codice_master
|
||||
- stabile_id o codice_stabile
|
||||
- titolo
|
||||
- note sintetiche
|
||||
- nome_file
|
||||
- classe_archivio normalizzata
|
||||
- voce_archivio umana
|
||||
- tag liberi
|
||||
- riferimento RFID o NFC se presente
|
||||
- pagine o consistenza fogli
|
||||
- checksum
|
||||
|
||||
### Campi minimi del profilo lite
|
||||
|
||||
- schema
|
||||
- versione
|
||||
- tipo_record
|
||||
- codice_master
|
||||
- stabile
|
||||
- classe
|
||||
- checksum
|
||||
|
||||
### Uso operativo previsto
|
||||
|
||||
- QR su singolo documento
|
||||
- QR su cartellina, faldone, scatola o foglio consegna
|
||||
- QR master finale su verbale di passaggio consegne con tabella di codifica incorporata
|
||||
- Tag NFC adesivo per scrittura e rilettura rapida da app o bridge locale
|
||||
|
||||
### Regola importante
|
||||
|
||||
Se la lettura trova un codice_master gia presente nel DB, il sistema deve collegare e arricchire il record esistente; se non lo trova, deve proporre import guidato o creazione rapida.
|
||||
|
||||
## Gruppo C - Flusso documenti e archivio fisico
|
||||
|
||||
### Flussi da realizzare
|
||||
|
||||
#### 1. Gestione file base
|
||||
|
||||
Una Blade dedicata deve permettere di registrare il singolo file o una raccolta di fogli con:
|
||||
|
||||
|
|
@ -47,12 +133,13 @@ ### 1. Gestione file base
|
|||
- descrizione
|
||||
- note
|
||||
- tag
|
||||
- categoria come tag strutturato
|
||||
- classificazione master strutturata
|
||||
- numero totale dei fogli o dell'incartamento
|
||||
- selezione del layout etichetta da usare
|
||||
- stampa etichetta con QR e fallback testuale nel titolo e sotto il QR
|
||||
- eventuale lettura o scrittura NFC
|
||||
|
||||
### 2. Primo contenitore fisico
|
||||
#### 2. Primo contenitore fisico
|
||||
|
||||
Dal file base bisogna poter scegliere il primo supporto:
|
||||
|
||||
|
|
@ -63,7 +150,7 @@ ### 2. Primo contenitore fisico
|
|||
|
||||
Il record deve restare collegato al documento digitale per consentire nuove stampe dell'etichetta in qualsiasi momento.
|
||||
|
||||
### 3. Contenitori successivi e movimentazione
|
||||
#### 3. Contenitori successivi e movimentazione
|
||||
|
||||
Serve una gestione separata dove il materiale gia preparato venga caricato nei contenitori successivi:
|
||||
|
||||
|
|
@ -74,43 +161,230 @@ ### 3. Contenitori successivi e movimentazione
|
|||
- scaffale
|
||||
- posto finale
|
||||
|
||||
La tab `movimentazione-codici` deve diventare il centro operativo per:
|
||||
La tab movimentazione-codici deve diventare il centro operativo per:
|
||||
|
||||
- cambiare il contenitore padre
|
||||
- aggiornare supporto operativo
|
||||
- aggiornare magazzino, scaffale, ripiano e dettaglio ubicazione
|
||||
- leggere codici da scanner, smartphone, QR o plugin locale
|
||||
- leggere codici da scanner, smartphone, QR o bridge locale
|
||||
- associare o riscrivere NFC quando consentito dal profilo utente
|
||||
|
||||
## Etichette 11354 e binding DB
|
||||
### Slice da fare subito su documenti
|
||||
|
||||
- Per il formato `11354` prevedere un campo di collegamento ai dati DB da riversare automaticamente in stampa.
|
||||
1. Rifinire il builder etichette su etichette-generiche.
|
||||
2. Tenere le impostazioni accanto alla riga o al campo che controllano.
|
||||
3. Rendere piu esplicita la modifica e la ristampa delle etichette gia create.
|
||||
4. Portare la gestione di supporto e ubicazione nella tab movimentazione-codici.
|
||||
5. Preparare il flusso file base -> primo supporto -> contenitori successivi.
|
||||
6. Introdurre una tabella unica di classificazione da cui derivano Categoria, Classe archivio e Voce archivio.
|
||||
|
||||
## Gruppo D - Etichette 11354 e binding DB
|
||||
|
||||
- Per il formato 11354 prevedere un campo di collegamento ai dati DB da riversare automaticamente in stampa.
|
||||
- Ogni riga deve poter leggere un campo DB oppure testo libero.
|
||||
- Le impostazioni della singola riga devono stare vicino alla riga stessa.
|
||||
- La ristampa deve essere disponibile anche se fallisce la lettura automatica del QR.
|
||||
- Il payload QR stampato deve poter scegliere tra XML lite, XML full o semplice codice master.
|
||||
|
||||
## Catalogo supporti e Amazon
|
||||
## Gruppo E - Plugin locale o bridge dispositivi
|
||||
|
||||
- Ogni supporto fisico scelto dall'utente deve poter avere il pulsante Amazon.
|
||||
- Il link Amazon deve arrivare dal catalogo prodotti gestito dal super admin.
|
||||
- L'archivio fisico non deve duplicare il catalogo: deve consumare il dato gia scelto.
|
||||
### Obiettivo
|
||||
|
||||
## Plugin locale / bridge dispositivi
|
||||
Un agent locale deve fare da ponte fra browser e hardware, sia in lettura che in scrittura.
|
||||
|
||||
Serve un plugin HTTP locale o agent Windows per passare dati hardware fuori dal browser:
|
||||
### Periferiche previste
|
||||
|
||||
- TWAIN/WIA scanner
|
||||
- lettori barcode e QR
|
||||
- NFC
|
||||
- seriali
|
||||
- eventuali periferiche di stampa/lettura
|
||||
- eventuali periferiche di stampa e lettura
|
||||
|
||||
La webapp deve restare il pannello operativo, mentre il bridge locale si occupa della parte hardware.
|
||||
### Flussi minimi del bridge
|
||||
|
||||
- GET /devices: elenco periferiche disponibili
|
||||
- POST /scan: acquisizione scanner o lettore QR
|
||||
- POST /nfc/read: lettura tag
|
||||
- POST /nfc/write: scrittura XML lite su tag NFC
|
||||
- POST /label/print: stampa etichetta con payload deciso dalla webapp
|
||||
|
||||
### Nota tecnica
|
||||
|
||||
La webapp resta il pannello operativo. Il bridge locale si occupa della parte hardware e non diventa mai il sistema autorevole dei dati.
|
||||
|
||||
## Gruppo F - Passaggio consegne tra amministratori
|
||||
|
||||
### Obiettivo
|
||||
|
||||
Preparare una procedura standard di uscita ed entrata che usa documenti umani piu un QR master/XML per ricostruire il contesto senza lavoro manuale ridondante.
|
||||
|
||||
### Flusso proposto
|
||||
|
||||
1. L'amministratore seleziona i faldoni, le cartelline e i documenti da consegnare.
|
||||
2. Il sistema produce un foglio consegna umano stampabile.
|
||||
3. In coda al foglio viene aggiunto un QR master con XML di riepilogo.
|
||||
4. L'amministratore ricevente scansiona il QR master.
|
||||
5. Il sistema propone import, ricostruzione dell'albero archivistico e collegamento ai record gia esistenti.
|
||||
|
||||
### Contenuto minimo del QR master di consegna
|
||||
|
||||
- identificativo passaggio
|
||||
- amministratore uscente
|
||||
- amministratore entrante se noto
|
||||
- stabile o gruppo stabili
|
||||
- elenco contenitori e codici master
|
||||
- schema di classificazione usato
|
||||
- checksum generale
|
||||
|
||||
## Gruppo G - HUB Assemblee dentro Stabili
|
||||
|
||||
### Obiettivo
|
||||
|
||||
Creare un hub operativo unico per ogni stabile che gestisca l'intero ciclo assembleare, dalla convocazione al verbale e alle attivita post-assemblea.
|
||||
|
||||
### Base dati gia presente
|
||||
|
||||
- Assemblea
|
||||
- OrdineGiorno
|
||||
- Convocazione
|
||||
- PresenzaAssemblea
|
||||
- Verbale
|
||||
- DocumentoAssemblea
|
||||
|
||||
### Cosa deve contenere l'HUB
|
||||
|
||||
- elenco assemblee per stabile in ordine cronologico
|
||||
- prima e seconda convocazione
|
||||
- allegati inviati per convocazione
|
||||
- verifica convocazioni prodotte e spedite
|
||||
- gestione destinatari email, PEC, RR e futuri provider postalizzazione
|
||||
- stato spedizione per ciascun destinatario
|
||||
- tabella millesimale di riferimento per assemblea o per singolo punto
|
||||
- gestione punti all'ordine del giorno
|
||||
- allegati e conteggi collegati a ogni punto OdG
|
||||
- norma o criterio di votazione associato al punto
|
||||
- gestione verbale e relativo invio
|
||||
- generazione ticket operativi derivati dalla delibera
|
||||
- registrazione presenza diretta o per delega anche via QR del partecipante
|
||||
|
||||
### Slice 1 consigliata per l'HUB
|
||||
|
||||
1. Pagina indice Assemblee Hub nel gruppo menu Stabile.
|
||||
2. Lista assemblee filtrabile per stabile, stato e data.
|
||||
3. Scheda assemblea con le due convocazioni e pannello allegati.
|
||||
4. Sezione destinatari convocazione con stato invio e canale.
|
||||
5. Sezione OdG con collegamento ad allegati e conti.
|
||||
|
||||
### Slice 2
|
||||
|
||||
1. Gestione presenze, deleghe e quorum.
|
||||
2. Regole di voto per punto OdG.
|
||||
3. Verbale, invio verbale e storico esiti.
|
||||
4. Creazione ticket da delibera.
|
||||
|
||||
### Slice 3
|
||||
|
||||
1. QR partecipante per check-in.
|
||||
2. Import automatico deleghe.
|
||||
3. Integrazione con postalizzazione online.
|
||||
4. Collegamento forte con archivio documentale e codice master.
|
||||
|
||||
## Gruppo H - Area pubblica normativa e knowledge base
|
||||
|
||||
### Obiettivo
|
||||
|
||||
Aprire sul sito pubblico una sezione informativa sulla gestione condominiale, con contenuti normativi referenziati e governati da ruoli editoriali distinti dall'area amministrativa riservata.
|
||||
|
||||
### Contenuti previsti
|
||||
|
||||
- articoli di riferimento
|
||||
- commi e leggi collegate
|
||||
- note applicative
|
||||
- collegamenti a casi pratici o domande frequenti
|
||||
- possibile forum o blog esterno federato ma non mescolato alla parte riservata
|
||||
|
||||
### Governance desiderata
|
||||
|
||||
- redazione affidata a un avvocato o associazione
|
||||
- pubblicazione separata dai permessi amministrativi ordinari
|
||||
- workflow bozza, revisione, pubblicazione
|
||||
|
||||
## Gruppo I - Bacheca amministratore e raccolta voti preliminari
|
||||
|
||||
### Obiettivo
|
||||
|
||||
Consentire all'amministratore di pubblicare comunicazioni e aprire una raccolta di consensi o indicazioni prima dell'assemblea.
|
||||
|
||||
### Funzioni richieste
|
||||
|
||||
- invio email con accesso alla bacheca
|
||||
- notizia pubblica o riservata per stabile o gruppo utenti
|
||||
- allegati
|
||||
- stato lettura
|
||||
- richiesta approvazione o voto preliminare
|
||||
- tracciamento dei riscontri prima dell'assemblea
|
||||
- collegamento eventuale a un punto OdG futuro
|
||||
|
||||
## Gruppo L - Catalogo supporti e Amazon
|
||||
|
||||
- Ogni supporto fisico scelto dall'utente deve poter avere il pulsante Amazon.
|
||||
- Il link Amazon deve arrivare dal catalogo prodotti gestito dal super admin.
|
||||
- L'archivio fisico non deve duplicare il catalogo: deve consumare il dato gia scelto.
|
||||
|
||||
## Gruppo M - Catalogo fornitore, Amazon e TecnoRepair
|
||||
|
||||
### Amazon prodotti fornitore
|
||||
|
||||
- La UI catalogo fornitore esiste gia in fornitore/prodotti.
|
||||
- Il servizio Creators API esiste gia, ma per usarlo davvero su staging servono le variabili ambiente AMAZON_CREATORS_* valorizzate sul nodo.
|
||||
- Variabili richieste per chiudere il setup:
|
||||
- AMAZON_CREATORS_ENABLED=true
|
||||
- AMAZON_CREATORS_API_BASE_URL
|
||||
- AMAZON_CREATORS_TOKEN_URL
|
||||
- AMAZON_CREATORS_CREDENTIAL_ID
|
||||
- AMAZON_CREATORS_CREDENTIAL_SECRET
|
||||
- AMAZON_CREATORS_SEARCH_PATH
|
||||
- AMAZON_CREATORS_GET_ITEMS_PATH
|
||||
- Variabili opzionali ma consigliate:
|
||||
- AMAZON_MARKETPLACE
|
||||
- AMAZON_REFERRAL_TAG
|
||||
- Obiettivo operativo: cercare, salvare e aggiornare candidati Amazon direttamente nel catalogo fornitore interno.
|
||||
|
||||
### TecnoRepair e scheda riparazione
|
||||
|
||||
- Esiste gia una base import TecnoRepair e una sincronizzazione prodotto catalogo da MDB.
|
||||
- Da completare il collegamento con la singola scheda riparazione per gestire meglio stato chiusura e riconsegna.
|
||||
- Campi legacy da intercettare o mappare esplicitamente:
|
||||
- isRiparato
|
||||
- isRiconsegnato
|
||||
- Tmarche
|
||||
- Tapparecchi
|
||||
- Cod_Marca come chiave di legame fra marca e apparecchio
|
||||
- Obiettivo funzionale:
|
||||
- marcare chiusa la singola riparazione quando il legacy la considera riparata
|
||||
- distinguere chiusa da riconsegnata
|
||||
- usare Tmarche e Tapparecchi per costruire o normalizzare marchio e modello nel catalogo prodotti
|
||||
- collegare il prodotto TecnoRepair alla scheda di lavorazione e agli eventuali candidati Amazon equivalenti o sostitutivi
|
||||
|
||||
## Primo ordine di esecuzione consigliato
|
||||
|
||||
1. Tabella unica di classificazione archivio e normalizzazione dei tre campi attuali.
|
||||
2. XML schema lite e full con checksum e codice master.
|
||||
3. Stampa QR coerente con il nuovo schema.
|
||||
4. Lettura QR e collegamento ai record gia esistenti.
|
||||
5. Hub Assemblee slice 1 dentro il gruppo Stabile.
|
||||
6. Bridge locale NFC e scrittura NTAG215.
|
||||
7. Foglio passaggio consegne con QR master.
|
||||
8. Area pubblica normativa e bacheca.
|
||||
|
||||
## Backlog successivo
|
||||
|
||||
1. Foto ubicazione finale come nel ticket.
|
||||
2. Vista modifica/ristampa dalle ultime etichette.
|
||||
2. Vista modifica e ristampa dalle ultime etichette.
|
||||
3. Blade separata per file singolo e per raccolta fogli.
|
||||
4. Conteggio pagine persistente su database.
|
||||
5. Impostazioni globali amministratore per formato data e convenzioni etichetta.
|
||||
6. Integrazione completa col bridge locale di comunicazione.
|
||||
7. QR multilivello per scatole o interi lotti documentali.
|
||||
8. Import ed export XML firmato o verificato con checksum forte.
|
||||
9. Collegamento tra verbali assemblea, delibere e ticket generati.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="space-y-4">
|
||||
@if(! $this->stabileAttivo)
|
||||
<div class="rounded-xl border border-amber-300 bg-amber-50 p-4 text-sm text-amber-800">
|
||||
Nessuno stabile attivo disponibile. Seleziona prima uno stabile per aprire l'hub assemblee.
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-slate-900">{{ $this->stabileAttivo->denominazione ?: 'Stabile attivo' }}</div>
|
||||
<div class="mt-1 text-sm text-slate-500">
|
||||
Prima slice HUB Assemblee: convocazioni, cronologia, allegati e avanzamento operativo dello stabile.
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">
|
||||
Stabile ID {{ (int) $this->stabileAttivo->id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Assemblee totali</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ (int) ($this->stats['totale'] ?? 0) }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Convocate</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ (int) ($this->stats['convocate'] ?? 0) }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">In calendario</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ (int) ($this->stats['in_calendario'] ?? 0) }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Con verbale</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ (int) ($this->stats['con_verbale'] ?? 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">Cronologia assemblee</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Vista iniziale per pilotare convocazioni, allegati, OdG, verbali e ticket derivati nelle prossime slice.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse($this->assembleeRows as $assemblea)
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">
|
||||
{{ $assemblea->tipo ?: 'Assemblea ordinaria' }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-slate-500">
|
||||
Prima convocazione:
|
||||
{{ $assemblea->data_prima_convocazione ? $assemblea->data_prima_convocazione->format('d/m/Y H:i') : 'n/d' }}
|
||||
· Seconda convocazione:
|
||||
{{ $assemblea->data_seconda_convocazione ? $assemblea->data_seconda_convocazione->format('d/m/Y H:i') : 'n/d' }}
|
||||
</div>
|
||||
@if((string) ($assemblea->luogo ?? '') !== '')
|
||||
<div class="mt-1 text-xs text-slate-500">Luogo: {{ $assemblea->luogo }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="rounded-full px-3 py-1 text-xs font-semibold {{ (string) ($assemblea->stato ?? '') === 'convocata' ? 'bg-amber-50 text-amber-700' : ((string) ($assemblea->stato ?? '') === 'svolta' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-700') }}">
|
||||
{{ $assemblea->stato ?: 'da impostare' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid gap-2 text-xs text-slate-600 md:grid-cols-4">
|
||||
<div class="rounded-lg bg-white px-3 py-2">OdG: <span class="font-semibold text-slate-900">{{ (int) ($assemblea->ordine_giorno_count ?? 0) }}</span></div>
|
||||
<div class="rounded-lg bg-white px-3 py-2">Convocazioni: <span class="font-semibold text-slate-900">{{ (int) ($assemblea->convocazioni_count ?? 0) }}</span></div>
|
||||
<div class="rounded-lg bg-white px-3 py-2">Presenze: <span class="font-semibold text-slate-900">{{ (int) ($assemblea->presenze_count ?? 0) }}</span></div>
|
||||
<div class="rounded-lg bg-white px-3 py-2">Documenti: <span class="font-semibold text-slate-900">{{ (int) ($assemblea->documenti_count ?? 0) }}</span></div>
|
||||
</div>
|
||||
|
||||
@if((string) ($assemblea->note ?? '') !== '')
|
||||
<div class="mt-3 rounded-lg border border-dashed border-slate-300 bg-white px-3 py-2 text-xs text-slate-600">
|
||||
{{ \\Illuminate\\Support\\Str::limit((string) $assemblea->note, 220) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-3 text-[11px] text-slate-500">
|
||||
Verbale {{ $assemblea->verbale ? 'presente' : 'non ancora collegato' }}.
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed p-6 text-sm text-slate-500">
|
||||
Nessuna assemblea registrata per lo stabile attivo.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
@ -45,6 +45,28 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if(! $this->amazonConfigured)
|
||||
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 p-4">
|
||||
<div class="text-sm font-semibold text-amber-900">Cosa serve per attivare l'import Amazon</div>
|
||||
<div class="mt-1 text-xs text-amber-800">
|
||||
Per completare l'integrazione servono le credenziali Creators API, i due endpoint operativi e l'attivazione delle variabili ambiente sul nodo dove gira NetGescon.
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid gap-2 md:grid-cols-2">
|
||||
@foreach($this->amazonSetupChecklist as $item)
|
||||
<div class="rounded-lg border px-3 py-2 text-xs {{ $item['configured'] ? 'border-emerald-200 bg-emerald-50 text-emerald-800' : 'border-amber-200 bg-white text-amber-900' }}">
|
||||
<div class="font-semibold">{{ $item['key'] }}{{ $item['required'] ? ' · obbligatoria' : ' · opzionale' }}</div>
|
||||
<div class="mt-1">{{ $item['display'] }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-3 rounded-lg border border-slate-200 bg-white px-3 py-3 text-xs text-slate-700">
|
||||
Con questi dati completi posso chiudere il collegamento Creators API, far partire la ricerca reale da staging e salvare i candidati Amazon nel catalogo interno del fornitore senza passaggi manuali aggiuntivi.
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(count($amazonRows) > 0)
|
||||
<div class="mt-4 grid gap-3 lg:grid-cols-2">
|
||||
@foreach($amazonRows as $row)
|
||||
|
|
|
|||
|
|
@ -349,11 +349,11 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
|||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Titolo documento</label>
|
||||
<input type="text" wire:model.defer="scannerAcquisitionForm.titolo" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Contratto manutenzione ascensore 2025" />
|
||||
<input type="text" wire:model.live.debounce.300ms="scannerAcquisitionForm.titolo" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Contratto manutenzione ascensore 2025" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Categoria</label>
|
||||
<select wire:model.defer="scannerAcquisitionForm.categoria" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||
<select wire:model.live="scannerAcquisitionForm.categoria" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||
@foreach($this->scannerCategoryOptions as $categoryKey => $categoryLabel)
|
||||
<option value="{{ $categoryKey }}">{{ $categoryLabel }}</option>
|
||||
@endforeach
|
||||
|
|
@ -361,7 +361,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
|||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Classe archivio</label>
|
||||
<select wire:model.defer="scannerAcquisitionForm.archivio_classe" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||
<select wire:model.live="scannerAcquisitionForm.archivio_classe" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||
@foreach($this->getArchivioClasseOptions() as $classKey => $classLabel)
|
||||
<option value="{{ $classKey }}">{{ $classLabel }}</option>
|
||||
@endforeach
|
||||
|
|
@ -406,7 +406,12 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
|||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Supporto fisico</label>
|
||||
<input type="text" wire:model.defer="scannerAcquisitionForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Cartella trasparente" />
|
||||
<select wire:model.live="scannerAcquisitionForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||
<option value="Busta trasparente con anelli da archiviare">Busta ad anelli</option>
|
||||
<option value="Cartella archivio">Cartella</option>
|
||||
<option value="Cartellina progetto">Cartellina progetto</option>
|
||||
<option value="Raccoglitore documenti singoli">Piccolo raccoglitore atti singoli</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Descrizione operativa</label>
|
||||
|
|
@ -447,6 +452,26 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
|||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Metadati archivio pronti per QR, collocazione fisica e ricerche OCR successive.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Primo QR XML del singolo atto</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Anteprima del payload che verrà salvato nei metadati archivio del documento: XML aperto per il QR stampato, envelope compresso opzionale e profilo NFC ridotto.</div>
|
||||
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-[11px] font-semibold text-slate-600">Codice archivio preview: {{ $this->scannerQrPreview['code'] !== '' ? $this->scannerQrPreview['code'] : 'in attesa dei dati minimi' }}</div>
|
||||
<div class="mt-3 space-y-3">
|
||||
<div>
|
||||
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">QR XML aperto · {{ (int) ($this->scannerQrPreview['xml_open_length'] ?? 0) }} caratteri</div>
|
||||
<textarea readonly rows="5" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ $this->scannerQrPreview['xml_open'] ?? '' }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">QR XML compresso</div>
|
||||
<textarea readonly rows="4" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ $this->scannerQrPreview['xml_zip'] ?? '' }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">NFC lite · {{ (int) ($this->scannerQrPreview['xml_nfc_length'] ?? 0) }} caratteri</div>
|
||||
<textarea readonly rows="4" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ $this->scannerQrPreview['xml_nfc'] ?? '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
|
|
|||
|
|
@ -386,6 +386,26 @@
|
|||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Il QR codifica l'identità archivistica e la relazione col contenitore padre, non l'host o un URL pubblico.</div>
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Template, binding campi DB e nodo della catena vengono salvati nell'etichetta, così la stampa reale segue il layout scelto.</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Payload XML e NFC</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Il QR stampato resta aperto e leggibile. Il profilo compresso è opzionale. L'NFC usa una variante minima pensata per il tag.</div>
|
||||
|
||||
<div class="mt-3 space-y-3">
|
||||
<div>
|
||||
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">QR XML aperto · {{ (int) data_get($preview, 'payload.qr_payload_length', 0) }} caratteri</div>
|
||||
<textarea readonly rows="5" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ data_get($preview, 'payload.qr_payload', '') }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">QR XML compresso</div>
|
||||
<textarea readonly rows="4" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ data_get($preview, 'payload.qr_payload_zip', '') }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">NFC lite · {{ (int) data_get($preview, 'payload.nfc_payload_length', 0) }} caratteri</div>
|
||||
<textarea readonly rows="4" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ data_get($preview, 'payload.nfc_payload', '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user