Aggiorna flussi operativi e refresh staging sicuro
This commit is contained in:
parent
5f248f825c
commit
d7e9a1c39d
480
app/Console/Commands/GesconFornitoriDedupCommand.php
Normal file
480
app/Console/Commands/GesconFornitoriDedupCommand.php
Normal file
|
|
@ -0,0 +1,480 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Fornitore;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class GesconFornitoriDedupCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'gescon:fornitori-dedup
|
||||||
|
{--apply : Applica la bonifica}
|
||||||
|
{--limit= : Limita il numero di gruppi elaborati}
|
||||||
|
{--admin-id= : In caso di parita preferisce il fornitore di questo amministratore}';
|
||||||
|
|
||||||
|
protected $description = 'Bonifica i doppioni fornitori mantenendo un record canonico per identita e salvando gli alias cod_forn legacy.';
|
||||||
|
|
||||||
|
/** @var array<int, int> */
|
||||||
|
private array $referenceCounts = [];
|
||||||
|
|
||||||
|
/** @var array<int, array{table:string,column:string}> */
|
||||||
|
private array $referenceColumns = [];
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('fornitori')) {
|
||||||
|
$this->error('Tabella fornitori non disponibile.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apply = (bool) $this->option('apply');
|
||||||
|
$limit = $this->option('limit') ? max(1, (int) $this->option('limit')) : null;
|
||||||
|
$preferredAdminId = $this->option('admin-id') !== null ? max(0, (int) $this->option('admin-id')) : null;
|
||||||
|
|
||||||
|
$groups = $this->loadDuplicateGroups($limit);
|
||||||
|
if ($groups === []) {
|
||||||
|
$this->info('Nessun doppione fornitore rilevato.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->referenceColumns = $this->discoverSupplierReferenceColumns();
|
||||||
|
$this->referenceCounts = $this->loadSupplierReferenceCounts();
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'groups' => 0,
|
||||||
|
'duplicates' => 0,
|
||||||
|
'merged' => 0,
|
||||||
|
'deleted' => 0,
|
||||||
|
'moved_links' => 0,
|
||||||
|
'skipped_delete' => 0,
|
||||||
|
'errors' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$previewRows = [];
|
||||||
|
|
||||||
|
foreach ($groups as $groupKey => $suppliers) {
|
||||||
|
$stats['groups']++;
|
||||||
|
$stats['duplicates'] += count($suppliers) - 1;
|
||||||
|
|
||||||
|
$master = $this->pickMaster($suppliers, $preferredAdminId);
|
||||||
|
$duplicates = array_values(array_filter($suppliers, fn(Fornitore $supplier): bool => (int) $supplier->id !== (int) $master->id));
|
||||||
|
|
||||||
|
$previewRows[] = [
|
||||||
|
'group' => $groupKey,
|
||||||
|
'keep' => '#' . (int) $master->id . ' ' . trim((string) ($master->ragione_sociale ?? '')),
|
||||||
|
'keep_code' => $master->cod_forn ?: '—',
|
||||||
|
'drop' => implode(', ', array_map(fn(Fornitore $supplier): string => '#' . (int) $supplier->id, $duplicates)),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $apply) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($master, $duplicates, &$stats): void {
|
||||||
|
$masterModel = Fornitore::query()->findOrFail((int) $master->id);
|
||||||
|
$duplicatesModels = Fornitore::query()
|
||||||
|
->whereIn('id', array_map(fn(Fornitore $supplier): int => (int) $supplier->id, $duplicates))
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($duplicatesModels->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$masterPayload = $this->buildMasterPayload($masterModel, $duplicatesModels->all());
|
||||||
|
$masterModel->fill($masterPayload);
|
||||||
|
if ($masterModel->isDirty()) {
|
||||||
|
$masterModel->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($duplicatesModels as $duplicate) {
|
||||||
|
$stats['moved_links'] += $this->moveSupplierReferences((int) $duplicate->id, (int) $masterModel->id);
|
||||||
|
|
||||||
|
if ($this->hasRemainingReferences((int) $duplicate->id)) {
|
||||||
|
$stats['skipped_delete']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$duplicate->delete();
|
||||||
|
$stats['deleted']++;
|
||||||
|
$stats['merged']++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$stats['errors']++;
|
||||||
|
$this->warn('Gruppo ' . $groupKey . ' non bonificato: ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->table(['Gruppo', 'Canonico', 'cod_forn', 'Duplicati'], array_slice($previewRows, 0, 20));
|
||||||
|
$this->line(json_encode($stats, JSON_UNESCAPED_UNICODE));
|
||||||
|
$this->info($apply ? 'Bonifica fornitori completata.' : 'Dry-run bonifica fornitori completato.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<int, Fornitore>> */
|
||||||
|
private function loadDuplicateGroups(?int $limit = null): array
|
||||||
|
{
|
||||||
|
$suppliers = Fornitore::query()->get([
|
||||||
|
'id',
|
||||||
|
'amministratore_id',
|
||||||
|
'cod_forn',
|
||||||
|
'old_id',
|
||||||
|
'rubrica_id',
|
||||||
|
'ragione_sociale',
|
||||||
|
'partita_iva',
|
||||||
|
'codice_fiscale',
|
||||||
|
'email',
|
||||||
|
'pec',
|
||||||
|
'telefono',
|
||||||
|
'cellulare',
|
||||||
|
'iban',
|
||||||
|
'codice_univoco',
|
||||||
|
'fe_features',
|
||||||
|
'operational_config',
|
||||||
|
'note',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$groups = [];
|
||||||
|
foreach ($suppliers as $supplier) {
|
||||||
|
$groupKey = $this->buildIdentityKey($supplier);
|
||||||
|
if ($groupKey === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups[$groupKey] ??= [];
|
||||||
|
$groups[$groupKey][] = $supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups = array_filter($groups, fn(array $items): bool => count($items) > 1);
|
||||||
|
ksort($groups);
|
||||||
|
|
||||||
|
if ($limit !== null) {
|
||||||
|
$groups = array_slice($groups, 0, $limit, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildIdentityKey(Fornitore $supplier): ?string
|
||||||
|
{
|
||||||
|
$rubricaId = (int) ($supplier->rubrica_id ?? 0);
|
||||||
|
if ($rubricaId > 0) {
|
||||||
|
return 'rubrica:' . $rubricaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$piva = $this->normalizeNumericIdentity($supplier->partita_iva);
|
||||||
|
if ($piva !== null) {
|
||||||
|
return 'piva:' . $piva;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cf = $this->normalizeAlphaNumericIdentity($supplier->codice_fiscale);
|
||||||
|
if ($cf !== null) {
|
||||||
|
return 'cf:' . $cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, Fornitore> $suppliers */
|
||||||
|
private function pickMaster(array $suppliers, ?int $preferredAdminId): Fornitore
|
||||||
|
{
|
||||||
|
$best = null;
|
||||||
|
$bestScore = null;
|
||||||
|
|
||||||
|
foreach ($suppliers as $supplier) {
|
||||||
|
$score = 0;
|
||||||
|
|
||||||
|
if ((int) ($supplier->cod_forn ?? 0) > 0) {
|
||||||
|
$score += 2000;
|
||||||
|
}
|
||||||
|
if ((int) ($supplier->rubrica_id ?? 0) > 0) {
|
||||||
|
$score += 400;
|
||||||
|
}
|
||||||
|
if ($preferredAdminId !== null && (int) ($supplier->amministratore_id ?? 0) === $preferredAdminId) {
|
||||||
|
$score += 300;
|
||||||
|
}
|
||||||
|
if (! empty($supplier->fe_features)) {
|
||||||
|
$score += 150;
|
||||||
|
}
|
||||||
|
if (! empty($supplier->operational_config)) {
|
||||||
|
$score += 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['partita_iva', 'codice_fiscale', 'email', 'pec', 'telefono', 'cellulare', 'iban', 'codice_univoco'] as $field) {
|
||||||
|
if (trim((string) ($supplier->{$field} ?? '')) !== '') {
|
||||||
|
$score += 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$score += ((int) ($this->referenceCounts[(int) $supplier->id] ?? 0)) * 25;
|
||||||
|
|
||||||
|
if ($best === null || $bestScore === null || $score > $bestScore || ($score === $bestScore && (int) $supplier->id < (int) $best->id)) {
|
||||||
|
$best = $supplier;
|
||||||
|
$bestScore = $score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $best ?? $suppliers[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, Fornitore> $duplicates */
|
||||||
|
private function buildMasterPayload(Fornitore $master, array $duplicates): array
|
||||||
|
{
|
||||||
|
$payload = [];
|
||||||
|
|
||||||
|
foreach (['rubrica_id', 'partita_iva', 'codice_fiscale', 'email', 'pec', 'telefono', 'cellulare', 'iban', 'codice_univoco'] as $field) {
|
||||||
|
if (trim((string) ($master->{$field} ?? '')) !== '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($duplicates as $duplicate) {
|
||||||
|
$value = $duplicate->{$field} ?? null;
|
||||||
|
if (trim((string) $value) !== '') {
|
||||||
|
$payload[$field] = $value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$masterFeFeatures = is_array($master->fe_features ?? null) ? $master->fe_features : [];
|
||||||
|
$masterOperationalConfig = is_array($master->operational_config ?? null) ? $master->operational_config : [];
|
||||||
|
|
||||||
|
$legacyCodFornAliases = [];
|
||||||
|
$legacyOldIdAliases = [];
|
||||||
|
$mergedSupplierIds = [];
|
||||||
|
|
||||||
|
if ((int) ($master->cod_forn ?? 0) > 0) {
|
||||||
|
$legacyCodFornAliases[] = (int) $master->cod_forn;
|
||||||
|
}
|
||||||
|
if ((int) ($master->old_id ?? 0) > 0) {
|
||||||
|
$legacyOldIdAliases[] = (int) $master->old_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($duplicates as $duplicate) {
|
||||||
|
if ((int) ($duplicate->cod_forn ?? 0) > 0) {
|
||||||
|
$legacyCodFornAliases[] = (int) $duplicate->cod_forn;
|
||||||
|
}
|
||||||
|
if ((int) ($duplicate->old_id ?? 0) > 0) {
|
||||||
|
$legacyOldIdAliases[] = (int) $duplicate->old_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mergedSupplierIds[] = (int) $duplicate->id;
|
||||||
|
$masterFeFeatures = $this->mergeConfigRecursive($masterFeFeatures, is_array($duplicate->fe_features ?? null) ? $duplicate->fe_features : []);
|
||||||
|
$masterOperationalConfig = $this->mergeConfigRecursive($masterOperationalConfig, is_array($duplicate->operational_config ?? null) ? $duplicate->operational_config : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
$legacyCodFornAliases = array_values(array_unique(array_filter(array_map('intval', array_merge(
|
||||||
|
$legacyCodFornAliases,
|
||||||
|
array_map('intval', (array) ($masterOperationalConfig['legacy_cod_forn_aliases'] ?? []))
|
||||||
|
)))));
|
||||||
|
$legacyOldIdAliases = array_values(array_unique(array_filter(array_map('intval', array_merge(
|
||||||
|
$legacyOldIdAliases,
|
||||||
|
array_map('intval', (array) ($masterOperationalConfig['legacy_old_id_aliases'] ?? []))
|
||||||
|
)))));
|
||||||
|
$mergedSupplierIds = array_values(array_unique(array_filter(array_map('intval', array_merge(
|
||||||
|
$mergedSupplierIds,
|
||||||
|
array_map('intval', (array) ($masterOperationalConfig['merged_supplier_ids'] ?? []))
|
||||||
|
)))));
|
||||||
|
|
||||||
|
$masterOperationalConfig['legacy_cod_forn_aliases'] = $legacyCodFornAliases;
|
||||||
|
$masterOperationalConfig['legacy_old_id_aliases'] = $legacyOldIdAliases;
|
||||||
|
$masterOperationalConfig['merged_supplier_ids'] = $mergedSupplierIds;
|
||||||
|
|
||||||
|
$payload['fe_features'] = $masterFeFeatures;
|
||||||
|
$payload['operational_config'] = $masterOperationalConfig;
|
||||||
|
|
||||||
|
if (trim((string) ($master->note ?? '')) === '' && $duplicates !== []) {
|
||||||
|
$payload['note'] = 'Fornitore canonico dopo bonifica duplicati.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, array{table:string,column:string}> */
|
||||||
|
private function discoverSupplierReferenceColumns(): array
|
||||||
|
{
|
||||||
|
$database = DB::getDatabaseName();
|
||||||
|
$rows = DB::select(
|
||||||
|
"SELECT table_name, column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = ?
|
||||||
|
AND table_name <> 'fornitori'
|
||||||
|
AND column_name IN ('fornitore_id', 'default_fornitore_id', 'assegnato_a_fornitore_id', 'fornitore_esterno_id')
|
||||||
|
ORDER BY table_name, column_name",
|
||||||
|
[$database]
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(function (object $row): ?array {
|
||||||
|
$table = (string) ($row->TABLE_NAME ?? $row->table_name ?? '');
|
||||||
|
$column = (string) ($row->COLUMN_NAME ?? $row->column_name ?? '');
|
||||||
|
if ($table === '' || $column === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['table' => $table, 'column' => $column];
|
||||||
|
}, $rows)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, int> */
|
||||||
|
private function loadSupplierReferenceCounts(): array
|
||||||
|
{
|
||||||
|
$counts = [];
|
||||||
|
foreach ($this->discoverSupplierReferenceColumns() as $reference) {
|
||||||
|
$table = $reference['table'];
|
||||||
|
$column = $reference['column'];
|
||||||
|
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = DB::table($table)
|
||||||
|
->selectRaw($column . ' as supplier_id, COUNT(*) as aggregate_count')
|
||||||
|
->whereNotNull($column)
|
||||||
|
->groupBy($column)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$supplierId = is_numeric($row->supplier_id ?? null) ? (int) $row->supplier_id : 0;
|
||||||
|
if ($supplierId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$counts[$supplierId] = (int) ($counts[$supplierId] ?? 0) + (int) ($row->aggregate_count ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function moveSupplierReferences(int $fromSupplierId, int $toSupplierId): int
|
||||||
|
{
|
||||||
|
if ($fromSupplierId <= 0 || $toSupplierId <= 0 || $fromSupplierId === $toSupplierId) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moved = 0;
|
||||||
|
foreach ($this->referenceColumns as $reference) {
|
||||||
|
$table = $reference['table'];
|
||||||
|
$column = $reference['column'];
|
||||||
|
|
||||||
|
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table === 'fornitore_stabile_impostazioni' && $column === 'fornitore_id') {
|
||||||
|
$moved += $this->mergeFornitoreStabileImpostazioni($fromSupplierId, $toSupplierId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moved += (int) DB::table($table)
|
||||||
|
->where($column, $fromSupplierId)
|
||||||
|
->update([$column => $toSupplierId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mergeFornitoreStabileImpostazioni(int $fromSupplierId, int $toSupplierId): int
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('fornitore_stabile_impostazioni')) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moved = 0;
|
||||||
|
$rows = DB::table('fornitore_stabile_impostazioni')
|
||||||
|
->where('fornitore_id', $fromSupplierId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$target = DB::table('fornitore_stabile_impostazioni')
|
||||||
|
->where('stabile_id', (int) $row->stabile_id)
|
||||||
|
->where('fornitore_id', $toSupplierId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $target) {
|
||||||
|
$moved += (int) DB::table('fornitore_stabile_impostazioni')
|
||||||
|
->where('id', (int) $row->id)
|
||||||
|
->update(['fornitore_id' => $toSupplierId]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasColumn('fornitore_stabile_impostazioni', 'meta')) {
|
||||||
|
$sourceMeta = json_decode((string) ($row->meta ?? '[]'), true);
|
||||||
|
$targetMeta = json_decode((string) ($target->meta ?? '[]'), true);
|
||||||
|
$mergedMeta = $this->mergeConfigRecursive(is_array($targetMeta) ? $targetMeta : [], is_array($sourceMeta) ? $sourceMeta : []);
|
||||||
|
|
||||||
|
DB::table('fornitore_stabile_impostazioni')
|
||||||
|
->where('id', (int) $target->id)
|
||||||
|
->update([
|
||||||
|
'meta' => json_encode($mergedMeta, JSON_UNESCAPED_UNICODE),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('fornitore_stabile_impostazioni')->where('id', (int) $row->id)->delete();
|
||||||
|
$moved++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasRemainingReferences(int $supplierId): bool
|
||||||
|
{
|
||||||
|
foreach ($this->referenceColumns as $reference) {
|
||||||
|
$table = $reference['table'];
|
||||||
|
$column = $reference['column'];
|
||||||
|
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DB::table($table)->where($column, $supplierId)->exists()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $master
|
||||||
|
* @param array<string, mixed> $duplicate
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function mergeConfigRecursive(array $master, array $duplicate): array
|
||||||
|
{
|
||||||
|
foreach ($duplicate as $key => $value) {
|
||||||
|
if (! array_key_exists($key, $master)) {
|
||||||
|
$master[$key] = $value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($master[$key]) && is_array($value)) {
|
||||||
|
$master[$key] = $this->mergeConfigRecursive($master[$key], $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $master;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeNumericIdentity(mixed $value): ?string
|
||||||
|
{
|
||||||
|
$digits = preg_replace('/\D+/', '', trim((string) $value)) ?? '';
|
||||||
|
if ($digits === '' || preg_match('/^0+$/', $digits) === 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strlen($digits) < 11 ? str_pad($digits, 11, '0', STR_PAD_LEFT) : $digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeAlphaNumericIdentity(mixed $value): ?string
|
||||||
|
{
|
||||||
|
$normalized = strtoupper(trim((string) $value));
|
||||||
|
$normalized = preg_replace('/\s+/', '', $normalized) ?? '';
|
||||||
|
if ($normalized === '' || in_array($normalized, ['0', '00', '000', 'ND', 'N/D', 'NULL'], true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,7 +44,7 @@ public function handle(): int
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tableCodes = collect(array_keys($legacyHeaders))->values();
|
$tableCodes = collect(array_keys($legacyHeaders))->values();
|
||||||
$legacyDetails = $this->loadLegacyDettagli($stableCode, $year, $tableCodes);
|
$legacyDetails = $this->loadLegacyDettagli($stableCode, $year, $tableCodes);
|
||||||
$this->assertLegacyVociHaveKnownTables($legacyVoci, $tableCodes);
|
$this->assertLegacyVociHaveKnownTables($legacyVoci, $tableCodes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2828,7 +2828,7 @@ private function stepOperazioni(?int $limit): int
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'tenant_id' => null,
|
'tenant_id' => null,
|
||||||
'gestione_id' => $this->resolveGestioneId($o->anno ?? null, $o->compet ?? null, $o->gestione ?? null),
|
'gestione_id' => $this->resolveGestioneId($o->anno ?? null, $o->compet ?? null, $o->gestione ?? null, $o->n_stra ?? null),
|
||||||
'legacy_id' => $legacy,
|
'legacy_id' => $legacy,
|
||||||
'descrizione' => $o->note ?? ($o->natura ?? 'Operazione'),
|
'descrizione' => $o->note ?? ($o->natura ?? 'Operazione'),
|
||||||
'data_operazione' => $o->dt_spe ?? now()->toDateString(),
|
'data_operazione' => $o->dt_spe ?? now()->toDateString(),
|
||||||
|
|
@ -5782,46 +5782,21 @@ private function resolveGestioneContabileIdForStabile(?int $stabileId, ?string $
|
||||||
return $match?->id;
|
return $match?->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveGestioneId($anno, $compet, $tipo): ?int
|
private function resolveGestioneId($anno, $compet, $tipo, $numeroStraordinaria = null): ?int
|
||||||
{
|
{
|
||||||
if (! Schema::hasTable('gestioni_contabili')) {
|
if (! Schema::hasTable('gestioni_contabili')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize tipo from O/R/S to descriptive if needed
|
$legacyYear = $anno !== null && $anno !== '' ? (string) $anno : null;
|
||||||
$map = ['O' => 'ordinaria', 'R' => 'riscaldamento', 'S' => 'straordinaria'];
|
$stabileId = $this->stabileId ?: $this->resolveStabileId();
|
||||||
$tipo = $map[$tipo ?? 'O'] ?? ($tipo ?: 'ordinaria');
|
|
||||||
$anno = $anno ?: date('Y');
|
|
||||||
$row = DB::table('gestioni_contabili')
|
|
||||||
->where(['anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo])
|
|
||||||
->first();
|
|
||||||
if ($row) {
|
|
||||||
return $row->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In dry-run, avoid creating missing gestione
|
return $this->resolveGestioneContabileIdForStabile(
|
||||||
if ($this->option('dry-run')) {
|
$stabileId,
|
||||||
return null;
|
$legacyYear,
|
||||||
}
|
$tipo !== null ? (string) $tipo : null,
|
||||||
|
is_numeric($numeroStraordinaria) ? (int) $numeroStraordinaria : null,
|
||||||
// Create minimal compatible record satisfying not-null constraints
|
);
|
||||||
$data = [
|
|
||||||
'tenant_id' => 'T1',
|
|
||||||
'anno_gestione' => (int) $anno,
|
|
||||||
'tipo_gestione' => $tipo,
|
|
||||||
'numero_straordinaria' => null,
|
|
||||||
'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno,
|
|
||||||
'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')),
|
|
||||||
'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')),
|
|
||||||
'stato' => 'aperta',
|
|
||||||
'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno,
|
|
||||||
'ultimo_protocollo' => 0,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
];
|
|
||||||
DB::table('gestioni_contabili')->insert($data);
|
|
||||||
$row = DB::table('gestioni_contabili')->where(['anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo])->first();
|
|
||||||
return $row?->id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function dynamicInsert(string $table, array $data): void
|
private function dynamicInsert(string $table, array $data): void
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,21 @@
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
|
||||||
class NetgesconNodeRefreshCommand extends Command
|
class NetgesconNodeRefreshCommand extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'netgescon:node-refresh
|
protected $signature = 'netgescon:node-refresh
|
||||||
{--remote=origin : Remote Git da usare}
|
{--remote=origin : Remote Git da usare}
|
||||||
{--branch=main : Branch da allineare}
|
{--branch=main : Branch da allineare}
|
||||||
|
{--admin-id= : ID amministratore per eventuale backup Drive}
|
||||||
|
{--drive : Carica il backup pre-update anche su Google Drive}
|
||||||
{--force : Continua anche con working tree sporco lato sync}
|
{--force : Continua anche con working tree sporco lato sync}
|
||||||
|
{--require-drive : Fallisce se il backup Drive non viene completato}
|
||||||
|
{--skip-backup : Salta backup pre-update (solo test/debug)}
|
||||||
{--skip-migrate : Salta php artisan migrate --force}
|
{--skip-migrate : Salta php artisan migrate --force}
|
||||||
|
{--skip-maintenance : Salta php artisan down/up (solo test/debug)}
|
||||||
{--skip-view-cache : Salta rebuild view cache}
|
{--skip-view-cache : Salta rebuild view cache}
|
||||||
{--progress-file= : File JSON dove scrivere lo stato avanzamento}';
|
{--progress-file= : File JSON dove scrivere lo stato avanzamento}';
|
||||||
|
|
||||||
|
|
@ -18,110 +25,292 @@ class NetgesconNodeRefreshCommand extends Command
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$remote = trim((string) $this->option('remote')) ?: 'origin';
|
$remote = trim((string) $this->option('remote')) ?: 'origin';
|
||||||
$branch = trim((string) $this->option('branch')) ?: 'main';
|
$branch = trim((string) $this->option('branch')) ?: 'main';
|
||||||
$force = (bool) $this->option('force');
|
$adminId = (int) $this->option('admin-id');
|
||||||
$skipMigrate = (bool) $this->option('skip-migrate');
|
$force = (bool) $this->option('force');
|
||||||
$skipViewCache = (bool) $this->option('skip-view-cache');
|
$driveBackup = (bool) $this->option('drive');
|
||||||
|
$requireDrive = (bool) $this->option('require-drive');
|
||||||
|
$skipBackup = (bool) $this->option('skip-backup');
|
||||||
|
$skipMigrate = (bool) $this->option('skip-migrate');
|
||||||
|
$skipMaintenance = (bool) $this->option('skip-maintenance');
|
||||||
|
$skipViewCache = (bool) $this->option('skip-view-cache');
|
||||||
|
$maintenanceOn = false;
|
||||||
|
$maintenanceError = null;
|
||||||
|
|
||||||
$this->writeProgress(2, 'Avvio refresh nodo da Gitea', 'running');
|
$this->writeProgress(2, 'Avvio refresh nodo da Gitea', 'running');
|
||||||
|
|
||||||
$gitParams = [
|
$fullOutput = [];
|
||||||
'--remote' => $remote,
|
|
||||||
'--branch' => $branch,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($force) {
|
if (! $skipBackup) {
|
||||||
$gitParams['--force'] = true;
|
$this->writeProgress(8, 'Backup pre-update in corso', 'running');
|
||||||
|
$backupArgs = [
|
||||||
|
'--differential' => true,
|
||||||
|
'--tag' => 'node-refresh-' . now()->format('Ymd-His'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($driveBackup) {
|
||||||
|
$backupArgs['--drive'] = true;
|
||||||
|
|
||||||
|
if ($adminId > 0) {
|
||||||
|
$backupArgs['--admin-id'] = (string) $adminId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requireDrive) {
|
||||||
|
$backupArgs['--require-drive'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$backupExit = Artisan::call('netgescon:preupdate-backup', $backupArgs);
|
||||||
|
$backupOutput = trim((string) Artisan::output());
|
||||||
|
$fullOutput[] = 'NODE REFRESH: php artisan netgescon:preupdate-backup';
|
||||||
|
$fullOutput[] = $backupOutput;
|
||||||
|
|
||||||
|
if ($backupExit !== 0) {
|
||||||
|
$this->error('Backup pre-update fallito.');
|
||||||
|
$this->line($backupOutput);
|
||||||
|
$this->writeProgress(100, 'Backup pre-update fallito', 'failed', $backupExit);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info("Sync Git da {$remote}/{$branch}...");
|
if (! $skipMaintenance) {
|
||||||
$this->writeProgress(15, 'Sync Git da Gitea in corso', 'running');
|
$this->writeProgress(16, 'Blocco accessi e maintenance mode', 'running');
|
||||||
$gitExit = Artisan::call('netgescon:git-sync', $gitParams);
|
$maintenanceExit = Artisan::call('down', ['--retry' => 60, '--refresh' => 15]);
|
||||||
$gitOutput = trim((string) Artisan::output());
|
$maintenanceOutput = trim((string) Artisan::output());
|
||||||
|
$fullOutput[] = 'NODE REFRESH: php artisan down --retry=60 --refresh=15';
|
||||||
|
$fullOutput[] = $maintenanceOutput;
|
||||||
|
|
||||||
if ($gitExit !== 0) {
|
if ($maintenanceExit !== 0) {
|
||||||
$this->error('Sync Git fallita.');
|
$this->error('Impossibile attivare la maintenance mode.');
|
||||||
$this->line($gitOutput);
|
$this->line($maintenanceOutput);
|
||||||
$this->writeProgress(100, 'Sync Git fallita', 'failed', $gitExit);
|
$this->writeProgress(100, 'Maintenance mode non attivata', 'failed', $maintenanceExit);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenanceOn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$gitParams = [
|
||||||
|
'--remote' => $remote,
|
||||||
|
'--branch' => $branch,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($force) {
|
||||||
|
$gitParams['--force'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Sync Git da {$remote}/{$branch}...");
|
||||||
|
$this->writeProgress(28, 'Sync Git da Gitea in corso', 'running');
|
||||||
|
$gitExit = Artisan::call('netgescon:git-sync', $gitParams);
|
||||||
|
$gitOutput = trim((string) Artisan::output());
|
||||||
|
|
||||||
|
if ($gitExit !== 0) {
|
||||||
|
$this->error('Sync Git fallita.');
|
||||||
|
$this->line($gitOutput);
|
||||||
|
$this->writeProgress(100, 'Sync Git fallita', 'failed', $gitExit);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullOutput[] = 'NODE REFRESH: netgescon:git-sync';
|
||||||
|
$fullOutput[] = $gitOutput;
|
||||||
|
|
||||||
|
$this->writeProgress(46, 'Composer install e autoload', 'running');
|
||||||
|
$composerResult = $this->runShellStep([
|
||||||
|
'composer',
|
||||||
|
'install',
|
||||||
|
'--no-interaction',
|
||||||
|
'--prefer-dist',
|
||||||
|
'--optimize-autoloader',
|
||||||
|
'--no-progress',
|
||||||
|
]);
|
||||||
|
$fullOutput[] = 'NODE REFRESH: composer install --no-interaction --prefer-dist --optimize-autoloader --no-progress';
|
||||||
|
$fullOutput[] = $composerResult['output'];
|
||||||
|
|
||||||
|
if (! $composerResult['success']) {
|
||||||
|
$this->error('Composer install fallito.');
|
||||||
|
$this->line($composerResult['output']);
|
||||||
|
$this->writeProgress(100, 'Composer install fallito', 'failed', $composerResult['exit_code']);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writeProgress(58, 'Build frontend se disponibile', 'running');
|
||||||
|
$frontendBuild = $this->attemptFrontendBuild();
|
||||||
|
if ($frontendBuild !== null) {
|
||||||
|
$fullOutput[] = $frontendBuild['label'];
|
||||||
|
$fullOutput[] = $frontendBuild['output'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Pulizia cache applicative...');
|
||||||
|
$this->writeProgress(68, 'Pulizia cache applicative', 'running');
|
||||||
|
$optimizeExit = Artisan::call('optimize:clear');
|
||||||
|
$optimizeOutput = trim((string) Artisan::output());
|
||||||
|
$fullOutput[] = 'NODE REFRESH: php artisan optimize:clear';
|
||||||
|
$fullOutput[] = $optimizeOutput;
|
||||||
|
|
||||||
|
if ($optimizeExit !== 0) {
|
||||||
|
$this->error('optimize:clear fallito.');
|
||||||
|
$this->line($optimizeOutput);
|
||||||
|
$this->writeProgress(100, 'Pulizia cache fallita', 'failed', $optimizeExit);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $skipMigrate) {
|
||||||
|
$this->info('Esecuzione migrate --force...');
|
||||||
|
$this->writeProgress(76, 'Applicazione migration', 'running');
|
||||||
|
$migrateExit = Artisan::call('migrate', ['--force' => true]);
|
||||||
|
$migrateOutput = trim((string) Artisan::output());
|
||||||
|
$fullOutput[] = 'NODE REFRESH: php artisan migrate --force';
|
||||||
|
$fullOutput[] = $migrateOutput;
|
||||||
|
|
||||||
|
if ($migrateExit !== 0) {
|
||||||
|
$this->error('Migration fallita.');
|
||||||
|
$this->line($migrateOutput);
|
||||||
|
$this->writeProgress(100, 'Migration fallita', 'failed', $migrateExit);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $skipViewCache) {
|
||||||
|
$this->info('Ricostruzione view cache...');
|
||||||
|
$this->writeProgress(84, 'Ricostruzione view cache', 'running');
|
||||||
|
|
||||||
|
$viewClearExit = Artisan::call('view:clear');
|
||||||
|
$viewClearOutput = trim((string) Artisan::output());
|
||||||
|
$fullOutput[] = 'NODE REFRESH: php artisan view:clear';
|
||||||
|
$fullOutput[] = $viewClearOutput;
|
||||||
|
|
||||||
|
if ($viewClearExit !== 0) {
|
||||||
|
$this->error('view:clear fallito.');
|
||||||
|
$this->line($viewClearOutput);
|
||||||
|
$this->writeProgress(100, 'view:clear fallito', 'failed', $viewClearExit);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$viewCacheExit = Artisan::call('view:cache');
|
||||||
|
$viewCacheOutput = trim((string) Artisan::output());
|
||||||
|
$fullOutput[] = 'NODE REFRESH: php artisan view:cache';
|
||||||
|
$fullOutput[] = $viewCacheOutput;
|
||||||
|
|
||||||
|
if ($viewCacheExit !== 0) {
|
||||||
|
$this->error('view:cache fallita.');
|
||||||
|
$this->line($viewCacheOutput);
|
||||||
|
$this->writeProgress(100, 'view:cache fallita', 'failed', $viewCacheExit);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writeProgress(91, 'Rigenerazione cache applicative finali', 'running');
|
||||||
|
foreach ([
|
||||||
|
'config:cache' => 'NODE REFRESH: php artisan config:cache',
|
||||||
|
'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());
|
||||||
|
$fullOutput[] = $label;
|
||||||
|
$fullOutput[] = $output;
|
||||||
|
|
||||||
|
if ($exit !== 0) {
|
||||||
|
$this->warn($artisanCommand . ' non completato: ' . ($output !== '' ? $output : 'uscita ' . $exit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line(implode(PHP_EOL . PHP_EOL, array_filter($fullOutput, static fn(string $chunk): bool => trim($chunk) !== '')));
|
||||||
|
$this->writeProgress(98, 'Riapertura nodo e chiusura refresh', 'running');
|
||||||
|
} finally {
|
||||||
|
if ($maintenanceOn) {
|
||||||
|
$upExit = Artisan::call('up');
|
||||||
|
$maintenanceUp = trim((string) Artisan::output());
|
||||||
|
$fullOutput[] = 'NODE REFRESH: php artisan up';
|
||||||
|
$fullOutput[] = $maintenanceUp;
|
||||||
|
|
||||||
|
if ($upExit !== 0) {
|
||||||
|
$maintenanceError = $maintenanceUp !== '' ? $maintenanceUp : 'php artisan up fallito';
|
||||||
|
}
|
||||||
|
|
||||||
|
$lockPath = storage_path('framework/down');
|
||||||
|
if (is_file($lockPath)) {
|
||||||
|
@unlink($lockPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($maintenanceError !== null) {
|
||||||
|
$this->error('Refresh completato ma il nodo non e stato riaperto automaticamente.');
|
||||||
|
$this->line($maintenanceError);
|
||||||
|
$this->writeProgress(100, 'Refresh completato ma nodo ancora in maintenance', 'failed', 1);
|
||||||
|
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fullOutput = [
|
|
||||||
'NODE REFRESH: netgescon:git-sync',
|
|
||||||
$gitOutput,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (! $skipMigrate) {
|
|
||||||
$this->info('Esecuzione migrate --force...');
|
|
||||||
$this->writeProgress(55, 'Applicazione migration', 'running');
|
|
||||||
$migrateExit = Artisan::call('migrate', ['--force' => true]);
|
|
||||||
$migrateOutput = trim((string) Artisan::output());
|
|
||||||
$fullOutput[] = 'NODE REFRESH: php artisan migrate --force';
|
|
||||||
$fullOutput[] = $migrateOutput;
|
|
||||||
|
|
||||||
if ($migrateExit !== 0) {
|
|
||||||
$this->error('Migration fallita.');
|
|
||||||
$this->line($migrateOutput);
|
|
||||||
$this->writeProgress(100, 'Migration fallita', 'failed', $migrateExit);
|
|
||||||
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('Pulizia cache applicative...');
|
|
||||||
$this->writeProgress(72, 'Pulizia cache applicative', 'running');
|
|
||||||
$optimizeExit = Artisan::call('optimize:clear');
|
|
||||||
$optimizeOutput = trim((string) Artisan::output());
|
|
||||||
$fullOutput[] = 'NODE REFRESH: php artisan optimize:clear';
|
|
||||||
$fullOutput[] = $optimizeOutput;
|
|
||||||
|
|
||||||
if ($optimizeExit !== 0) {
|
|
||||||
$this->error('optimize:clear fallito.');
|
|
||||||
$this->line($optimizeOutput);
|
|
||||||
$this->writeProgress(100, 'Pulizia cache fallita', 'failed', $optimizeExit);
|
|
||||||
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $skipViewCache) {
|
|
||||||
$this->info('Ricostruzione view cache...');
|
|
||||||
$this->writeProgress(88, 'Ricostruzione view cache', 'running');
|
|
||||||
|
|
||||||
$viewClearExit = Artisan::call('view:clear');
|
|
||||||
$viewClearOutput = trim((string) Artisan::output());
|
|
||||||
$fullOutput[] = 'NODE REFRESH: php artisan view:clear';
|
|
||||||
$fullOutput[] = $viewClearOutput;
|
|
||||||
|
|
||||||
if ($viewClearExit !== 0) {
|
|
||||||
$this->error('view:clear fallito.');
|
|
||||||
$this->line($viewClearOutput);
|
|
||||||
$this->writeProgress(100, 'view:clear fallito', 'failed', $viewClearExit);
|
|
||||||
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$viewCacheExit = Artisan::call('view:cache');
|
|
||||||
$viewCacheOutput = trim((string) Artisan::output());
|
|
||||||
$fullOutput[] = 'NODE REFRESH: php artisan view:cache';
|
|
||||||
$fullOutput[] = $viewCacheOutput;
|
|
||||||
|
|
||||||
if ($viewCacheExit !== 0) {
|
|
||||||
$this->error('view:cache fallita.');
|
|
||||||
$this->line($viewCacheOutput);
|
|
||||||
$this->writeProgress(100, 'view:cache fallita', 'failed', $viewCacheExit);
|
|
||||||
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->line(implode(PHP_EOL . PHP_EOL, array_filter($fullOutput, static fn(string $chunk): bool => trim($chunk) !== '')));
|
|
||||||
$this->writeProgress(100, 'Refresh nodo completato', 'completed', 0);
|
$this->writeProgress(100, 'Refresh nodo completato', 'completed', 0);
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array{success:bool,exit_code:int,output:string} */
|
||||||
|
private function runShellStep(array $command): array
|
||||||
|
{
|
||||||
|
$result = Process::path(base_path())
|
||||||
|
->timeout(3600)
|
||||||
|
->run($command);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => $result->successful(),
|
||||||
|
'exit_code' => $result->exitCode(),
|
||||||
|
'output' => trim((string) ($result->output() !== '' ? $result->output() : $result->errorOutput())),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{label:string,output:string}|null */
|
||||||
|
private function attemptFrontendBuild(): ?array
|
||||||
|
{
|
||||||
|
if (! is_file(base_path('package.json'))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nodeCheck = $this->runShellStep(['bash', '-lc', 'command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1']);
|
||||||
|
if (! $nodeCheck['success']) {
|
||||||
|
return [
|
||||||
|
'label' => 'NODE REFRESH: frontend build skipped',
|
||||||
|
'output' => 'node/npm non disponibili sul nodo: build frontend saltata senza bloccare il deploy.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$installCommand = is_file(base_path('package-lock.json'))
|
||||||
|
? ['npm', 'ci', '--no-audit', '--no-fund']
|
||||||
|
: ['npm', 'install', '--no-audit', '--no-fund'];
|
||||||
|
|
||||||
|
$installResult = $this->runShellStep($installCommand);
|
||||||
|
if (! $installResult['success']) {
|
||||||
|
return [
|
||||||
|
'label' => 'NODE REFRESH: npm install/ci',
|
||||||
|
'output' => $installResult['output'] !== ''
|
||||||
|
? $installResult['output']
|
||||||
|
: 'Installazione dipendenze frontend fallita; build saltata.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$buildResult = $this->runShellStep(['npm', 'run', 'build']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'NODE REFRESH: npm run build',
|
||||||
|
'output' => $buildResult['output'] !== ''
|
||||||
|
? $buildResult['output']
|
||||||
|
: ($buildResult['success'] ? 'Build frontend completata.' : 'Build frontend fallita.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void
|
private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void
|
||||||
{
|
{
|
||||||
$path = trim((string) $this->option('progress-file'));
|
$path = trim((string) $this->option('progress-file'));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Fornitore;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
@ -70,24 +71,7 @@ public function handle(): int
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$fornitoreMap = [];
|
$fornitoreMap = $this->buildFornitoreMapByLegacyCode($codes);
|
||||||
if (! empty($codes) && Schema::hasTable('fornitori')) {
|
|
||||||
$fornitori = DB::table('fornitori')
|
|
||||||
->whereIn('old_id', $codes)
|
|
||||||
->get(['id', 'old_id']);
|
|
||||||
|
|
||||||
foreach ($fornitori as $fornitore) {
|
|
||||||
$raw = trim((string) $fornitore->old_id);
|
|
||||||
$noZero = ltrim($raw, '0');
|
|
||||||
|
|
||||||
if ($raw !== '') {
|
|
||||||
$fornitoreMap[$raw] = (int) $fornitore->id;
|
|
||||||
}
|
|
||||||
if ($noZero !== '') {
|
|
||||||
$fornitoreMap[$noZero] = (int) $fornitore->id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$created = 0;
|
$created = 0;
|
||||||
$updated = 0;
|
$updated = 0;
|
||||||
|
|
@ -415,13 +399,15 @@ private function resolveFatturaElettronicaId(int $stabileId, int $fornitoreId, s
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreId);
|
||||||
|
|
||||||
$base = DB::table('fatture_elettroniche')
|
$base = DB::table('fatture_elettroniche')
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
|
->whereIn('fornitore_id', $fornitoreIds)
|
||||||
->whereDate('data_fattura', $dataDocumento)
|
->whereDate('data_fattura', $dataDocumento)
|
||||||
->whereRaw('LOWER(TRIM(numero_fattura)) = ?', [$numeroNorm]);
|
->whereRaw('LOWER(TRIM(numero_fattura)) = ?', [$numeroNorm]);
|
||||||
|
|
||||||
$strict = (clone $base)
|
$strict = (clone $base)
|
||||||
->where('fornitore_id', $fornitoreId)
|
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->value('id');
|
->value('id');
|
||||||
|
|
||||||
|
|
@ -444,4 +430,118 @@ private function resolveFatturaElettronicaId(int $stabileId, int $fornitoreId, s
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<int, string> $codes
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
private function buildFornitoreMapByLegacyCode(array $codes): array
|
||||||
|
{
|
||||||
|
if ($codes === [] || ! Schema::hasTable('fornitori')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
$suppliers = Fornitore::query()->get(['id', 'cod_forn', 'old_id', 'operational_config']);
|
||||||
|
|
||||||
|
foreach ($suppliers as $supplier) {
|
||||||
|
$directCodes = [];
|
||||||
|
if ((int) ($supplier->cod_forn ?? 0) > 0) {
|
||||||
|
$directCodes[] = (string) (int) $supplier->cod_forn;
|
||||||
|
}
|
||||||
|
if ((int) ($supplier->old_id ?? 0) > 0) {
|
||||||
|
$directCodes[] = (string) (int) $supplier->old_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operationalConfig = is_array($supplier->operational_config ?? null) ? $supplier->operational_config : [];
|
||||||
|
$aliasCodes = array_map(
|
||||||
|
fn($value): string => (string) (int) $value,
|
||||||
|
array_filter((array) ($operationalConfig['legacy_cod_forn_aliases'] ?? []), fn($value): bool => is_numeric($value) && (int) $value > 0)
|
||||||
|
);
|
||||||
|
$aliasOldIds = array_map(
|
||||||
|
fn($value): string => (string) (int) $value,
|
||||||
|
array_filter((array) ($operationalConfig['legacy_old_id_aliases'] ?? []), fn($value): bool => is_numeric($value) && (int) $value > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (array_merge($directCodes, $aliasCodes, $aliasOldIds) as $code) {
|
||||||
|
$this->registerFornitoreCodeMap($map, $code, (int) $supplier->id, in_array($code, $directCodes, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($codes as $code) {
|
||||||
|
$trim = trim((string) $code);
|
||||||
|
if ($trim === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$noZero = ltrim($trim, '0');
|
||||||
|
if ($noZero !== '' && isset($map[$noZero]) && ! isset($map[$trim])) {
|
||||||
|
$map[$trim] = $map[$noZero];
|
||||||
|
}
|
||||||
|
if ($noZero !== '' && isset($map[$trim]) && ! isset($map[$noZero])) {
|
||||||
|
$map[$noZero] = $map[$trim];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, int> $map */
|
||||||
|
private function registerFornitoreCodeMap(array &$map, string $code, int $supplierId, bool $isDirectCode): void
|
||||||
|
{
|
||||||
|
$trim = trim($code);
|
||||||
|
if ($supplierId <= 0 || $trim === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keys = array_values(array_unique(array_filter([$trim, ltrim($trim, '0')], fn($value): bool => $value !== '')));
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (! isset($map[$key]) || $isDirectCode) {
|
||||||
|
$map[$key] = $supplierId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, int> */
|
||||||
|
private function resolveEquivalentFornitoreIds(int $fornitoreId): array
|
||||||
|
{
|
||||||
|
if ($fornitoreId <= 0 || ! Schema::hasTable('fornitori')) {
|
||||||
|
return [$fornitoreId];
|
||||||
|
}
|
||||||
|
|
||||||
|
$supplier = Fornitore::query()->find($fornitoreId, ['id', 'rubrica_id', 'partita_iva', 'codice_fiscale', 'old_id']);
|
||||||
|
if (! $supplier) {
|
||||||
|
return [$fornitoreId];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = Fornitore::query()->where('id', $fornitoreId);
|
||||||
|
|
||||||
|
$rubricaId = (int) ($supplier->rubrica_id ?? 0);
|
||||||
|
if ($rubricaId > 0) {
|
||||||
|
$query->orWhere('rubrica_id', $rubricaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$partitaIva = trim((string) ($supplier->partita_iva ?? ''));
|
||||||
|
if ($partitaIva !== '') {
|
||||||
|
$query->orWhere('partita_iva', $partitaIva);
|
||||||
|
}
|
||||||
|
|
||||||
|
$codiceFiscale = trim((string) ($supplier->codice_fiscale ?? ''));
|
||||||
|
if ($codiceFiscale !== '') {
|
||||||
|
$query->orWhere('codice_fiscale', $codiceFiscale);
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldId = (int) ($supplier->old_id ?? 0);
|
||||||
|
if ($oldId > 0) {
|
||||||
|
$query->orWhere('old_id', $oldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = $query->pluck('id')
|
||||||
|
->map(fn($value) => (int) $value)
|
||||||
|
->filter(fn($value) => $value > 0)
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return $ids !== [] ? $ids : [$fornitoreId];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,8 +185,8 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$existing = $matchingSchede->first();
|
$existing = $matchingSchede->first();
|
||||||
$duplicateSchede = $matchingSchede->slice(1);
|
$duplicateSchede = $matchingSchede->slice(1);
|
||||||
$duplicateSchedaIds = $duplicateSchede
|
$duplicateSchedaIds = $duplicateSchede
|
||||||
->pluck('id')
|
->pluck('id')
|
||||||
->filter(fn($value): bool => is_numeric($value) && (int) $value > 0)
|
->filter(fn($value): bool => is_numeric($value) && (int) $value > 0)
|
||||||
|
|
@ -383,7 +383,7 @@ private function consolidateLegacyDuplicates(int $amministratoreId, ?int $fornit
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var AssistenzaTecnorepairScheda $canonical */
|
/** @var AssistenzaTecnorepairScheda $canonical */
|
||||||
$canonical = $matchingSchede->first();
|
$canonical = $matchingSchede->first();
|
||||||
$duplicateSchede = $matchingSchede->slice(1);
|
$duplicateSchede = $matchingSchede->slice(1);
|
||||||
|
|
||||||
foreach ($duplicateSchede as $duplicateScheda) {
|
foreach ($duplicateSchede as $duplicateScheda) {
|
||||||
|
|
|
||||||
|
|
@ -614,24 +614,24 @@ private function loadAffitti(): void
|
||||||
return [$a->id => $label];
|
return [$a->id => $label];
|
||||||
})->all();
|
})->all();
|
||||||
$this->affittiList = $rows->map(fn(AffittoImmobile $a) => [
|
$this->affittiList = $rows->map(fn(AffittoImmobile $a) => [
|
||||||
'warnings' => $this->buildAffittoWarnings($a),
|
'warnings' => $this->buildAffittoWarnings($a),
|
||||||
'id' => $a->id,
|
'id' => $a->id,
|
||||||
'codice_appartamento' => $a->codice_appartamento,
|
'codice_appartamento' => $a->codice_appartamento,
|
||||||
'descrizione_immobile' => $a->descrizione_immobile,
|
'descrizione_immobile' => $a->descrizione_immobile,
|
||||||
'nome_inquilino' => $a->nome_inquilino,
|
'nome_inquilino' => $a->nome_inquilino,
|
||||||
'proprietario_nome' => $a->proprietario_nome,
|
'proprietario_nome' => $a->proprietario_nome,
|
||||||
'inizio_contratto' => $a->inizio_contratto,
|
'inizio_contratto' => $a->inizio_contratto,
|
||||||
'presa_in_carico_operativa_dal' => $a->presa_in_carico_operativa_dal,
|
'presa_in_carico_operativa_dal' => $a->presa_in_carico_operativa_dal,
|
||||||
'prossima_registrazione' => $a->prossima_registrazione,
|
'prossima_registrazione' => $a->prossima_registrazione,
|
||||||
'tipo_riga' => $a->tipo_riga,
|
'tipo_riga' => $a->tipo_riga,
|
||||||
'stabile_id' => $a->stabile_id,
|
'stabile_id' => $a->stabile_id,
|
||||||
'dovuti_totale' => (float) ($a->dovuti_totale ?? 0),
|
'dovuti_totale' => (float) ($a->dovuti_totale ?? 0),
|
||||||
'pagati_totale' => (float) ($a->pagati_totale ?? 0),
|
'pagati_totale' => (float) ($a->pagati_totale ?? 0),
|
||||||
'saldo_totale' => (float) ($a->dovuti_totale ?? 0) - (float) ($a->pagati_totale ?? 0),
|
'saldo_totale' => (float) ($a->dovuti_totale ?? 0) - (float) ($a->pagati_totale ?? 0),
|
||||||
'obsoleto' => (bool) ($a->obsoleto ?? false),
|
'obsoleto' => (bool) ($a->obsoleto ?? false),
|
||||||
'obsoleto_dal' => $a->obsoleto_dal,
|
'obsoleto_dal' => $a->obsoleto_dal,
|
||||||
'stabile' => $a->stabile?->codice_univoco ?: ($a->stabile?->codice_stabile ?: $a->codice_stabile_legacy),
|
'stabile' => $a->stabile?->codice_univoco ?: ($a->stabile?->codice_stabile ?: $a->codice_stabile_legacy),
|
||||||
'stabile_nome' => $a->stabile?->denominazione,
|
'stabile_nome' => $a->stabile?->denominazione,
|
||||||
])->all();
|
])->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
470
app/Filament/Pages/Condomini/ContrattiHub.php
Normal file
470
app/Filament/Pages/Condomini/ContrattiHub.php
Normal file
|
|
@ -0,0 +1,470 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Filament\Pages\Condomini;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Strumenti\DocumentiArchivio;
|
||||||
|
use App\Models\Documento;
|
||||||
|
use App\Models\Fornitore;
|
||||||
|
use App\Models\Scadenza;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Models\StabileContratto;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Support\StabileContext;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class ContrattiHub extends Page
|
||||||
|
{
|
||||||
|
protected static ?string $navigationLabel = 'HUB Contratti';
|
||||||
|
|
||||||
|
protected static ?string $title = 'HUB Contratti';
|
||||||
|
|
||||||
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
|
||||||
|
|
||||||
|
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 34;
|
||||||
|
|
||||||
|
protected static ?string $slug = 'condomini/contratti-hub';
|
||||||
|
|
||||||
|
protected string $view = 'filament.pages.condomini.contratti-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('registra_contratto')
|
||||||
|
->label('Registra contratto')
|
||||||
|
->icon('heroicon-o-plus')
|
||||||
|
->form($this->getContrattoFormSchema())
|
||||||
|
->action(function (array $data): void {
|
||||||
|
$this->createContratto($data);
|
||||||
|
}),
|
||||||
|
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, 'attivi' => 0, 'scadenze' => 0, 'documenti' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = StabileContratto::query()->where('stabile_id', $stabileId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'totale' => (clone $query)->count(),
|
||||||
|
'attivi' => (clone $query)->where('stato', 'attivo')->count(),
|
||||||
|
'scadenze' => Scadenza::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->whereNotNull('stabile_contratto_id')
|
||||||
|
->whereDate('data_scadenza', '>=', now()->toDateString())
|
||||||
|
->whereDate('data_scadenza', '<=', now()->addDays(45)->toDateString())
|
||||||
|
->count(),
|
||||||
|
'documenti' => Documento::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('tipo_documento', 'contratto')
|
||||||
|
->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContrattiRowsProperty(): Collection
|
||||||
|
{
|
||||||
|
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return StabileContratto::query()
|
||||||
|
->with(['fornitore:id,ragione_sociale,nome,cognome', 'documentoPrincipale:id,nome,numero_protocollo'])
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->orderByRaw("CASE WHEN stato = 'attivo' THEN 0 WHEN stato = 'in_revisione' THEN 1 ELSE 2 END")
|
||||||
|
->orderByDesc('decorrenza_dal')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpcomingScadenzeProperty(): Collection
|
||||||
|
{
|
||||||
|
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scadenza::query()
|
||||||
|
->with('stabileContratto:id,titolo,tipo_contratto')
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->whereNotNull('stabile_contratto_id')
|
||||||
|
->whereDate('data_scadenza', '>=', now()->subDays(7)->toDateString())
|
||||||
|
->orderBy('data_scadenza')
|
||||||
|
->limit(10)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getContrattoFormSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Select::make('stabile_id')
|
||||||
|
->label('Stabile')
|
||||||
|
->options(fn(): array=> $this->getStabiliOptions())
|
||||||
|
->default(fn(): ?int => $this->stabileAttivo?->id)
|
||||||
|
->searchable()
|
||||||
|
->required(),
|
||||||
|
Select::make('fornitore_id')
|
||||||
|
->label('Fornitore / manutentore')
|
||||||
|
->options(fn(): array=> $this->getFornitoriOptions())
|
||||||
|
->searchable()
|
||||||
|
->nullable(),
|
||||||
|
Select::make('tipo_contratto')
|
||||||
|
->label('Tipo contratto')
|
||||||
|
->options($this->getTipoContrattoOptions())
|
||||||
|
->required(),
|
||||||
|
TextInput::make('titolo')
|
||||||
|
->label('Titolo')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('servizio_label')
|
||||||
|
->label('Servizio / utenza')
|
||||||
|
->placeholder('Es. Ascensore scala A, Acqua potabile, Luce comune')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('codice_contratto')
|
||||||
|
->label('Codice contratto')
|
||||||
|
->maxLength(80),
|
||||||
|
TextInput::make('riferimento_esterno')
|
||||||
|
->label('Riferimento esterno')
|
||||||
|
->placeholder('Es. numero cliente, riferimento pratica, ID POD/PDR principale')
|
||||||
|
->maxLength(120),
|
||||||
|
DatePicker::make('data_stipula')
|
||||||
|
->label('Data stipula'),
|
||||||
|
DatePicker::make('decorrenza_dal')
|
||||||
|
->label('Decorrenza dal')
|
||||||
|
->required(),
|
||||||
|
DatePicker::make('decorrenza_al')
|
||||||
|
->label('Decorrenza al'),
|
||||||
|
Select::make('frequenza_scadenze')
|
||||||
|
->label('Frequenza scadenze')
|
||||||
|
->options($this->getFrequenzaOptions())
|
||||||
|
->default('annuale')
|
||||||
|
->required(),
|
||||||
|
TextInput::make('giorno_scadenza')
|
||||||
|
->label('Giorno scadenza')
|
||||||
|
->numeric()
|
||||||
|
->minValue(1)
|
||||||
|
->maxValue(28),
|
||||||
|
TextInput::make('giorni_preavviso')
|
||||||
|
->label('Giorni preavviso')
|
||||||
|
->numeric()
|
||||||
|
->default(30)
|
||||||
|
->minValue(0)
|
||||||
|
->maxValue(365),
|
||||||
|
Toggle::make('rinnovo_automatico')
|
||||||
|
->label('Rinnovo automatico')
|
||||||
|
->default(false),
|
||||||
|
TextInput::make('importo_periodico')
|
||||||
|
->label('Importo periodico')
|
||||||
|
->numeric()
|
||||||
|
->step(0.01)
|
||||||
|
->minValue(0),
|
||||||
|
Select::make('documento_principale_id')
|
||||||
|
->label('Documento principale')
|
||||||
|
->options(fn(callable $get): array=> $this->getDocumentiContrattoOptions((int) ($get('stabile_id') ?: 0)))
|
||||||
|
->searchable()
|
||||||
|
->nullable(),
|
||||||
|
Repeater::make('codici_collegati')
|
||||||
|
->label('Codici collegati')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('chiave')
|
||||||
|
->label('Chiave')
|
||||||
|
->placeholder('Es. POD, PDR, matricola_contatore, codice_cliente, utenza, matricola_ascensore')
|
||||||
|
->required(),
|
||||||
|
TextInput::make('valore')
|
||||||
|
->label('Valore')
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->addActionLabel('Aggiungi codice')
|
||||||
|
->defaultItems(0)
|
||||||
|
->columns(2),
|
||||||
|
Textarea::make('note')
|
||||||
|
->label('Note')
|
||||||
|
->rows(4),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createContratto(array $data): void
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
Notification::make()->title('Utente non valido')->danger()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stabileId = (int) ($data['stabile_id'] ?? 0);
|
||||||
|
$stabile = StabileContext::accessibleStabili($user)->firstWhere('id', $stabileId);
|
||||||
|
if (! $stabile instanceof Stabile) {
|
||||||
|
Notification::make()->title('Stabile non valido')->danger()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contract = StabileContratto::query()->create([
|
||||||
|
'stabile_id' => $stabileId,
|
||||||
|
'fornitore_id' => isset($data['fornitore_id']) && is_numeric($data['fornitore_id']) ? (int) $data['fornitore_id'] : null,
|
||||||
|
'documento_principale_id' => isset($data['documento_principale_id']) && is_numeric($data['documento_principale_id']) ? (int) $data['documento_principale_id'] : null,
|
||||||
|
'titolo' => trim((string) ($data['titolo'] ?? '')),
|
||||||
|
'tipo_contratto' => trim((string) ($data['tipo_contratto'] ?? 'altro')),
|
||||||
|
'categoria_impianto' => trim((string) ($data['tipo_contratto'] ?? 'altro')),
|
||||||
|
'servizio_label' => $this->nullableString($data['servizio_label'] ?? null),
|
||||||
|
'codice_contratto' => $this->nullableString($data['codice_contratto'] ?? null),
|
||||||
|
'riferimento_esterno' => $this->nullableString($data['riferimento_esterno'] ?? null),
|
||||||
|
'stato' => 'attivo',
|
||||||
|
'data_stipula' => $data['data_stipula'] ?? null,
|
||||||
|
'decorrenza_dal' => $data['decorrenza_dal'] ?? null,
|
||||||
|
'decorrenza_al' => $data['decorrenza_al'] ?? null,
|
||||||
|
'frequenza_scadenze' => $this->nullableString($data['frequenza_scadenze'] ?? null),
|
||||||
|
'giorno_scadenza' => isset($data['giorno_scadenza']) && is_numeric($data['giorno_scadenza']) ? (int) $data['giorno_scadenza'] : null,
|
||||||
|
'giorni_preavviso' => isset($data['giorni_preavviso']) && is_numeric($data['giorni_preavviso']) ? (int) $data['giorni_preavviso'] : 30,
|
||||||
|
'rinnovo_automatico' => (bool) ($data['rinnovo_automatico'] ?? false),
|
||||||
|
'importo_periodico' => isset($data['importo_periodico']) && is_numeric($data['importo_periodico']) ? (float) $data['importo_periodico'] : null,
|
||||||
|
'valuta' => 'EUR',
|
||||||
|
'codici_collegati' => $this->normalizeKeyValueRows($data['codici_collegati'] ?? []),
|
||||||
|
'dati_contratto' => [
|
||||||
|
'origine' => 'hub_contratti_stabile',
|
||||||
|
'documenti_url' => DocumentiArchivio::getUrl(panel: 'admin-filament'),
|
||||||
|
],
|
||||||
|
'note' => $this->nullableString($data['note'] ?? null),
|
||||||
|
'created_by' => (int) $user->id,
|
||||||
|
'updated_by' => (int) $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->syncScadenzeContratto($contract);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Contratto registrato')
|
||||||
|
->body('Scadenze programmate: ' . $contract->scadenze()->count())
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function syncScadenzeContratto(StabileContratto $contract): void
|
||||||
|
{
|
||||||
|
$contract->scadenze()->delete();
|
||||||
|
|
||||||
|
foreach ($this->buildScheduleDates($contract) as $date) {
|
||||||
|
$popupDal = $date->copy()->subDays(max(0, (int) ($contract->giorni_preavviso ?? 0)));
|
||||||
|
|
||||||
|
Scadenza::query()->create([
|
||||||
|
'stabile_id' => (int) $contract->stabile_id,
|
||||||
|
'stabile_contratto_id' => (int) $contract->id,
|
||||||
|
'descrizione_sintetica' => $contract->titolo,
|
||||||
|
'data_scadenza' => $date->toDateString(),
|
||||||
|
'natura' => 'contratto_stabile',
|
||||||
|
'natura_label' => 'Scadenza contratto',
|
||||||
|
'gestione_tipo' => $contract->tipo_contratto,
|
||||||
|
'fatto' => 'N',
|
||||||
|
'tipo_riga' => trim((string) ($contract->servizio_label ?: $contract->tipo_contratto)),
|
||||||
|
'richiede_pop_up' => true,
|
||||||
|
'giorni_anticipo' => (int) ($contract->giorni_preavviso ?? 0),
|
||||||
|
'popup_dal' => $popupDal->toDateString(),
|
||||||
|
'legacy_payload' => [
|
||||||
|
'source' => 'stabile_contratti',
|
||||||
|
'codice_contratto' => $contract->codice_contratto,
|
||||||
|
'riferimento_esterno' => $contract->riferimento_esterno,
|
||||||
|
'codici_collegati' => $contract->codici_collegati ?? [],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, Carbon> */
|
||||||
|
protected function buildScheduleDates(StabileContratto $contract): array
|
||||||
|
{
|
||||||
|
$start = $contract->decorrenza_dal instanceof Carbon
|
||||||
|
? $contract->decorrenza_dal->copy()
|
||||||
|
: ($contract->decorrenza_al instanceof Carbon ? $contract->decorrenza_al->copy() : now()->copy());
|
||||||
|
$end = $contract->decorrenza_al instanceof Carbon ? $contract->decorrenza_al->copy() : null;
|
||||||
|
$frequency = trim((string) ($contract->frequenza_scadenze ?? 'una_tantum'));
|
||||||
|
|
||||||
|
if ($contract->giorno_scadenza) {
|
||||||
|
$start->day(min(28, (int) $contract->giorno_scadenza));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($frequency === '' || $frequency === 'una_tantum') {
|
||||||
|
return [$end ?: $start];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dates = [];
|
||||||
|
$cursor = $start->copy();
|
||||||
|
$limit = $end
|
||||||
|
? $end->copy()
|
||||||
|
: ($contract->rinnovo_automatico ? $start->copy()->addMonths(24) : $start->copy()->addMonths(12));
|
||||||
|
|
||||||
|
while ($cursor <= $limit && count($dates) < 60) {
|
||||||
|
$dates[] = $cursor->copy();
|
||||||
|
$cursor = match ($frequency) {
|
||||||
|
'mensile' => $cursor->copy()->addMonthNoOverflow(),
|
||||||
|
'bimestrale' => $cursor->copy()->addMonthsNoOverflow(2),
|
||||||
|
'trimestrale' => $cursor->copy()->addMonthsNoOverflow(3),
|
||||||
|
'semestrale' => $cursor->copy()->addMonthsNoOverflow(6),
|
||||||
|
default => $cursor->copy()->addYearNoOverflow(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($contract->giorno_scadenza) {
|
||||||
|
$cursor->day(min(28, (int) $contract->giorno_scadenza));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTipoContrattoOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'acqua' => 'Acqua',
|
||||||
|
'energia_elettrica' => 'Energia elettrica',
|
||||||
|
'ascensore' => 'Ascensore',
|
||||||
|
'gas' => 'Gas',
|
||||||
|
'pulizie' => 'Pulizie',
|
||||||
|
'portierato' => 'Portierato',
|
||||||
|
'assicurazione' => 'Assicurazione',
|
||||||
|
'antincendio' => 'Antincendio',
|
||||||
|
'raffrescamento' => 'Raffrescamento',
|
||||||
|
'riscaldamento' => 'Riscaldamento',
|
||||||
|
'telefonia_dati' => 'Telefonia / dati',
|
||||||
|
'altro' => 'Altro servizio',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFrequenzaOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'una_tantum' => 'Una tantum',
|
||||||
|
'mensile' => 'Mensile',
|
||||||
|
'bimestrale' => 'Bimestrale',
|
||||||
|
'trimestrale' => 'Trimestrale',
|
||||||
|
'semestrale' => 'Semestrale',
|
||||||
|
'annuale' => 'Annuale',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getStabiliOptions(): array
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return StabileContext::accessibleStabili($user)
|
||||||
|
->mapWithKeys(fn(Stabile $stabile): array=> [(string) $stabile->id => trim((string) ($stabile->denominazione ?: ('Stabile #' . $stabile->id)))])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFornitoriOptions(): array
|
||||||
|
{
|
||||||
|
return Fornitore::query()
|
||||||
|
->orderByRaw("COALESCE(NULLIF(ragione_sociale, ''), CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, ''))) asc")
|
||||||
|
->limit(1500)
|
||||||
|
->get(['id', 'ragione_sociale', 'nome', 'cognome'])
|
||||||
|
->mapWithKeys(function (Fornitore $fornitore): array {
|
||||||
|
$label = trim((string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? ''))));
|
||||||
|
|
||||||
|
return [(string) $fornitore->id => ($label !== '' ? $label : ('Fornitore #' . $fornitore->id))];
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDocumentiContrattoOptions(int $stabileId): array
|
||||||
|
{
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Documento::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('tipo_documento', 'contratto')
|
||||||
|
->orderByDesc('data_documento')
|
||||||
|
->limit(250)
|
||||||
|
->get(['id', 'nome', 'numero_protocollo'])
|
||||||
|
->mapWithKeys(fn(Documento $documento): array=> [
|
||||||
|
(string) $documento->id => trim((string) (($documento->numero_protocollo ? $documento->numero_protocollo . ' · ' : '') . ($documento->nome ?: ('Documento #' . $documento->id)))),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, mixed> $rows */
|
||||||
|
protected function normalizeKeyValueRows(array $rows): array
|
||||||
|
{
|
||||||
|
return collect($rows)
|
||||||
|
->map(function ($row): ?array {
|
||||||
|
$key = trim((string) data_get($row, 'chiave', ''));
|
||||||
|
$value = trim((string) data_get($row, 'valore', ''));
|
||||||
|
|
||||||
|
return ($key !== '' && $value !== '') ? ['chiave' => $key, 'valore' => $value] : null;
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function nullableString(mixed $value): ?string
|
||||||
|
{
|
||||||
|
$value = trim((string) $value);
|
||||||
|
|
||||||
|
return $value !== '' ? $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contractBadgeColor(string $status): string
|
||||||
|
{
|
||||||
|
return match ($status) {
|
||||||
|
'attivo' => 'emerald',
|
||||||
|
'in_revisione' => 'amber',
|
||||||
|
'scaduto' => 'rose',
|
||||||
|
default => 'slate',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contractTypeLabel(?string $type): string
|
||||||
|
{
|
||||||
|
$key = trim((string) $type);
|
||||||
|
|
||||||
|
return $this->getTipoContrattoOptions()[$key] ?? ($key !== '' ? Str::headline($key) : 'Contratto');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -195,13 +195,7 @@ public function ensureAcquaAceaContract(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fornitoreAcea = Fornitore::query()
|
$fornitoreAcea = $this->resolvePreferredAcquaSupplier($stabileId);
|
||||||
->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%ato2%'])
|
|
||||||
->orderBy('ragione_sociale')
|
|
||||||
->first() ?? Fornitore::query()
|
|
||||||
->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%'])
|
|
||||||
->orderBy('ragione_sociale')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $fornitoreAcea) {
|
if (! $fornitoreAcea) {
|
||||||
Notification::make()->title('Fornitore Acea Ato2 non trovato in anagrafica fornitori.')->warning()->send();
|
Notification::make()->title('Fornitore Acea Ato2 non trovato in anagrafica fornitori.')->warning()->send();
|
||||||
|
|
@ -1241,11 +1235,7 @@ public function getAcquaContrattoStabileProperty(): array
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$fornitoreSuggerito = Fornitore::query()
|
$fornitoreSuggerito = $this->resolvePreferredAcquaSupplier($stabileId);
|
||||||
->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%ato2%'])
|
|
||||||
->value('ragione_sociale') ?? Fornitore::query()
|
|
||||||
->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%'])
|
|
||||||
->value('ragione_sociale');
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'servizio_id' => $servizio?->id,
|
'servizio_id' => $servizio?->id,
|
||||||
|
|
@ -1256,7 +1246,7 @@ public function getAcquaContrattoStabileProperty(): array
|
||||||
'codice_cliente' => $servizio?->codice_cliente,
|
'codice_cliente' => $servizio?->codice_cliente,
|
||||||
'codice_contratto' => $servizio?->codice_contratto,
|
'codice_contratto' => $servizio?->codice_contratto,
|
||||||
'matricola' => $servizio?->contatore_matricola,
|
'matricola' => $servizio?->contatore_matricola,
|
||||||
'suggerito_fornitore' => $fornitoreSuggerito ? (string) $fornitoreSuggerito : null,
|
'suggerito_fornitore' => $fornitoreSuggerito?->ragione_sociale,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1429,6 +1419,8 @@ public function getAcquaFatturePerGestioneProperty(): array
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
|
$fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreIds);
|
||||||
|
|
||||||
if (Schema::hasTable('fornitori') && Schema::hasColumn('fornitori', 'fe_features')) {
|
if (Schema::hasTable('fornitori') && Schema::hasColumn('fornitori', 'fe_features')) {
|
||||||
$waterEnabledSupplierIds = Fornitore::query()
|
$waterEnabledSupplierIds = Fornitore::query()
|
||||||
->where(function (Builder $query) use ($stabileId): Builder {
|
->where(function (Builder $query) use ($stabileId): Builder {
|
||||||
|
|
@ -1452,7 +1444,17 @@ public function getAcquaFatturePerGestioneProperty(): array
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$fornitoreIds = array_values(array_unique(array_merge($fornitoreIds, $waterEnabledSupplierIds)));
|
$fornitoreIds = array_values(array_unique(array_merge(
|
||||||
|
$fornitoreIds,
|
||||||
|
$this->resolveEquivalentFornitoreIds($waterEnabledSupplierIds)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fornitoreIds === []) {
|
||||||
|
$preferredSupplier = $this->resolvePreferredAcquaSupplier($stabileId);
|
||||||
|
if ($preferredSupplier) {
|
||||||
|
$fornitoreIds = $this->resolveEquivalentFornitoreIds([(int) $preferredSupplier->id]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($fornitoreIds !== []) {
|
if ($fornitoreIds !== []) {
|
||||||
|
|
@ -1557,6 +1559,192 @@ public function getAcquaFatturePerGestioneProperty(): array
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolvePreferredAcquaSupplier(?int $stabileId = null): ?Fornitore
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('fornitori')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$serviceSupplierId = null;
|
||||||
|
if ($stabileId) {
|
||||||
|
$serviceSupplierId = StabileServizio::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('tipo', 'acqua')
|
||||||
|
->whereNotNull('fornitore_id')
|
||||||
|
->orderByDesc('attivo')
|
||||||
|
->orderBy('id')
|
||||||
|
->value('fornitore_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = Fornitore::query()
|
||||||
|
->where(function (Builder $query): Builder {
|
||||||
|
return $query->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%ato2%'])
|
||||||
|
->orWhereRaw('LOWER(ragione_sociale) like ?', ['%acea acqua%']);
|
||||||
|
})
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'amministratore_id',
|
||||||
|
'rubrica_id',
|
||||||
|
'ragione_sociale',
|
||||||
|
'partita_iva',
|
||||||
|
'codice_fiscale',
|
||||||
|
'old_id',
|
||||||
|
'fe_features',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($candidates->isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userAdminId = null;
|
||||||
|
$user = Auth::user();
|
||||||
|
if ($user instanceof User && isset($user->amministratore_id) && is_numeric($user->amministratore_id)) {
|
||||||
|
$userAdminId = (int) $user->amministratore_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stableInvoiceCounts = [];
|
||||||
|
if ($stabileId) {
|
||||||
|
$candidateIds = $candidates->pluck('id')
|
||||||
|
->map(fn($value) => (int) $value)
|
||||||
|
->filter(fn($value) => $value > 0)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($candidateIds !== []) {
|
||||||
|
$feCounts = DB::table('fatture_elettroniche')
|
||||||
|
->selectRaw('fornitore_id, COUNT(*) as aggregate_count')
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->whereIn('fornitore_id', $candidateIds)
|
||||||
|
->groupBy('fornitore_id')
|
||||||
|
->pluck('aggregate_count', 'fornitore_id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$contabiliCounts = DB::table('contabilita_fatture_fornitori')
|
||||||
|
->selectRaw('fornitore_id, COUNT(*) as aggregate_count')
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->whereIn('fornitore_id', $candidateIds)
|
||||||
|
->groupBy('fornitore_id')
|
||||||
|
->pluck('aggregate_count', 'fornitore_id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
foreach ($candidateIds as $candidateId) {
|
||||||
|
$stableInvoiceCounts[$candidateId] = (int) ($feCounts[$candidateId] ?? 0) + (int) ($contabiliCounts[$candidateId] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sorted = $candidates->sort(function (Fornitore $left, Fornitore $right) use ($serviceSupplierId, $userAdminId, $stableInvoiceCounts): int {
|
||||||
|
$leftScore = $this->scorePreferredAcquaSupplier($left, $serviceSupplierId, $userAdminId, $stableInvoiceCounts);
|
||||||
|
$rightScore = $this->scorePreferredAcquaSupplier($right, $serviceSupplierId, $userAdminId, $stableInvoiceCounts);
|
||||||
|
|
||||||
|
if ($leftScore === $rightScore) {
|
||||||
|
return (int) $left->id <=> (int) $right->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rightScore <=> $leftScore;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $sorted->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, int> $supplierIds */
|
||||||
|
private function resolveEquivalentFornitoreIds(array $supplierIds): array
|
||||||
|
{
|
||||||
|
$supplierIds = array_values(array_unique(array_filter(array_map('intval', $supplierIds), fn(int $value): bool => $value > 0)));
|
||||||
|
if ($supplierIds === [] || ! Schema::hasTable('fornitori')) {
|
||||||
|
return $supplierIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
$suppliers = Fornitore::query()
|
||||||
|
->whereIn('id', $supplierIds)
|
||||||
|
->get(['id', 'rubrica_id', 'partita_iva', 'codice_fiscale', 'old_id']);
|
||||||
|
|
||||||
|
if ($suppliers->isEmpty()) {
|
||||||
|
return $supplierIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rubricaIds = $suppliers->pluck('rubrica_id')
|
||||||
|
->filter(fn($value) => is_numeric($value) && (int) $value > 0)
|
||||||
|
->map(fn($value) => (int) $value)
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$partiteIva = $suppliers->pluck('partita_iva')
|
||||||
|
->map(fn($value) => trim((string) $value))
|
||||||
|
->filter(fn(string $value): bool => $value !== '')
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$codiciFiscali = $suppliers->pluck('codice_fiscale')
|
||||||
|
->map(fn($value) => trim((string) $value))
|
||||||
|
->filter(fn(string $value): bool => $value !== '')
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$legacyIds = $suppliers->pluck('old_id')
|
||||||
|
->filter(fn($value) => is_numeric($value) && (int) $value > 0)
|
||||||
|
->map(fn($value) => (int) $value)
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$matches = Fornitore::query()
|
||||||
|
->where(function (Builder $query) use ($supplierIds, $rubricaIds, $partiteIva, $codiciFiscali, $legacyIds): Builder {
|
||||||
|
$query->whereIn('id', $supplierIds);
|
||||||
|
|
||||||
|
if ($rubricaIds !== []) {
|
||||||
|
$query->orWhereIn('rubrica_id', $rubricaIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($partiteIva !== []) {
|
||||||
|
$query->orWhereIn('partita_iva', $partiteIva);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($codiciFiscali !== []) {
|
||||||
|
$query->orWhereIn('codice_fiscale', $codiciFiscali);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($legacyIds !== []) {
|
||||||
|
$query->orWhereIn('old_id', $legacyIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
})
|
||||||
|
->pluck('id')
|
||||||
|
->map(fn($value) => (int) $value)
|
||||||
|
->filter(fn($value) => $value > 0)
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return $matches !== [] ? $matches : $supplierIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, int> $stableInvoiceCounts */
|
||||||
|
private function scorePreferredAcquaSupplier(Fornitore $supplier, ?int $serviceSupplierId, ?int $userAdminId, array $stableInvoiceCounts): int
|
||||||
|
{
|
||||||
|
$score = 0;
|
||||||
|
|
||||||
|
if ($serviceSupplierId && (int) $supplier->id === $serviceSupplierId) {
|
||||||
|
$score += 4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
$waterFeatures = is_array($supplier->fe_features ?? null) ? $supplier->fe_features : [];
|
||||||
|
if ((bool) data_get($waterFeatures, 'acqua.enabled', false)) {
|
||||||
|
$score += 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userAdminId && isset($supplier->amministratore_id) && (int) $supplier->amministratore_id === $userAdminId) {
|
||||||
|
$score += 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
$score += ((int) ($stableInvoiceCounts[(int) $supplier->id] ?? 0)) * 25;
|
||||||
|
|
||||||
|
return $score;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array<int, array<string,mixed>> */
|
/** @return array<int, array<string,mixed>> */
|
||||||
public function getAcquaLettureCondominiProperty(): array
|
public function getAcquaLettureCondominiProperty(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Models\CommunicationMessage;
|
use App\Models\CommunicationMessage;
|
||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
use App\Models\DocumentoCollegato;
|
use App\Models\DocumentoCollegato;
|
||||||
|
use App\Models\DocumentoStabile;
|
||||||
use App\Models\InsuranceClaim;
|
use App\Models\InsuranceClaim;
|
||||||
use App\Models\InsurancePolicy;
|
use App\Models\InsurancePolicy;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
|
|
@ -68,6 +69,10 @@ class StabilePage extends Page
|
||||||
|
|
||||||
public ?string $gestioneDataInizio = null;
|
public ?string $gestioneDataInizio = null;
|
||||||
|
|
||||||
|
public ?string $presaInCaricoAmministrazione = null;
|
||||||
|
|
||||||
|
public $verbaleNominaUpload = null;
|
||||||
|
|
||||||
public ?string $gestioneDataFine = null;
|
public ?string $gestioneDataFine = null;
|
||||||
|
|
||||||
/** @var array<int> */
|
/** @var array<int> */
|
||||||
|
|
@ -274,6 +279,97 @@ public function toggleMostraRiscaldamento(): void
|
||||||
$this->dispatch('refresh-gestioni-stabile');
|
$this->dispatch('refresh-gestioni-stabile');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function saveAmministrazioneSetup(): void
|
||||||
|
{
|
||||||
|
if (! $this->stabile instanceof StabileModel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
||||||
|
$config['amministrazione'] = array_merge(
|
||||||
|
(array) ($config['amministrazione'] ?? []),
|
||||||
|
[
|
||||||
|
'presa_in_carico_dal' => $this->presaInCaricoAmministrazione ?: null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->stabile->configurazione_avanzata = $config;
|
||||||
|
|
||||||
|
if ($this->presaInCaricoAmministrazione) {
|
||||||
|
$this->stabile->data_nomina = $this->presaInCaricoAmministrazione;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stabile->save();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Dati amministrazione aggiornati')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uploadVerbaleNomina(): void
|
||||||
|
{
|
||||||
|
if (! $this->stabile instanceof StabileModel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_object($this->verbaleNominaUpload) || ! method_exists($this->verbaleNominaUpload, 'storeAs')) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Seleziona un file da caricare')
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower((string) ($this->verbaleNominaUpload->getClientOriginalExtension() ?: 'pdf'));
|
||||||
|
$fileName = 'stabile-' . (int) $this->stabile->id . '-verbale-nomina.' . $ext;
|
||||||
|
$path = $this->verbaleNominaUpload->storeAs('stabili/verbali-nomina', $fileName, 'public');
|
||||||
|
|
||||||
|
$documento = DocumentoStabile::query()->create([
|
||||||
|
'stabile_id' => (int) $this->stabile->id,
|
||||||
|
'nome_file' => $fileName,
|
||||||
|
'nome_originale' => (string) $this->verbaleNominaUpload->getClientOriginalName(),
|
||||||
|
'percorso_file' => $path,
|
||||||
|
'categoria' => 'verbale_nomina',
|
||||||
|
'tipo_mime' => (string) ($this->verbaleNominaUpload->getClientMimeType() ?: 'application/octet-stream'),
|
||||||
|
'dimensione' => (int) ($this->verbaleNominaUpload->getSize() ?: 0),
|
||||||
|
'descrizione' => 'Verbale di nomina amministrazione',
|
||||||
|
'pubblico' => false,
|
||||||
|
'protetto' => false,
|
||||||
|
'versione' => 1,
|
||||||
|
'caricato_da' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
||||||
|
$config['amministrazione'] = array_merge(
|
||||||
|
(array) ($config['amministrazione'] ?? []),
|
||||||
|
['verbale_nomina_documento_id' => (int) $documento->id]
|
||||||
|
);
|
||||||
|
$this->stabile->configurazione_avanzata = $config;
|
||||||
|
$this->stabile->save();
|
||||||
|
|
||||||
|
$this->verbaleNominaUpload = null;
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Verbale di nomina caricato')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVerbaleNominaDocumento(): ?DocumentoStabile
|
||||||
|
{
|
||||||
|
if (! $this->stabile instanceof StabileModel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$documentId = $this->stabile->getNominaVerbaleDocumentoId();
|
||||||
|
if (! $documentId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentoStabile::query()->find($documentId);
|
||||||
|
}
|
||||||
|
|
||||||
public function savePeriodiEmissione(): void
|
public function savePeriodiEmissione(): void
|
||||||
{
|
{
|
||||||
if (! $this->stabile) {
|
if (! $this->stabile) {
|
||||||
|
|
@ -1037,6 +1133,9 @@ private function initializeOfficialMailSettings(): void
|
||||||
if ($this->officialMailboxes === []) {
|
if ($this->officialMailboxes === []) {
|
||||||
$this->officialMailboxes = $this->buildDefaultOfficialMailboxes();
|
$this->officialMailboxes = $this->buildDefaultOfficialMailboxes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->presaInCaricoAmministrazione = data_get($config, 'amministrazione.presa_in_carico_dal')
|
||||||
|
?: ($this->stabile->data_nomina?->format('Y-m-d'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,16 @@
|
||||||
use App\Modules\Contabilita\Models\MovimentoBanca;
|
use App\Modules\Contabilita\Models\MovimentoBanca;
|
||||||
use App\Modules\Contabilita\Models\PianoConti;
|
use App\Modules\Contabilita\Models\PianoConti;
|
||||||
use App\Modules\Contabilita\Models\SaldoConto;
|
use App\Modules\Contabilita\Models\SaldoConto;
|
||||||
use App\Modules\Contabilita\Services\MovimentoBancaRiconciliazioneService;
|
|
||||||
use App\Modules\Contabilita\Services\MovimentoBancaPrimaNotaService;
|
use App\Modules\Contabilita\Services\MovimentoBancaPrimaNotaService;
|
||||||
|
use App\Modules\Contabilita\Services\MovimentoBancaRiconciliazioneService;
|
||||||
use App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService;
|
use App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService;
|
||||||
use App\Services\Contabilita\ExcelMovimentiParser;
|
use App\Services\Contabilita\ExcelMovimentiParser;
|
||||||
use App\Services\Contabilita\IntesaCsvParser;
|
use App\Services\Contabilita\IntesaCsvParser;
|
||||||
use App\Services\Contabilita\MovimentiBancaImporter;
|
use App\Services\Contabilita\MovimentiBancaImporter;
|
||||||
use App\Services\Contabilita\MpsQifParser;
|
use App\Services\Contabilita\MpsQifParser;
|
||||||
use App\Services\Contabilita\UnicreditWriParser;
|
use App\Services\Contabilita\UnicreditWriParser;
|
||||||
use App\Support\ArchivioPaths;
|
|
||||||
use App\Support\AnnoGestioneContext;
|
use App\Support\AnnoGestioneContext;
|
||||||
|
use App\Support\ArchivioPaths;
|
||||||
use App\Support\GestioneContext;
|
use App\Support\GestioneContext;
|
||||||
use App\Support\StabileContext;
|
use App\Support\StabileContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Tables\Columns\BadgeColumn;
|
use Filament\Tables\Columns\IconColumn;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Concerns\InteractsWithTable;
|
use Filament\Tables\Concerns\InteractsWithTable;
|
||||||
use Filament\Tables\Contracts\HasTable;
|
use Filament\Tables\Contracts\HasTable;
|
||||||
|
|
@ -795,16 +795,7 @@ protected function getHeaderActions(): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestioneContabile::query()
|
return $this->getVisibleGestioneOptions($stabileId);
|
||||||
->where('stabile_id', $stabileId)
|
|
||||||
->orderByDesc('anno_gestione')
|
|
||||||
->orderBy('tipo_gestione')
|
|
||||||
->orderByDesc('id')
|
|
||||||
->get(['id', 'denominazione', 'protocollo_prefix', 'stato'])
|
|
||||||
->mapWithKeys(fn(GestioneContabile $g) => [
|
|
||||||
(string) $g->id => trim(($g->protocollo_prefix ? $g->protocollo_prefix . ' · ' : '') . $g->denominazione) . ($g->stato !== 'aperta' ? (' (' . $g->stato . ')') : ''),
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
})
|
})
|
||||||
->default(function (): ?string {
|
->default(function (): ?string {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
@ -1277,16 +1268,7 @@ public function table(Table $table): Table
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestioneContabile::query()
|
return $this->getVisibleGestioneOptions($stabileId);
|
||||||
->where('stabile_id', $stabileId)
|
|
||||||
->orderByDesc('anno_gestione')
|
|
||||||
->orderBy('tipo_gestione')
|
|
||||||
->orderByDesc('id')
|
|
||||||
->get(['id', 'denominazione', 'protocollo_prefix', 'stato'])
|
|
||||||
->mapWithKeys(fn(GestioneContabile $g) => [
|
|
||||||
(string) $g->id => trim(($g->protocollo_prefix ? $g->protocollo_prefix . ' · ' : '') . $g->denominazione) . ($g->stato !== 'aperta' ? (' (' . $g->stato . ')') : ''),
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
})
|
})
|
||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
|
|
@ -1310,6 +1292,13 @@ public function table(Table $table): Table
|
||||||
->date('d/m/Y')
|
->date('d/m/Y')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
|
|
||||||
|
IconColumn::make('riconciliazione_stato')
|
||||||
|
->label('Sem')
|
||||||
|
->alignCenter()
|
||||||
|
->icon(fn(MovimentoBanca $record): string => $this->getMovimentoSemaphoreMeta($record)['icon'])
|
||||||
|
->color(fn(MovimentoBanca $record): string => $this->getMovimentoSemaphoreMeta($record)['color'])
|
||||||
|
->tooltip(fn(MovimentoBanca $record): string => $this->getMovimentoSemaphoreMeta($record)['label']),
|
||||||
|
|
||||||
TextColumn::make('valuta')
|
TextColumn::make('valuta')
|
||||||
->label('Valuta')
|
->label('Valuta')
|
||||||
->date('d/m/Y'),
|
->date('d/m/Y'),
|
||||||
|
|
@ -1359,8 +1348,9 @@ public function table(Table $table): Table
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Action::make('dettaglio_movimento')
|
Action::make('dettaglio_movimento')
|
||||||
->label('Apri dati')
|
->hiddenLabel()
|
||||||
->icon('heroicon-o-eye')
|
->icon('heroicon-o-eye')
|
||||||
|
->tooltip('Apri dati')
|
||||||
->modalWidth('7xl')
|
->modalWidth('7xl')
|
||||||
->action(function (MovimentoBanca $record): void {
|
->action(function (MovimentoBanca $record): void {
|
||||||
$this->detailMovementId = $record->id;
|
$this->detailMovementId = $record->id;
|
||||||
|
|
@ -1375,8 +1365,9 @@ public function table(Table $table): Table
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
Action::make('conferma_movimento')
|
Action::make('conferma_movimento')
|
||||||
->label('Conferma')
|
->hiddenLabel()
|
||||||
->icon('heroicon-o-check-circle')
|
->icon('heroicon-o-check-circle')
|
||||||
|
->tooltip('Conferma')
|
||||||
->visible(fn(MovimentoBanca $record): bool => (bool) ($record->da_confermare ?? false))
|
->visible(fn(MovimentoBanca $record): bool => (bool) ($record->da_confermare ?? false))
|
||||||
->action(function (MovimentoBanca $record): void {
|
->action(function (MovimentoBanca $record): void {
|
||||||
if (! Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) {
|
if (! Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) {
|
||||||
|
|
@ -1408,16 +1399,7 @@ public function table(Table $table): Table
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestioneContabile::query()
|
return $this->getVisibleGestioneOptions($stabileId);
|
||||||
->where('stabile_id', $stabileId)
|
|
||||||
->orderByDesc('anno_gestione')
|
|
||||||
->orderBy('tipo_gestione')
|
|
||||||
->orderByDesc('id')
|
|
||||||
->get(['id', 'denominazione', 'protocollo_prefix', 'stato'])
|
|
||||||
->mapWithKeys(fn(GestioneContabile $g) => [
|
|
||||||
(string) $g->id => trim(($g->protocollo_prefix ? $g->protocollo_prefix . ' · ' : '') . $g->denominazione) . ($g->stato !== 'aperta' ? (' (' . $g->stato . ')') : ''),
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
DatePicker::make('data')
|
DatePicker::make('data')
|
||||||
|
|
@ -1531,8 +1513,9 @@ public function table(Table $table): Table
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Action::make('genera_prima_nota')
|
Action::make('genera_prima_nota')
|
||||||
->label('Genera prima nota')
|
->hiddenLabel()
|
||||||
->icon('heroicon-o-book-open')
|
->icon('heroicon-o-book-open')
|
||||||
|
->tooltip('Genera prima nota')
|
||||||
->visible(function (MovimentoBanca $record): bool {
|
->visible(function (MovimentoBanca $record): bool {
|
||||||
if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) {
|
if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1557,19 +1540,7 @@ public function table(Table $table): Table
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestioneContabile::query()
|
return $this->getVisibleGestioneOptions($stabileId);
|
||||||
->where('stabile_id', $stabileId)
|
|
||||||
->orderByDesc('anno_gestione')
|
|
||||||
->orderBy('tipo_gestione')
|
|
||||||
->orderBy('numero_straordinaria')
|
|
||||||
->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria'])
|
|
||||||
->mapWithKeys(fn(GestioneContabile $g) => [
|
|
||||||
(string) $g->id => $g->denominazione
|
|
||||||
. ' (' . $g->tipo_gestione . ' ' . $g->anno_gestione
|
|
||||||
. ($g->numero_straordinaria ? ' #' . $g->numero_straordinaria : '')
|
|
||||||
. ')',
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
})
|
})
|
||||||
->searchable()
|
->searchable()
|
||||||
->visible(fn(MovimentoBanca $record) => empty($record->gestione_id))
|
->visible(fn(MovimentoBanca $record) => empty($record->gestione_id))
|
||||||
|
|
@ -1633,19 +1604,7 @@ public function table(Table $table): Table
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestioneContabile::query()
|
return $this->getVisibleGestioneOptions($stabileId);
|
||||||
->where('stabile_id', $stabileId)
|
|
||||||
->orderByDesc('anno_gestione')
|
|
||||||
->orderBy('tipo_gestione')
|
|
||||||
->orderBy('numero_straordinaria')
|
|
||||||
->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria'])
|
|
||||||
->mapWithKeys(fn(GestioneContabile $g) => [
|
|
||||||
(string) $g->id => $g->denominazione
|
|
||||||
. ' (' . $g->tipo_gestione . ' ' . $g->anno_gestione
|
|
||||||
. ($g->numero_straordinaria ? ' #' . $g->numero_straordinaria : '')
|
|
||||||
. ')',
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
})
|
})
|
||||||
->searchable()
|
->searchable()
|
||||||
->visible(fn(MovimentoBanca $record) => empty($record->gestione_id))
|
->visible(fn(MovimentoBanca $record) => empty($record->gestione_id))
|
||||||
|
|
@ -2074,8 +2033,9 @@ protected function resolveDefaultGestioneId(int $stabileId): ?string
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$anno = AnnoGestioneContext::resolveActiveAnno();
|
$anno = AnnoGestioneContext::resolveActiveAnno();
|
||||||
$tipo = GestioneContext::resolveActiveGestione();
|
$tipo = GestioneContext::resolveActiveGestione();
|
||||||
|
$stabile = Stabile::query()->find($stabileId);
|
||||||
|
|
||||||
$match = GestioneContabile::query()
|
$match = GestioneContabile::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
|
|
@ -2086,6 +2046,10 @@ protected function resolveDefaultGestioneId(int $stabileId): ?string
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
if ($match && ! $this->isGestioneVisibleForStabile($match, $stabile)) {
|
||||||
|
$match = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (! $match) {
|
if (! $match) {
|
||||||
$match = GestioneContabile::query()
|
$match = GestioneContabile::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
|
|
@ -2093,11 +2057,96 @@ protected function resolveDefaultGestioneId(int $stabileId): ?string
|
||||||
->orderByDesc('gestione_attiva')
|
->orderByDesc('gestione_attiva')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
if ($match && ! $this->isGestioneVisibleForStabile($match, $stabile)) {
|
||||||
|
$match = GestioneContabile::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('stato', 'aperta')
|
||||||
|
->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria', 'protocollo_prefix', 'stato', 'gestione_attiva'])
|
||||||
|
->first(fn(GestioneContabile $gestione): bool => $this->isGestioneVisibleForStabile($gestione, $stabile));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $match ? (string) $match->id : null;
|
return $match ? (string) $match->id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
protected function getVisibleGestioneOptions(int $stabileId): array
|
||||||
|
{
|
||||||
|
$stabile = Stabile::query()->find($stabileId);
|
||||||
|
|
||||||
|
return GestioneContabile::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('stato', 'aperta')
|
||||||
|
->orderByDesc('anno_gestione')
|
||||||
|
->orderBy('tipo_gestione')
|
||||||
|
->orderBy('numero_straordinaria')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria', 'protocollo_prefix', 'stato'])
|
||||||
|
->filter(fn(GestioneContabile $gestione): bool => $this->isGestioneVisibleForStabile($gestione, $stabile))
|
||||||
|
->mapWithKeys(fn(GestioneContabile $gestione): array=> [(string) $gestione->id => $this->formatGestioneLabel($gestione, $stabile)])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isGestioneVisibleForStabile(GestioneContabile $gestione, ?Stabile $stabile): bool
|
||||||
|
{
|
||||||
|
if (trim((string) ($gestione->stato ?? '')) !== 'aperta') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $stabile instanceof Stabile) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) ($gestione->tipo_gestione ?? '') === 'riscaldamento' && ! $stabile->hasOperationalHeating()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = is_numeric($gestione->anno_gestione) ? (int) $gestione->anno_gestione : null;
|
||||||
|
|
||||||
|
return $stabile->isOperationalYearVisible($year);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formatGestioneLabel(GestioneContabile $gestione, ?Stabile $stabile): string
|
||||||
|
{
|
||||||
|
$label = trim((string) ($gestione->denominazione ?? ''));
|
||||||
|
if ($label === '') {
|
||||||
|
$label = (string) $gestione->anno_gestione . ' - ' . (string) $gestione->tipo_gestione;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($gestione->numero_straordinaria) && (int) $gestione->numero_straordinaria > 0) {
|
||||||
|
$label .= ' #' . (int) $gestione->numero_straordinaria;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = trim((string) ($gestione->protocollo_prefix ?? ''));
|
||||||
|
if ($prefix !== '') {
|
||||||
|
$label = $prefix . ' · ' . $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stabile instanceof Stabile && $stabile->getTakeoverDate()) {
|
||||||
|
$year = is_numeric($gestione->anno_gestione) ? (int) $gestione->anno_gestione : null;
|
||||||
|
if (is_int($year) && $year < (int) $stabile->getTakeoverDate()->year) {
|
||||||
|
$label .= ' · pre-presa in carico';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{icon:string,color:string,label:string} */
|
||||||
|
protected function getMovimentoSemaphoreMeta(MovimentoBanca $record): array
|
||||||
|
{
|
||||||
|
if (! empty($record->registrazione_id) || $this->hasOperationalRiconciliazione($record)) {
|
||||||
|
return ['icon' => 'heroicon-m-check-circle', 'color' => 'success', 'label' => 'Riconciliato'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) ($record->da_confermare ?? false)) {
|
||||||
|
return ['icon' => 'heroicon-m-exclamation-triangle', 'color' => 'warning', 'label' => 'Da confermare'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['icon' => 'heroicon-m-minus-circle', 'color' => 'gray', 'label' => 'Da lavorare'];
|
||||||
|
}
|
||||||
|
|
||||||
protected function resolveGestioneForYear(int $stabileId, int $year, ?string $preferredType = null): ?GestioneContabile
|
protected function resolveGestioneForYear(int $stabileId, int $year, ?string $preferredType = null): ?GestioneContabile
|
||||||
{
|
{
|
||||||
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
||||||
|
|
@ -2110,7 +2159,7 @@ protected function resolveGestioneForYear(int $stabileId, int $year, ?string $pr
|
||||||
|
|
||||||
if (is_string($preferredType) && trim($preferredType) !== '') {
|
if (is_string($preferredType) && trim($preferredType) !== '') {
|
||||||
$preferred = trim((string) $preferredType);
|
$preferred = trim((string) $preferredType);
|
||||||
$match = (clone $query)
|
$match = (clone $query)
|
||||||
->where('tipo_gestione', $preferred)
|
->where('tipo_gestione', $preferred)
|
||||||
->orderByDesc('gestione_attiva')
|
->orderByDesc('gestione_attiva')
|
||||||
->orderByRaw("CASE WHEN stato = 'aperta' THEN 0 ELSE 1 END")
|
->orderByRaw("CASE WHEN stato = 'aperta' THEN 0 ELSE 1 END")
|
||||||
|
|
@ -2430,10 +2479,10 @@ public function getRiconciliazioneStats(): array
|
||||||
|
|
||||||
$records = (clone $query)->get(['id', 'registrazione_id', 'match_data']);
|
$records = (clone $query)->get(['id', 'registrazione_id', 'match_data']);
|
||||||
|
|
||||||
$totale = $records->count();
|
$totale = $records->count();
|
||||||
$senzaPrimaNota = $records->filter(fn(MovimentoBanca $record): bool => empty($record->registrazione_id) && ! $this->hasOperationalRiconciliazione($record))->count();
|
$senzaPrimaNota = $records->filter(fn(MovimentoBanca $record): bool => empty($record->registrazione_id) && ! $this->hasOperationalRiconciliazione($record))->count();
|
||||||
$collegati = $records->filter(fn(MovimentoBanca $record): bool => ! empty($record->registrazione_id) || $this->hasOperationalRiconciliazione($record))->count();
|
$collegati = $records->filter(fn(MovimentoBanca $record): bool => ! empty($record->registrazione_id) || $this->hasOperationalRiconciliazione($record))->count();
|
||||||
$daConfermare = Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')
|
$daConfermare = Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')
|
||||||
? (clone $query)->where('da_confermare', true)->count()
|
? (clone $query)->where('da_confermare', true)->count()
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
|
@ -2554,8 +2603,8 @@ public function getRiconciliazioneCandidates(): array
|
||||||
|
|
||||||
public function riconciliaCanoneAffitto(int $canoneDovutoId): void
|
public function riconciliaCanoneAffitto(int $canoneDovutoId): void
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$current = $this->getRiconciliazioneCurrent();
|
$current = $this->getRiconciliazioneCurrent();
|
||||||
$stabileId = $this->getActiveStabileId();
|
$stabileId = $this->getActiveStabileId();
|
||||||
|
|
||||||
if (! $user instanceof User || ! $current || ! ($current['record'] instanceof MovimentoBanca) || ! $stabileId) {
|
if (! $user instanceof User || ! $current || ! ($current['record'] instanceof MovimentoBanca) || ! $stabileId) {
|
||||||
|
|
@ -2589,8 +2638,8 @@ public function riconciliaCanoneAffitto(int $canoneDovutoId): void
|
||||||
|
|
||||||
public function riconciliaPagamentoFornitore(int $fatturaFornitoreId): void
|
public function riconciliaPagamentoFornitore(int $fatturaFornitoreId): void
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$current = $this->getRiconciliazioneCurrent();
|
$current = $this->getRiconciliazioneCurrent();
|
||||||
$stabileId = $this->getActiveStabileId();
|
$stabileId = $this->getActiveStabileId();
|
||||||
|
|
||||||
if (! $user instanceof User || ! $current || ! ($current['record'] instanceof MovimentoBanca) || ! $stabileId) {
|
if (! $user instanceof User || ! $current || ! ($current['record'] instanceof MovimentoBanca) || ! $stabileId) {
|
||||||
|
|
@ -2614,19 +2663,19 @@ public function riconciliaPagamentoFornitore(int $fatturaFornitoreId): void
|
||||||
$current['record'],
|
$current['record'],
|
||||||
(int) $fattura->fornitore_id,
|
(int) $fattura->fornitore_id,
|
||||||
[
|
[
|
||||||
'gestione_id' => (int) (($fattura->gestione_id ?? 0) ?: ($current['record']->gestione_id ?? 0)),
|
'gestione_id' => (int) (($fattura->gestione_id ?? 0) ?: ($current['record']->gestione_id ?? 0)),
|
||||||
'fattura_fornitore_id' => (int) $fattura->id,
|
'fattura_fornitore_id' => (int) $fattura->id,
|
||||||
'descrizione' => 'Pagamento fattura fornitore ' . trim((string) ($fattura->numero_documento ?: $fattura->id)),
|
'descrizione' => 'Pagamento fattura fornitore ' . trim((string) ($fattura->numero_documento ?: $fattura->id)),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$matchData = is_array($current['record']->match_data ?? null) ? $current['record']->match_data : [];
|
$matchData = is_array($current['record']->match_data ?? null) ? $current['record']->match_data : [];
|
||||||
$matchData['riconciliato_operativo'] = true;
|
$matchData['riconciliato_operativo'] = true;
|
||||||
$matchData['riconciliazione_tipo'] = 'pagamento_fattura_fornitore';
|
$matchData['riconciliazione_tipo'] = 'pagamento_fattura_fornitore';
|
||||||
$matchData['riconciliazione_label'] = 'Pagamento fattura fornitore';
|
$matchData['riconciliazione_label'] = 'Pagamento fattura fornitore';
|
||||||
$matchData['riconciliazione_at'] = now()->toDateTimeString();
|
$matchData['riconciliazione_at'] = now()->toDateTimeString();
|
||||||
$matchData['fattura_fornitore_id'] = (int) $fattura->id;
|
$matchData['fattura_fornitore_id'] = (int) $fattura->id;
|
||||||
$matchData['fornitore_id'] = (int) $fattura->fornitore_id;
|
$matchData['fornitore_id'] = (int) $fattura->fornitore_id;
|
||||||
|
|
||||||
if (Schema::hasColumn('contabilita_movimenti_banca', 'match_data')) {
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'match_data')) {
|
||||||
$current['record']->match_data = $matchData;
|
$current['record']->match_data = $matchData;
|
||||||
|
|
@ -2767,15 +2816,15 @@ protected function buildIncassoCandidates(MovimentoBanca $movimento, int $stabil
|
||||||
->get()
|
->get()
|
||||||
->map(function (IncassoPagamento $incasso) use ($movimento, $descrizione, $importo, $dateColumn, $importoColumn): array {
|
->map(function (IncassoPagamento $incasso) use ($movimento, $descrizione, $importo, $dateColumn, $importoColumn): array {
|
||||||
$candidateDateRaw = data_get($incasso, $dateColumn);
|
$candidateDateRaw = data_get($incasso, $dateColumn);
|
||||||
$candidateDate = $candidateDateRaw instanceof Carbon
|
$candidateDate = $candidateDateRaw instanceof Carbon
|
||||||
? $candidateDateRaw
|
? $candidateDateRaw
|
||||||
: ($candidateDateRaw ? Carbon::parse((string) $candidateDateRaw) : null);
|
: ($candidateDateRaw ? Carbon::parse((string) $candidateDateRaw) : null);
|
||||||
$candidateImporto = (float) data_get($incasso, $importoColumn, 0);
|
$candidateImporto = (float) data_get($incasso, $importoColumn, 0);
|
||||||
$candidateLabel = trim(
|
$candidateLabel = trim(
|
||||||
(string) (data_get($incasso, 'causale')
|
(string) (data_get($incasso, 'causale')
|
||||||
?: data_get($incasso, 'descrizione')
|
?: data_get($incasso, 'descrizione')
|
||||||
?: data_get($incasso, 'modalita_pagamento_label')
|
?: data_get($incasso, 'modalita_pagamento_label')
|
||||||
?: 'Incasso')
|
?: 'Incasso')
|
||||||
);
|
);
|
||||||
$candidateNote = trim((string) (data_get($incasso, 'note_bancarie') ?: data_get($incasso, 'descrizione') ?: ''));
|
$candidateNote = trim((string) (data_get($incasso, 'note_bancarie') ?: data_get($incasso, 'descrizione') ?: ''));
|
||||||
|
|
||||||
|
|
@ -2812,7 +2861,7 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$importo = abs((float) $movimento->importo);
|
$importo = abs((float) $movimento->importo);
|
||||||
$descrizione = (string) ($movimento->descrizione_estesa_pulita ?? $movimento->descrizione_estesa ?? $movimento->descrizione ?? '');
|
$descrizione = (string) ($movimento->descrizione_estesa_pulita ?? $movimento->descrizione_estesa ?? $movimento->descrizione ?? '');
|
||||||
|
|
||||||
return AffittoCanoneDovuto::query()
|
return AffittoCanoneDovuto::query()
|
||||||
|
|
@ -2862,7 +2911,7 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil
|
||||||
$managementStart = Carbon::parse($affitto->inizio_contratto)->startOfMonth();
|
$managementStart = Carbon::parse($affitto->inizio_contratto)->startOfMonth();
|
||||||
}
|
}
|
||||||
if (! empty($affitto->presa_in_carico_operativa_dal)) {
|
if (! empty($affitto->presa_in_carico_operativa_dal)) {
|
||||||
$takeoverDate = Carbon::parse($affitto->presa_in_carico_operativa_dal)->startOfMonth();
|
$takeoverDate = Carbon::parse($affitto->presa_in_carico_operativa_dal)->startOfMonth();
|
||||||
$managementStart = $managementStart ? $managementStart->max($takeoverDate) : $takeoverDate;
|
$managementStart = $managementStart ? $managementStart->max($takeoverDate) : $takeoverDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2875,8 +2924,8 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil
|
||||||
return $candidateMonth->gte($managementStart);
|
return $candidateMonth->gte($managementStart);
|
||||||
})
|
})
|
||||||
->map(function (AffittoCanoneDovuto $canone) use ($movimento, $descrizione, $importo): array {
|
->map(function (AffittoCanoneDovuto $canone) use ($movimento, $descrizione, $importo): array {
|
||||||
$affitto = $canone->affitto;
|
$affitto = $canone->affitto;
|
||||||
$tenantLabel = $this->resolveRubricaLabel($affitto?->rubricaInquilino) ?: trim((string) ($affitto?->nome_inquilino ?? 'Inquilino'));
|
$tenantLabel = $this->resolveRubricaLabel($affitto?->rubricaInquilino) ?: trim((string) ($affitto?->nome_inquilino ?? 'Inquilino'));
|
||||||
$immobileLabel = trim((string) ($affitto?->descrizione_immobile ?? $affitto?->indirizzo_immobile ?? 'immobile'));
|
$immobileLabel = trim((string) ($affitto?->descrizione_immobile ?? $affitto?->indirizzo_immobile ?? 'immobile'));
|
||||||
|
|
||||||
$score = $this->scoreCandidate(
|
$score = $this->scoreCandidate(
|
||||||
|
|
@ -2889,20 +2938,20 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil
|
||||||
);
|
);
|
||||||
|
|
||||||
if ((int) ($affitto?->conto_bancario_id ?? 0) > 0 && (int) ($movimento->conto_id ?? 0) === (int) $affitto->conto_bancario_id) {
|
if ((int) ($affitto?->conto_bancario_id ?? 0) > 0 && (int) ($movimento->conto_id ?? 0) === (int) $affitto->conto_bancario_id) {
|
||||||
$score['totale'] = min(100, $score['totale'] + 10);
|
$score['totale'] = min(100, $score['totale'] + 10);
|
||||||
$score['motivo'] .= ' · stesso conto affitto';
|
$score['motivo'] .= ' · stesso conto affitto';
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tipo' => 'Canone affitto aperto',
|
'tipo' => 'Canone affitto aperto',
|
||||||
'titolo' => trim($tenantLabel . ' · ' . $immobileLabel),
|
'titolo' => trim($tenantLabel . ' · ' . $immobileLabel),
|
||||||
'score' => $score['totale'],
|
'score' => $score['totale'],
|
||||||
'importo' => (float) $canone->totale,
|
'importo' => (float) $canone->totale,
|
||||||
'data' => ($canone->data_scadenza ?? $canone->data_emissione)?->format('d/m/Y') ?? '—',
|
'data' => ($canone->data_scadenza ?? $canone->data_emissione)?->format('d/m/Y') ?? '—',
|
||||||
'dettaglio' => 'Canone ' . str_pad((string) $canone->mese, 2, '0', STR_PAD_LEFT) . '/' . (string) $canone->anno . ($canone->n_ricevuta ? (' · ricevuta ' . $canone->n_ricevuta) : ''),
|
'dettaglio' => 'Canone ' . str_pad((string) $canone->mese, 2, '0', STR_PAD_LEFT) . '/' . (string) $canone->anno . ($canone->n_ricevuta ? (' · ricevuta ' . $canone->n_ricevuta) : ''),
|
||||||
'motivo' => $score['motivo'],
|
'motivo' => $score['motivo'],
|
||||||
'azione' => 'riconcilia_affitto',
|
'azione' => 'riconcilia_affitto',
|
||||||
'canone_dovuto_id' => (int) $canone->id,
|
'canone_dovuto_id' => (int) $canone->id,
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->sortByDesc('score')
|
->sortByDesc('score')
|
||||||
|
|
@ -2915,8 +2964,8 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil
|
||||||
protected function buildFatturaCandidates(MovimentoBanca $movimento, int $stabileId): array
|
protected function buildFatturaCandidates(MovimentoBanca $movimento, int $stabileId): array
|
||||||
{
|
{
|
||||||
if (Schema::hasTable('contabilita_fatture_fornitori')) {
|
if (Schema::hasTable('contabilita_fatture_fornitori')) {
|
||||||
$importo = abs((float) $movimento->importo);
|
$importo = abs((float) $movimento->importo);
|
||||||
$descrizione = (string) ($movimento->descrizione_estesa_pulita ?? $movimento->descrizione_estesa ?? $movimento->descrizione ?? '');
|
$descrizione = (string) ($movimento->descrizione_estesa_pulita ?? $movimento->descrizione_estesa ?? $movimento->descrizione ?? '');
|
||||||
$movementHints = is_array($movimento->match_data ?? null) ? $movimento->match_data : [];
|
$movementHints = is_array($movimento->match_data ?? null) ? $movimento->match_data : [];
|
||||||
|
|
||||||
return FatturaFornitore::query()
|
return FatturaFornitore::query()
|
||||||
|
|
@ -2957,32 +3006,32 @@ protected function buildFatturaCandidates(MovimentoBanca $movimento, int $stabil
|
||||||
$fornitoreLabel . ' ' . trim((string) ($fattura->numero_documento ?? ''))
|
$fornitoreLabel . ' ' . trim((string) ($fattura->numero_documento ?? ''))
|
||||||
);
|
);
|
||||||
|
|
||||||
$fatturaHints = $this->extractFatturaPaymentHints($fattura);
|
$fatturaHints = $this->extractFatturaPaymentHints($fattura);
|
||||||
$movementProvider = strtoupper(trim((string) ($movementHints['payment_provider'] ?? $movementHints['beneficiario'] ?? '')));
|
$movementProvider = strtoupper(trim((string) ($movementHints['payment_provider'] ?? $movementHints['beneficiario'] ?? '')));
|
||||||
$fatturaProvider = strtoupper(trim((string) ($fatturaHints['provider'] ?? $fornitoreLabel)));
|
$fatturaProvider = strtoupper(trim((string) ($fatturaHints['provider'] ?? $fornitoreLabel)));
|
||||||
|
|
||||||
if ($movementProvider !== '' && $fatturaProvider !== '' && str_contains($fatturaProvider, $movementProvider)) {
|
if ($movementProvider !== '' && $fatturaProvider !== '' && str_contains($fatturaProvider, $movementProvider)) {
|
||||||
$score['totale'] = min(100, $score['totale'] + 18);
|
$score['totale'] = min(100, $score['totale'] + 18);
|
||||||
$score['motivo'] .= ' · fornitore coerente con il movimento';
|
$score['motivo'] .= ' · fornitore coerente con il movimento';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($movementHints['cbill'] ?? null) && ($fatturaHints['cbill'] ?? null) && (string) $movementHints['cbill'] === (string) $fatturaHints['cbill']) {
|
if (($movementHints['cbill'] ?? null) && ($fatturaHints['cbill'] ?? null) && (string) $movementHints['cbill'] === (string) $fatturaHints['cbill']) {
|
||||||
$score['totale'] = min(100, $score['totale'] + 35);
|
$score['totale'] = min(100, $score['totale'] + 35);
|
||||||
$score['motivo'] .= ' · CBILL coincidente';
|
$score['motivo'] .= ' · CBILL coincidente';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($movementHints['sia'] ?? null) && ($fatturaHints['sia'] ?? null) && strtoupper((string) $movementHints['sia']) === strtoupper((string) $fatturaHints['sia'])) {
|
if (($movementHints['sia'] ?? null) && ($fatturaHints['sia'] ?? null) && strtoupper((string) $movementHints['sia']) === strtoupper((string) $fatturaHints['sia'])) {
|
||||||
$score['totale'] = min(100, $score['totale'] + 15);
|
$score['totale'] = min(100, $score['totale'] + 15);
|
||||||
$score['motivo'] .= ' · SIA coerente';
|
$score['motivo'] .= ' · SIA coerente';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($movementHints['suggested_voce_spesa_code'] ?? null) === 'ACQ' && ($fatturaHints['is_water'] ?? false)) {
|
if (($movementHints['suggested_voce_spesa_code'] ?? null) === 'ACQ' && ($fatturaHints['is_water'] ?? false)) {
|
||||||
$score['totale'] = min(100, $score['totale'] + 20);
|
$score['totale'] = min(100, $score['totale'] + 20);
|
||||||
$score['motivo'] .= ' · spesa acqua/ACQ';
|
$score['motivo'] .= ' · spesa acqua/ACQ';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((float) ($fattura->ritenuta_importo ?? 0) > 0) {
|
if ((float) ($fattura->ritenuta_importo ?? 0) > 0) {
|
||||||
$score['totale'] = min(100, $score['totale'] + 5);
|
$score['totale'] = min(100, $score['totale'] + 5);
|
||||||
$score['motivo'] .= ' · netto coerente con RA';
|
$score['motivo'] .= ' · netto coerente con RA';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3001,15 +3050,15 @@ protected function buildFatturaCandidates(MovimentoBanca $movimento, int $stabil
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tipo' => 'Fattura fornitore',
|
'tipo' => 'Fattura fornitore',
|
||||||
'titolo' => $fornitoreLabel . ' · n. ' . trim((string) ($fattura->numero_documento ?: $fattura->id)),
|
'titolo' => $fornitoreLabel . ' · n. ' . trim((string) ($fattura->numero_documento ?: $fattura->id)),
|
||||||
'score' => $score['totale'],
|
'score' => $score['totale'],
|
||||||
'importo' => $targetImporto,
|
'importo' => $targetImporto,
|
||||||
'data' => $fattura->data_documento?->format('d/m/Y') ?? '—',
|
'data' => $fattura->data_documento?->format('d/m/Y') ?? '—',
|
||||||
'dettaglio' => implode(' · ', $detailBits),
|
'dettaglio' => implode(' · ', $detailBits),
|
||||||
'motivo' => $score['motivo'],
|
'motivo' => $score['motivo'],
|
||||||
'azione' => 'riconcilia_pagamento_fornitore',
|
'azione' => 'riconcilia_pagamento_fornitore',
|
||||||
'fattura_fornitore_id'=> (int) $fattura->id,
|
'fattura_fornitore_id' => (int) $fattura->id,
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->filter()
|
->filter()
|
||||||
|
|
@ -3081,15 +3130,15 @@ protected function getOperationalRiconciliazioneLabel(MovimentoBanca $record): ?
|
||||||
}
|
}
|
||||||
|
|
||||||
$matchData = is_array($record->match_data ?? null) ? $record->match_data : [];
|
$matchData = is_array($record->match_data ?? null) ? $record->match_data : [];
|
||||||
$label = trim((string) ($matchData['riconciliazione_label'] ?? ''));
|
$label = trim((string) ($matchData['riconciliazione_label'] ?? ''));
|
||||||
if ($label !== '') {
|
if ($label !== '') {
|
||||||
return $label;
|
return $label;
|
||||||
}
|
}
|
||||||
|
|
||||||
return match ((string) ($matchData['riconciliazione_tipo'] ?? '')) {
|
return match ((string) ($matchData['riconciliazione_tipo'] ?? '')) {
|
||||||
'affitto_canone' => 'Canone affitto chiuso',
|
'affitto_canone' => 'Canone affitto chiuso',
|
||||||
'pagamento_fattura_fornitore' => 'Pagamento fattura fornitore',
|
'pagamento_fattura_fornitore' => 'Pagamento fattura fornitore',
|
||||||
default => 'Riconciliato operativamente',
|
default => 'Riconciliato operativamente',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3129,12 +3178,16 @@ protected function extractFatturaPaymentHints(FatturaFornitore $fattura): array
|
||||||
$hints['sia'] = trim((string) $payment['sia']);
|
$hints['sia'] = trim((string) $payment['sia']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (empty($hints['sia']) && is_string($fattura->fornitore?->sia_effettivo ?? null) && trim((string) $fattura->fornitore->sia_effettivo) !== '') {
|
||||||
|
$hints['sia'] = trim((string) $fattura->fornitore->sia_effettivo);
|
||||||
|
}
|
||||||
|
|
||||||
if (is_string($payload['voce_spesa_code'] ?? null) && trim((string) $payload['voce_spesa_code']) !== '') {
|
if (is_string($payload['voce_spesa_code'] ?? null) && trim((string) $payload['voce_spesa_code']) !== '') {
|
||||||
$hints['voce_spesa_code'] = strtoupper(trim((string) $payload['voce_spesa_code']));
|
$hints['voce_spesa_code'] = strtoupper(trim((string) $payload['voce_spesa_code']));
|
||||||
}
|
}
|
||||||
|
|
||||||
$type = strtoupper(trim((string) ($payload['type'] ?? $payload['tipo'] ?? '')));
|
$type = strtoupper(trim((string) ($payload['type'] ?? $payload['tipo'] ?? '')));
|
||||||
$fe = $fattura->fatturaElettronica;
|
$fe = $fattura->fatturaElettronica;
|
||||||
$isWater = $type === 'ACQUA'
|
$isWater = $type === 'ACQUA'
|
||||||
|| $type === 'WATER'
|
|| $type === 'WATER'
|
||||||
|| (($hints['voce_spesa_code'] ?? null) === 'ACQ')
|
|| (($hints['voce_spesa_code'] ?? null) === 'ACQ')
|
||||||
|
|
@ -3386,12 +3439,12 @@ public function getGestioniContabiliRilevate(): array
|
||||||
$folder = ArchivioPaths::gestioneFolderName($gestione, (int) $gestione->anno_gestione, (string) $gestione->tipo_gestione);
|
$folder = ArchivioPaths::gestioneFolderName($gestione, (int) $gestione->anno_gestione, (string) $gestione->tipo_gestione);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $gestione->id,
|
'id' => (int) $gestione->id,
|
||||||
'year' => (int) ($gestione->anno_gestione ?? 0),
|
'year' => (int) ($gestione->anno_gestione ?? 0),
|
||||||
'tipo' => (string) ($gestione->tipo_gestione ?? ''),
|
'tipo' => (string) ($gestione->tipo_gestione ?? ''),
|
||||||
'folder' => $folder,
|
'folder' => $folder,
|
||||||
'label' => trim(($gestione->protocollo_prefix ? $gestione->protocollo_prefix . ' · ' : '') . ($gestione->denominazione ?: $folder)),
|
'label' => trim(($gestione->protocollo_prefix ? $gestione->protocollo_prefix . ' · ' : '') . ($gestione->denominazione ?: $folder)),
|
||||||
'stato' => (string) ($gestione->stato ?? ''),
|
'stato' => (string) ($gestione->stato ?? ''),
|
||||||
'active' => (bool) ($gestione->gestione_attiva ?? false),
|
'active' => (bool) ($gestione->gestione_attiva ?? false),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
|
|
@ -3403,13 +3456,13 @@ public function getGestioniContabiliRilevate(): array
|
||||||
public function getArchivioOperativoSummary(): array
|
public function getArchivioOperativoSummary(): array
|
||||||
{
|
{
|
||||||
$stabile = $this->getActiveStabile();
|
$stabile = $this->getActiveStabile();
|
||||||
$conto = $this->contoId
|
$conto = $this->contoId
|
||||||
? DatiBancari::query()->where('stabile_id', $this->getActiveStabileId())->whereKey($this->contoId)->first()
|
? DatiBancari::query()->where('stabile_id', $this->getActiveStabileId())->whereKey($this->contoId)->first()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
$referenceYear = $this->resolveArchivioReferenceYear();
|
$referenceYear = $this->resolveArchivioReferenceYear();
|
||||||
$activeGestione = null;
|
$activeGestione = null;
|
||||||
$stabileId = $this->getActiveStabileId();
|
$stabileId = $this->getActiveStabileId();
|
||||||
if ($stabileId) {
|
if ($stabileId) {
|
||||||
$defaultGestioneId = $this->resolveDefaultGestioneId($stabileId);
|
$defaultGestioneId = $this->resolveDefaultGestioneId($stabileId);
|
||||||
if ($defaultGestioneId && is_numeric($defaultGestioneId)) {
|
if ($defaultGestioneId && is_numeric($defaultGestioneId)) {
|
||||||
|
|
@ -3425,15 +3478,15 @@ public function getArchivioOperativoSummary(): array
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'reference_year' => $referenceYear,
|
'reference_year' => $referenceYear,
|
||||||
'gestione_folder' => ArchivioPaths::gestioneFolderName($activeGestione, $referenceYear, 'ordinaria'),
|
'gestione_folder' => ArchivioPaths::gestioneFolderName($activeGestione, $referenceYear, 'ordinaria'),
|
||||||
'gestione_label' => $activeGestione
|
'gestione_label' => $activeGestione
|
||||||
? trim(((string) ($activeGestione->protocollo_prefix ?? '') !== '' ? $activeGestione->protocollo_prefix . ' · ' : '') . (string) ($activeGestione->denominazione ?? 'Gestione'))
|
? trim(((string) ($activeGestione->protocollo_prefix ?? '') !== '' ? $activeGestione->protocollo_prefix . ' · ' : '') . (string) ($activeGestione->denominazione ?? 'Gestione'))
|
||||||
: 'Gestione da associare',
|
: 'Gestione da associare',
|
||||||
'banca_folder' => $bancaFolder,
|
'banca_folder' => $bancaFolder,
|
||||||
'fatture_xml_folder' => 'fatture_xml/' . $referenceYear,
|
'fatture_xml_folder' => 'fatture_xml/' . $referenceYear,
|
||||||
'fatture_pdf_folder' => 'fatture/' . ArchivioPaths::gestioneFolderName($activeGestione, $referenceYear, 'ordinaria'),
|
'fatture_pdf_folder' => 'fatture/' . ArchivioPaths::gestioneFolderName($activeGestione, $referenceYear, 'ordinaria'),
|
||||||
'base_folder' => $stabile ? ArchivioPaths::stabileBase($stabile, $stabile->amministratore) : null,
|
'base_folder' => $stabile ? ArchivioPaths::stabileBase($stabile, $stabile->amministratore) : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3442,19 +3495,19 @@ public function getRiconciliazioneBucketRows(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
'title' => 'Costi / Entrate',
|
'title' => 'Costi / Entrate',
|
||||||
'detail' => 'Lettura economica standard dei movimenti: incassi, spese, pagamenti, bonifici e competenze correnti.',
|
'detail' => 'Lettura economica standard dei movimenti: incassi, spese, pagamenti, bonifici e competenze correnti.',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'title' => 'Crediti / Debiti',
|
'title' => 'Crediti / Debiti',
|
||||||
'detail' => 'Base già pronta con crediti verso condòmini e debiti verso fornitori, utile per incassi rate, FE passive e note di rettifica.',
|
'detail' => 'Base già pronta con crediti verso condòmini e debiti verso fornitori, utile per incassi rate, FE passive e note di rettifica.',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'title' => 'Fondi / Accantonamenti',
|
'title' => 'Fondi / Accantonamenti',
|
||||||
'detail' => 'Predisposti bucket separati per fondo riserva e accantonamenti, così la riconciliazione non confonde disponibilità corrente e somme vincolate.',
|
'detail' => 'Predisposti bucket separati per fondo riserva e accantonamenti, così la riconciliazione non confonde disponibilità corrente e somme vincolate.',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'title' => 'Rimborsi',
|
'title' => 'Rimborsi',
|
||||||
'detail' => 'Separazione tra rimborsi da distribuire e rimborsi da utilizzare, per tenere distinta la quadratura patrimoniale dalla spesa/ricavo puro.',
|
'detail' => 'Separazione tra rimborsi da distribuire e rimborsi da utilizzare, per tenere distinta la quadratura patrimoniale dalla spesa/ricavo puro.',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
@ -3519,7 +3572,7 @@ protected function archiveImportedBankSourceFile(DatiBancari $conto, string $tem
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$year = (int) now()->format('Y');
|
$year = (int) now()->format('Y');
|
||||||
$gestione = null;
|
$gestione = null;
|
||||||
if ($gestioneId && DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
if ($gestioneId && DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
||||||
$gestione = GestioneContabile::query()->find($gestioneId);
|
$gestione = GestioneContabile::query()->find($gestioneId);
|
||||||
|
|
@ -3538,15 +3591,15 @@ protected function archiveImportedBankSourceFile(DatiBancari $conto, string $tem
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
$originalName = basename($sourcePath);
|
$originalName = basename($sourcePath);
|
||||||
$safeName = preg_replace('/[^A-Za-z0-9._-]+/', '-', $originalName) ?? $originalName;
|
$safeName = preg_replace('/[^A-Za-z0-9._-]+/', '-', $originalName) ?? $originalName;
|
||||||
$targetPath = $targetDir . '/import/' . now()->format('Ymd-His') . '-' . $formatPrefix . $safeName;
|
$targetPath = $targetDir . '/import/' . now()->format('Ymd-His') . '-' . $formatPrefix . $safeName;
|
||||||
|
|
||||||
Storage::disk('local')->makeDirectory($targetDir . '/import');
|
Storage::disk('local')->makeDirectory($targetDir . '/import');
|
||||||
Storage::disk('local')->put($targetPath, Storage::disk('local')->get($sourcePath));
|
Storage::disk('local')->put($targetPath, Storage::disk('local')->get($sourcePath));
|
||||||
|
|
||||||
$matchData = [
|
$matchData = [
|
||||||
'archived_bank_source_path' => $targetPath,
|
'archived_bank_source_path' => $targetPath,
|
||||||
'gestione_folder' => ArchivioPaths::gestioneFolderName($gestione, $year, 'ordinaria'),
|
'gestione_folder' => ArchivioPaths::gestioneFolderName($gestione, $year, 'ordinaria'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return $targetPath;
|
return $targetPath;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Contabilita;
|
namespace App\Filament\Pages\Contabilita;
|
||||||
|
|
||||||
use App\Models\DatiBancari;
|
use App\Models\DatiBancari;
|
||||||
|
|
@ -9,10 +8,10 @@
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Modules\Contabilita\Models\MovimentoBanca;
|
use App\Modules\Contabilita\Models\MovimentoBanca;
|
||||||
use App\Modules\Contabilita\Models\SaldoConto;
|
use App\Modules\Contabilita\Models\SaldoConto;
|
||||||
|
use App\Services\Contabilita\ExcelMovimentiParser;
|
||||||
|
use App\Services\Contabilita\IntesaCsvParser;
|
||||||
use App\Services\Contabilita\MovimentiBancaImporter;
|
use App\Services\Contabilita\MovimentiBancaImporter;
|
||||||
use App\Services\Contabilita\UnicreditWriParser;
|
use App\Services\Contabilita\UnicreditWriParser;
|
||||||
use App\Services\Contabilita\IntesaCsvParser;
|
|
||||||
use App\Services\Contabilita\ExcelMovimentiParser;
|
|
||||||
use App\Support\AnnoGestioneContext;
|
use App\Support\AnnoGestioneContext;
|
||||||
use App\Support\GestioneContext;
|
use App\Support\GestioneContext;
|
||||||
use App\Support\StabileContext;
|
use App\Support\StabileContext;
|
||||||
|
|
@ -100,14 +99,14 @@ protected function getHeaderActions(): array
|
||||||
->label('Conto (IBAN)')
|
->label('Conto (IBAN)')
|
||||||
->native(false)
|
->native(false)
|
||||||
->searchable()
|
->searchable()
|
||||||
->options(fn(): array => $this->getIbanOptions())
|
->options(fn(): array=> $this->getIbanOptions())
|
||||||
->helperText('Opzionale: se non selezionato, l\'import prova a rilevare il conto dai dati.'),
|
->helperText('Opzionale: se non selezionato, l\'import prova a rilevare il conto dai dati.'),
|
||||||
|
|
||||||
Select::make('gestione_id')
|
Select::make('gestione_id')
|
||||||
->label('Gestione (opzionale)')
|
->label('Gestione (opzionale)')
|
||||||
->native(false)
|
->native(false)
|
||||||
->searchable()
|
->searchable()
|
||||||
->options(fn(): array => $this->getGestioniOptions())
|
->options(fn(): array=> $this->getGestioniOptions())
|
||||||
->default(fn(): ?string => $this->resolveDefaultGestioneId()),
|
->default(fn(): ?string => $this->resolveDefaultGestioneId()),
|
||||||
|
|
||||||
FileUpload::make('file')
|
FileUpload::make('file')
|
||||||
|
|
@ -141,9 +140,9 @@ protected function getHeaderActions(): array
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$content = Storage::disk('local')->get($path);
|
$content = Storage::disk('local')->get($path);
|
||||||
$importer = new MovimentiBancaImporter(new UnicreditWriParser(), new IntesaCsvParser(), new ExcelMovimentiParser());
|
$importer = new MovimentiBancaImporter(new UnicreditWriParser(), new IntesaCsvParser(), new ExcelMovimentiParser());
|
||||||
$res = $importer->importUnicreditWri(
|
$res = $importer->importUnicreditWri(
|
||||||
$content,
|
$content,
|
||||||
$stabileId,
|
$stabileId,
|
||||||
basename($path),
|
basename($path),
|
||||||
|
|
@ -177,7 +176,7 @@ protected function getHeaderActions(): array
|
||||||
->label('Conto (IBAN)')
|
->label('Conto (IBAN)')
|
||||||
->native(false)
|
->native(false)
|
||||||
->searchable()
|
->searchable()
|
||||||
->options(fn(): array => $this->getIbanOptions())
|
->options(fn(): array=> $this->getIbanOptions())
|
||||||
->required(),
|
->required(),
|
||||||
|
|
||||||
Textarea::make('note')
|
Textarea::make('note')
|
||||||
|
|
@ -246,7 +245,7 @@ protected function getHeaderActions(): array
|
||||||
$created = 0;
|
$created = 0;
|
||||||
DB::transaction(function () use ($rows, $stabileId, $contoId, $iban, $note, &$created, $user): void {
|
DB::transaction(function () use ($rows, $stabileId, $contoId, $iban, $note, &$created, $user): void {
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$date = $row['data_saldo'] ?? null;
|
$date = $row['data_saldo'] ?? null;
|
||||||
$saldo = $row['saldo'] ?? null;
|
$saldo = $row['saldo'] ?? null;
|
||||||
|
|
||||||
if (! $date || ! is_numeric($saldo)) {
|
if (! $date || ! is_numeric($saldo)) {
|
||||||
|
|
@ -255,11 +254,11 @@ protected function getHeaderActions(): array
|
||||||
|
|
||||||
SaldoConto::query()->create([
|
SaldoConto::query()->create([
|
||||||
'stabile_id' => (int) $stabileId,
|
'stabile_id' => (int) $stabileId,
|
||||||
'conto_id' => $contoId,
|
'conto_id' => $contoId,
|
||||||
'iban' => $iban,
|
'iban' => $iban,
|
||||||
'data_saldo' => $date,
|
'data_saldo' => $date,
|
||||||
'saldo' => (float) $saldo,
|
'saldo' => (float) $saldo,
|
||||||
'note' => $note !== '' ? $note : null,
|
'note' => $note !== '' ? $note : null,
|
||||||
'created_by' => (int) ($user->id ?? 0) ?: null,
|
'created_by' => (int) ($user->id ?? 0) ?: null,
|
||||||
'updated_by' => (int) ($user->id ?? 0) ?: null,
|
'updated_by' => (int) ($user->id ?? 0) ?: null,
|
||||||
]);
|
]);
|
||||||
|
|
@ -314,7 +313,7 @@ public function table(Table $table): Table
|
||||||
Select::make('gestione_ids')
|
Select::make('gestione_ids')
|
||||||
->label('Gestione')
|
->label('Gestione')
|
||||||
->multiple()
|
->multiple()
|
||||||
->options(fn(): array => $this->getGestioniOptions())
|
->options(fn(): array=> $this->getGestioniOptions())
|
||||||
->searchable(),
|
->searchable(),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
|
|
@ -384,10 +383,10 @@ public function getTotaleDisponibilita(): ?float
|
||||||
}
|
}
|
||||||
|
|
||||||
$periodo = $this->getTableFilterState('periodo');
|
$periodo = $this->getTableFilterState('periodo');
|
||||||
$from = is_array($periodo) ? ($periodo['from'] ?? null) : null;
|
$from = is_array($periodo) ? ($periodo['from'] ?? null) : null;
|
||||||
$to = is_array($periodo) ? ($periodo['to'] ?? null) : null;
|
$to = is_array($periodo) ? ($periodo['to'] ?? null) : null;
|
||||||
|
|
||||||
$gestioni = $this->getTableFilterState('gestioni');
|
$gestioni = $this->getTableFilterState('gestioni');
|
||||||
$gestioneIds = is_array($gestioni) ? ($gestioni['gestione_ids'] ?? null) : null;
|
$gestioneIds = is_array($gestioni) ? ($gestioni['gestione_ids'] ?? null) : null;
|
||||||
$gestioneIds = is_array($gestioneIds) ? array_values(array_filter(array_map('intval', $gestioneIds), fn(int $v) => $v > 0)) : [];
|
$gestioneIds = is_array($gestioneIds) ? array_values(array_filter(array_map('intval', $gestioneIds), fn(int $v) => $v > 0)) : [];
|
||||||
|
|
||||||
|
|
@ -425,10 +424,10 @@ protected function resolveSaldoConto(DatiBancari $record): ?float
|
||||||
$iban = is_string($record->iban) ? trim((string) $record->iban) : '';
|
$iban = is_string($record->iban) ? trim((string) $record->iban) : '';
|
||||||
|
|
||||||
$periodo = $this->getTableFilterState('periodo');
|
$periodo = $this->getTableFilterState('periodo');
|
||||||
$from = is_array($periodo) ? ($periodo['from'] ?? null) : null;
|
$from = is_array($periodo) ? ($periodo['from'] ?? null) : null;
|
||||||
$to = is_array($periodo) ? ($periodo['to'] ?? null) : null;
|
$to = is_array($periodo) ? ($periodo['to'] ?? null) : null;
|
||||||
|
|
||||||
$gestioni = $this->getTableFilterState('gestioni');
|
$gestioni = $this->getTableFilterState('gestioni');
|
||||||
$gestioneIds = is_array($gestioni) ? ($gestioni['gestione_ids'] ?? null) : null;
|
$gestioneIds = is_array($gestioni) ? ($gestioni['gestione_ids'] ?? null) : null;
|
||||||
$gestioneIds = is_array($gestioneIds) ? array_values(array_filter(array_map('intval', $gestioneIds), fn(int $v) => $v > 0)) : [];
|
$gestioneIds = is_array($gestioneIds) ? array_values(array_filter(array_map('intval', $gestioneIds), fn(int $v) => $v > 0)) : [];
|
||||||
|
|
||||||
|
|
@ -451,7 +450,7 @@ protected function resolveSaldoConto(DatiBancari $record): ?float
|
||||||
|
|
||||||
// Base da saldi intermedi: se c'è un saldo <= from (se filtrato) o altrimenti ultimo saldo disponibile.
|
// Base da saldi intermedi: se c'è un saldo <= from (se filtrato) o altrimenti ultimo saldo disponibile.
|
||||||
$baseSaldo = null;
|
$baseSaldo = null;
|
||||||
$baseDate = null;
|
$baseDate = null;
|
||||||
if (DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
if (DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
||||||
$qSaldo = SaldoConto::query()
|
$qSaldo = SaldoConto::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
|
|
@ -464,7 +463,7 @@ protected function resolveSaldoConto(DatiBancari $record): ?float
|
||||||
$row = $qSaldo->orderByDesc('data_saldo')->orderByDesc('id')->first(['data_saldo', 'saldo']);
|
$row = $qSaldo->orderByDesc('data_saldo')->orderByDesc('id')->first(['data_saldo', 'saldo']);
|
||||||
if ($row) {
|
if ($row) {
|
||||||
$baseSaldo = (float) $row->saldo;
|
$baseSaldo = (float) $row->saldo;
|
||||||
$baseDate = $row->data_saldo->toDateString();
|
$baseDate = $row->data_saldo->toDateString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -539,13 +538,13 @@ protected function resolveLatestSaldoSnapshotLabel(DatiBancari $record): string
|
||||||
return 'Nessuno estratto registrato';
|
return 'Nessuno estratto registrato';
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts = [];
|
$parts = [];
|
||||||
$parts[] = $snapshot->tipo_estratto
|
$parts[] = $snapshot->tipo_estratto
|
||||||
? ($this->getTipoEstrattoOptions()[$snapshot->tipo_estratto] ?? 'Saldo registrato')
|
? ($this->getTipoEstrattoOptions()[$snapshot->tipo_estratto] ?? 'Saldo registrato')
|
||||||
: 'Saldo registrato';
|
: 'Saldo registrato';
|
||||||
if ($snapshot->periodo_da || $snapshot->periodo_a) {
|
if ($snapshot->periodo_da || $snapshot->periodo_a) {
|
||||||
$from = $snapshot->periodo_da?->format('d/m/Y');
|
$from = $snapshot->periodo_da?->format('d/m/Y');
|
||||||
$to = $snapshot->periodo_a?->format('d/m/Y');
|
$to = $snapshot->periodo_a?->format('d/m/Y');
|
||||||
$parts[] = trim(($from ?: '...') . ' - ' . ($to ?: '...'));
|
$parts[] = trim(($from ?: '...') . ' - ' . ($to ?: '...'));
|
||||||
}
|
}
|
||||||
if ($snapshot->documento instanceof DocumentoStabile) {
|
if ($snapshot->documento instanceof DocumentoStabile) {
|
||||||
|
|
@ -558,10 +557,10 @@ protected function resolveLatestSaldoSnapshotLabel(DatiBancari $record): string
|
||||||
protected function getTipoEstrattoOptions(): array
|
protected function getTipoEstrattoOptions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'estratto_conto' => 'Estratto conto',
|
'estratto_conto' => 'Estratto conto',
|
||||||
'saldo_banca' => 'Saldo banca',
|
'saldo_banca' => 'Saldo banca',
|
||||||
'riconciliazione' => 'Riconciliazione',
|
'riconciliazione' => 'Riconciliazione',
|
||||||
'altro' => 'Altro',
|
'altro' => 'Altro',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -585,12 +584,12 @@ protected function getContiOptions(): array
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get(['id', 'denominazione_banca', 'iban', 'legacy_cod_cassa', 'numero_conto'])
|
->get(['id', 'denominazione_banca', 'iban', 'legacy_cod_cassa', 'numero_conto'])
|
||||||
->mapWithKeys(function (DatiBancari $conto): array {
|
->mapWithKeys(function (DatiBancari $conto): array {
|
||||||
$parts = [];
|
$parts = [];
|
||||||
$parts[] = trim((string) ($conto->denominazione_banca ?: 'Conto'));
|
$parts[] = trim((string) ($conto->denominazione_banca ?: 'Conto'));
|
||||||
|
|
||||||
$legacyCodCassa = is_string($conto->legacy_cod_cassa) ? trim((string) $conto->legacy_cod_cassa) : '';
|
$legacyCodCassa = is_string($conto->legacy_cod_cassa) ? trim((string) $conto->legacy_cod_cassa) : '';
|
||||||
$numeroConto = is_string($conto->numero_conto) ? trim((string) $conto->numero_conto) : '';
|
$numeroConto = is_string($conto->numero_conto) ? trim((string) $conto->numero_conto) : '';
|
||||||
$iban = is_string($conto->iban) ? trim((string) $conto->iban) : '';
|
$iban = is_string($conto->iban) ? trim((string) $conto->iban) : '';
|
||||||
|
|
||||||
if ($legacyCodCassa !== '') {
|
if ($legacyCodCassa !== '') {
|
||||||
$parts[] = 'cassa ' . strtoupper($legacyCodCassa);
|
$parts[] = 'cassa ' . strtoupper($legacyCodCassa);
|
||||||
|
|
|
||||||
|
|
@ -748,35 +748,18 @@ public function getGestioniStraordinarieList(): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stabile = Stabile::query()->find($stabileId);
|
||||||
|
|
||||||
return GestioneContabile::query()
|
return GestioneContabile::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
->where('tipo_gestione', 'straordinaria')
|
->where('tipo_gestione', 'straordinaria')
|
||||||
|
->where('stato', 'aperta')
|
||||||
->orderByDesc('anno_gestione')
|
->orderByDesc('anno_gestione')
|
||||||
->orderByDesc('numero_straordinaria')
|
->orderByDesc('numero_straordinaria')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->get(['id', 'anno_gestione', 'tipo_gestione', 'denominazione', 'protocollo_prefix', 'numero_straordinaria', 'stato'])
|
->get(['id', 'anno_gestione', 'tipo_gestione', 'denominazione', 'protocollo_prefix', 'numero_straordinaria', 'stato'])
|
||||||
->map(function (GestioneContabile $g): array {
|
->filter(fn(GestioneContabile $g): bool => $this->isGestioneVisibleForStabile($g, $stabile))
|
||||||
$label = trim((string) ($g->denominazione ?? ''));
|
->map(fn(GestioneContabile $g): array=> ['id' => (int) $g->id, 'label' => $this->formatGestioneLabel($g, $stabile)])
|
||||||
if ($label === '') {
|
|
||||||
$label = (string) $g->anno_gestione . ' - ' . (string) $g->tipo_gestione;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_numeric($g->numero_straordinaria) && (int) $g->numero_straordinaria > 0) {
|
|
||||||
$label .= ' • STR ' . (int) $g->numero_straordinaria;
|
|
||||||
}
|
|
||||||
|
|
||||||
$prefix = trim((string) ($g->protocollo_prefix ?? ''));
|
|
||||||
if ($prefix !== '') {
|
|
||||||
$label = $prefix . ' — ' . $label;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stato = trim((string) ($g->stato ?? ''));
|
|
||||||
if ($stato !== '') {
|
|
||||||
$label .= ' • ' . strtoupper($stato);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['id' => (int) $g->id, 'label' => $label];
|
|
||||||
})
|
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1104,47 +1087,10 @@ public function header(Schema $schema): Schema
|
||||||
->schema([
|
->schema([
|
||||||
Select::make('gestione_id')
|
Select::make('gestione_id')
|
||||||
->label('Gestione')
|
->label('Gestione')
|
||||||
->options(function (): array {
|
->options(fn(): array=> $this->getVisibleGestioneOptions())
|
||||||
$user = Auth::user();
|
|
||||||
if (! $user instanceof User) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$activeStabileId = StabileContext::resolveActiveStabileId($user);
|
|
||||||
$stabileId = (int) ($this->record?->stabile_id ?: $activeStabileId);
|
|
||||||
if (! $stabileId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestioneContabile::query()
|
|
||||||
->where('stabile_id', (int) $stabileId)
|
|
||||||
->orderByDesc('anno_gestione')
|
|
||||||
->orderByDesc('numero_straordinaria')
|
|
||||||
->orderByDesc('id')
|
|
||||||
->get(['id', 'anno_gestione', 'tipo_gestione', 'denominazione', 'protocollo_prefix', 'stato', 'numero_straordinaria'])
|
|
||||||
->mapWithKeys(function (GestioneContabile $g) {
|
|
||||||
$label = trim((string) ($g->denominazione ?? ''));
|
|
||||||
if ($label === '') {
|
|
||||||
$label = (string) $g->anno_gestione . ' - ' . (string) $g->tipo_gestione;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((string) ($g->tipo_gestione ?? '') === 'straordinaria' && is_numeric($g->numero_straordinaria) && (int) $g->numero_straordinaria > 0) {
|
|
||||||
$label .= ' (N.' . (int) $g->numero_straordinaria . ')';
|
|
||||||
}
|
|
||||||
$prefix = trim((string) ($g->protocollo_prefix ?? ''));
|
|
||||||
if ($prefix !== '') {
|
|
||||||
$label = $prefix . ' — ' . $label;
|
|
||||||
}
|
|
||||||
if (is_string($g->stato) && $g->stato !== '') {
|
|
||||||
$label .= ' [' . $g->stato . ']';
|
|
||||||
}
|
|
||||||
return [(string) $g->id => $label];
|
|
||||||
})
|
|
||||||
->all();
|
|
||||||
})
|
|
||||||
->searchable()
|
->searchable()
|
||||||
->required()
|
->required()
|
||||||
->helperText('Ordinaria / riscaldamento / straordinaria (per anno).'),
|
->helperText('Solo gestioni aperte e coerenti con lo stabile attivo.'),
|
||||||
|
|
||||||
Select::make('causale_contabile')
|
Select::make('causale_contabile')
|
||||||
->label('Causale contabile')
|
->label('Causale contabile')
|
||||||
|
|
@ -2553,6 +2499,11 @@ private function mapFeRitenutaToTributo(?string $rtCode, ?string $causale): stri
|
||||||
|
|
||||||
private function resolveRiscaldamentoGestioneByDate(int $stabileId, string $date): int
|
private function resolveRiscaldamentoGestioneByDate(int $stabileId, string $date): int
|
||||||
{
|
{
|
||||||
|
$stabile = Stabile::query()->find($stabileId);
|
||||||
|
if ($stabile instanceof Stabile && ! $stabile->hasOperationalHeating()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$dt = Carbon::parse($date);
|
$dt = Carbon::parse($date);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
|
|
@ -2561,7 +2512,8 @@ private function resolveRiscaldamentoGestioneByDate(int $stabileId, string $date
|
||||||
|
|
||||||
$q = GestioneContabile::query()
|
$q = GestioneContabile::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
->where('tipo_gestione', 'riscaldamento');
|
->where('tipo_gestione', 'riscaldamento')
|
||||||
|
->where('stato', 'aperta');
|
||||||
|
|
||||||
$match = (clone $q)
|
$match = (clone $q)
|
||||||
->whereNotNull('data_inizio')
|
->whereNotNull('data_inizio')
|
||||||
|
|
@ -2595,6 +2547,8 @@ private function resolveDefaultGestioneId(int $stabileId, ?int $year = null): in
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stabile = Stabile::query()->find($stabileId);
|
||||||
|
|
||||||
$baseQuery = GestioneContabile::query()
|
$baseQuery = GestioneContabile::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
->where('stato', 'aperta');
|
->where('stato', 'aperta');
|
||||||
|
|
@ -2603,6 +2557,16 @@ private function resolveDefaultGestioneId(int $stabileId, ?int $year = null): in
|
||||||
$baseQuery->where('anno_gestione', $year);
|
$baseQuery->where('anno_gestione', $year);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($stabile instanceof Stabile) {
|
||||||
|
if (! $stabile->hasOperationalHeating()) {
|
||||||
|
$baseQuery->where('tipo_gestione', '!=', 'riscaldamento');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stabile->getTakeoverDate()) {
|
||||||
|
$baseQuery->where('anno_gestione', '>=', (int) $stabile->getTakeoverDate()->year);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$ordinariaId = (clone $baseQuery)
|
$ordinariaId = (clone $baseQuery)
|
||||||
->where('tipo_gestione', 'ordinaria')
|
->where('tipo_gestione', 'ordinaria')
|
||||||
->orderByDesc('anno_gestione')
|
->orderByDesc('anno_gestione')
|
||||||
|
|
@ -2621,6 +2585,80 @@ private function resolveDefaultGestioneId(int $stabileId, ?int $year = null): in
|
||||||
return is_numeric($fallbackId) ? (int) $fallbackId : 0;
|
return is_numeric($fallbackId) ? (int) $fallbackId : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
private function getVisibleGestioneOptions(): array
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeStabileId = StabileContext::resolveActiveStabileId($user);
|
||||||
|
$stabileId = (int) ($this->record?->stabile_id ?: $activeStabileId);
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stabile = Stabile::query()->find($stabileId);
|
||||||
|
|
||||||
|
return GestioneContabile::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('stato', 'aperta')
|
||||||
|
->orderByDesc('anno_gestione')
|
||||||
|
->orderByDesc('numero_straordinaria')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->get(['id', 'anno_gestione', 'tipo_gestione', 'denominazione', 'protocollo_prefix', 'stato', 'numero_straordinaria'])
|
||||||
|
->filter(fn(GestioneContabile $g): bool => $this->isGestioneVisibleForStabile($g, $stabile))
|
||||||
|
->mapWithKeys(fn(GestioneContabile $g) => [(string) $g->id => $this->formatGestioneLabel($g, $stabile)])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isGestioneVisibleForStabile(GestioneContabile $gestione, ?Stabile $stabile): bool
|
||||||
|
{
|
||||||
|
if (trim((string) ($gestione->stato ?? '')) !== 'aperta') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stabile instanceof Stabile) {
|
||||||
|
if ((string) ($gestione->tipo_gestione ?? '') === 'riscaldamento' && ! $stabile->hasOperationalHeating()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = is_numeric($gestione->anno_gestione) ? (int) $gestione->anno_gestione : null;
|
||||||
|
if (! $stabile->isOperationalYearVisible($year)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatGestioneLabel(GestioneContabile $gestione, ?Stabile $stabile): string
|
||||||
|
{
|
||||||
|
$label = trim((string) ($gestione->denominazione ?? ''));
|
||||||
|
if ($label === '') {
|
||||||
|
$label = (string) $gestione->anno_gestione . ' - ' . (string) $gestione->tipo_gestione;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) ($gestione->tipo_gestione ?? '') === 'straordinaria' && is_numeric($gestione->numero_straordinaria) && (int) $gestione->numero_straordinaria > 0) {
|
||||||
|
$label .= ' (N.' . (int) $gestione->numero_straordinaria . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = trim((string) ($gestione->protocollo_prefix ?? ''));
|
||||||
|
if ($prefix !== '') {
|
||||||
|
$label = $prefix . ' — ' . $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stabile instanceof Stabile && $stabile->getTakeoverDate()) {
|
||||||
|
$year = is_numeric($gestione->anno_gestione) ? (int) $gestione->anno_gestione : null;
|
||||||
|
if (is_int($year) && $year < (int) $stabile->getTakeoverDate()->year) {
|
||||||
|
$label .= ' • pre-presa in carico';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $label;
|
||||||
|
}
|
||||||
|
|
||||||
public function getLinkedFatturaElettronica(): ?FatturaElettronica
|
public function getLinkedFatturaElettronica(): ?FatturaElettronica
|
||||||
{
|
{
|
||||||
if ($this->linkedFeCache !== null) {
|
if ($this->linkedFeCache !== null) {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class SaldiContiArchivio extends Page implements HasTable
|
||||||
{
|
{
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|
||||||
#[Url(as: 'conto_id')]
|
#[Url( as : 'conto_id')]
|
||||||
public ?int $contoId = null;
|
public ?int $contoId = null;
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Saldi conti';
|
protected static ?string $navigationLabel = 'Saldi conti';
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,16 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Contabilita;
|
namespace App\Filament\Pages\Contabilita;
|
||||||
|
|
||||||
use App\Filament\Pages\Condomini\TabelleMillesimaliProspetto;
|
use App\Filament\Pages\Condomini\TabelleMillesimaliProspetto;
|
||||||
use App\Filament\Pages\Contabilita\VoceSpesaMastrino;
|
use App\Filament\Pages\Contabilita\VoceSpesaMastrino;
|
||||||
|
use App\Models\GestioneContabile;
|
||||||
use App\Models\RipartizioneSpeseInquilini;
|
use App\Models\RipartizioneSpeseInquilini;
|
||||||
use App\Models\TabellaMillesimale;
|
use App\Models\TabellaMillesimale;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\VoceSpesa;
|
use App\Models\VoceSpesa;
|
||||||
use App\Models\GestioneContabile;
|
|
||||||
use App\Support\StabileContext;
|
|
||||||
use App\Support\AnnoGestioneContext;
|
use App\Support\AnnoGestioneContext;
|
||||||
|
use App\Support\StabileContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\BulkAction;
|
use Filament\Actions\BulkAction;
|
||||||
|
|
@ -81,15 +80,15 @@ public static function canAccess(): bool
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$tab = (string) request()->query('tab', 'ordinaria');
|
$tab = (string) request()->query('tab', 'ordinaria');
|
||||||
$this->tipoGestioneTab = in_array($tab, ['ordinaria', 'acqua', 'riscaldamento', 'straordinaria'], true) ? $tab : 'ordinaria';
|
$this->tipoGestioneTab = in_array($tab, ['ordinaria', 'acqua', 'riscaldamento', 'straordinaria'], true) ? $tab : 'ordinaria';
|
||||||
|
|
||||||
$this->gestioneContabileId = $this->resolveGestioneContabileId();
|
$this->gestioneContabileId = $this->resolveGestioneContabileId();
|
||||||
|
|
||||||
$preset = (string) request()->query('preset', 'manuale');
|
$preset = (string) request()->query('preset', 'manuale');
|
||||||
$this->presetRipartizione = in_array($preset, ['manuale', 'confedilizia'], true) ? $preset : 'manuale';
|
$this->presetRipartizione = in_array($preset, ['manuale', 'confedilizia'], true) ? $preset : 'manuale';
|
||||||
|
|
||||||
$focus = request()->query('tabella');
|
$focus = request()->query('tabella');
|
||||||
$this->tabellaFocusId = is_numeric($focus) ? (int) $focus : null;
|
$this->tabellaFocusId = is_numeric($focus) ? (int) $focus : null;
|
||||||
if (! $this->tabellaFocusId || $this->tabellaFocusId <= 0) {
|
if (! $this->tabellaFocusId || $this->tabellaFocusId <= 0) {
|
||||||
$this->tabellaFocusId = $this->getFirstTabellaIdForFocus();
|
$this->tabellaFocusId = $this->getFirstTabellaIdForFocus();
|
||||||
|
|
@ -114,7 +113,7 @@ protected function getHeaderActions(): array
|
||||||
Select::make('anno_sorgente')
|
Select::make('anno_sorgente')
|
||||||
->label('Anno sorgente')
|
->label('Anno sorgente')
|
||||||
->options(function (): array {
|
->options(function (): array {
|
||||||
$y = (int) now()->year;
|
$y = (int) now()->year;
|
||||||
$out = [];
|
$out = [];
|
||||||
for ($i = 0; $i < 8; $i++) {
|
for ($i = 0; $i < 8; $i++) {
|
||||||
$out[(string) ($y - $i)] = (string) ($y - $i);
|
$out[(string) ($y - $i)] = (string) ($y - $i);
|
||||||
|
|
@ -134,7 +133,7 @@ protected function getHeaderActions(): array
|
||||||
])
|
])
|
||||||
->action(function (array $data): void {
|
->action(function (array $data): void {
|
||||||
$annoSorgente = is_numeric($data['anno_sorgente'] ?? null) ? (int) $data['anno_sorgente'] : 0;
|
$annoSorgente = is_numeric($data['anno_sorgente'] ?? null) ? (int) $data['anno_sorgente'] : 0;
|
||||||
$aggiorna = (bool) ($data['aggiorna_esistenti'] ?? true);
|
$aggiorna = (bool) ($data['aggiorna_esistenti'] ?? true);
|
||||||
$this->importaVociPerAnno($annoSorgente, $aggiorna);
|
$this->importaVociPerAnno($annoSorgente, $aggiorna);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -183,7 +182,7 @@ private function mapTabellaToAnno(?int $sourceTabellaId, int $stabileId, int $ta
|
||||||
return $sourceTabellaId;
|
return $sourceTabellaId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione');
|
$hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione');
|
||||||
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
||||||
|
|
||||||
$q = TabellaMillesimale::query()
|
$q = TabellaMillesimale::query()
|
||||||
|
|
@ -219,7 +218,7 @@ private function importaVociPerAnno(int $annoSorgente, bool $aggiornaEsistenti =
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria');
|
$tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria');
|
||||||
$annoTarget = AnnoGestioneContext::resolveActiveAnno($user);
|
$annoTarget = AnnoGestioneContext::resolveActiveAnno($user);
|
||||||
|
|
||||||
$gestioneTargetId = $this->gestioneContabileId ?: $this->resolveGestioneContabileId();
|
$gestioneTargetId = $this->gestioneContabileId ?: $this->resolveGestioneContabileId();
|
||||||
|
|
@ -297,7 +296,7 @@ private function importaVociPerAnno(int $annoSorgente, bool $aggiornaEsistenti =
|
||||||
}
|
}
|
||||||
|
|
||||||
$targetId = (int) $existing[$codice];
|
$targetId = (int) $existing[$codice];
|
||||||
$target = VoceSpesa::query()->whereKey($targetId)->first();
|
$target = VoceSpesa::query()->whereKey($targetId)->first();
|
||||||
if (! $target) {
|
if (! $target) {
|
||||||
$skipped++;
|
$skipped++;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -327,7 +326,7 @@ private function importaVociPerAnno(int $annoSorgente, bool $aggiornaEsistenti =
|
||||||
// Crea nuova voce per gestione target (preserva codice).
|
// Crea nuova voce per gestione target (preserva codice).
|
||||||
$data = $src->toArray();
|
$data = $src->toArray();
|
||||||
unset($data['id'], $data['id_voce'], $data['created_at'], $data['updated_at']);
|
unset($data['id'], $data['id_voce'], $data['created_at'], $data['updated_at']);
|
||||||
$data['stabile_id'] = $stabileId;
|
$data['stabile_id'] = $stabileId;
|
||||||
$data['gestione_contabile_id'] = $gestioneTargetId;
|
$data['gestione_contabile_id'] = $gestioneTargetId;
|
||||||
if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) {
|
if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) {
|
||||||
$data['tipo_gestione'] = $tipo;
|
$data['tipo_gestione'] = $tipo;
|
||||||
|
|
@ -363,8 +362,8 @@ private function ricollegaTabellePerAnnoAttivo(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$annoTarget = AnnoGestioneContext::resolveActiveAnno($user);
|
$annoTarget = AnnoGestioneContext::resolveActiveAnno($user);
|
||||||
$tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria');
|
$tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria');
|
||||||
$gestioneTargetId = $this->gestioneContabileId ?: $this->resolveGestioneContabileId();
|
$gestioneTargetId = $this->gestioneContabileId ?: $this->resolveGestioneContabileId();
|
||||||
if (! $gestioneTargetId) {
|
if (! $gestioneTargetId) {
|
||||||
Notification::make()->title('Gestione non trovata')->danger()->send();
|
Notification::make()->title('Gestione non trovata')->danger()->send();
|
||||||
|
|
@ -441,11 +440,11 @@ public function getTabelleMillesimaliOptions(): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$tipoTab = $this->tipoGestioneTab ?: 'ordinaria';
|
$tipoTab = $this->tipoGestioneTab ?: 'ordinaria';
|
||||||
$tipo = $this->normalizeTipoGestione($tipoTab);
|
$tipo = $this->normalizeTipoGestione($tipoTab);
|
||||||
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
||||||
$hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione');
|
$hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione');
|
||||||
$anno = AnnoGestioneContext::resolveActiveAnno($user);
|
$anno = AnnoGestioneContext::resolveActiveAnno($user);
|
||||||
|
|
||||||
$q = TabellaMillesimale::query()
|
$q = TabellaMillesimale::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
|
|
@ -481,15 +480,15 @@ public function getTabelleMillesimaliOptions(): array
|
||||||
|
|
||||||
public function updatedTabellaFocusId($value): void
|
public function updatedTabellaFocusId($value): void
|
||||||
{
|
{
|
||||||
$id = is_numeric($value) ? (int) $value : null;
|
$id = is_numeric($value) ? (int) $value : null;
|
||||||
$this->tabellaFocusId = $id && $id > 0 ? $id : null;
|
$this->tabellaFocusId = $id && $id > 0 ? $id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array{label: string, count: int, totale_prev: float, totale_cons: float, voci: array<int, array{codice: string, descrizione: string, prev: float, cons: float, prop: float, inq: float, attiva: bool}>} */
|
/** @return array{label: string, count: int, totale_prev: float, totale_cons: float, voci: array<int, array{codice: string, descrizione: string, prev: float, cons: float, prop: float, inq: float, attiva: bool}>} */
|
||||||
public function getDettaglioTabellaFocus(): array
|
public function getDettaglioTabellaFocus(): array
|
||||||
{
|
{
|
||||||
$id = (int) ($this->tabellaFocusId ?? 0);
|
$id = (int) ($this->tabellaFocusId ?? 0);
|
||||||
$opts = $this->getTabelleMillesimaliOptions();
|
$opts = $this->getTabelleMillesimaliOptions();
|
||||||
$label = $id > 0 && array_key_exists($id, $opts) ? (string) $opts[$id] : '—';
|
$label = $id > 0 && array_key_exists($id, $opts) ? (string) $opts[$id] : '—';
|
||||||
|
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
|
|
@ -511,13 +510,13 @@ public function getDettaglioTabellaFocus(): array
|
||||||
|
|
||||||
$voci = $records->map(function ($r): array {
|
$voci = $records->map(function ($r): array {
|
||||||
return [
|
return [
|
||||||
'codice' => (string) ($r->codice ?? ''),
|
'codice' => (string) ($r->codice ?? ''),
|
||||||
'descrizione' => (string) ($r->descrizione ?? ''),
|
'descrizione' => (string) ($r->descrizione ?? ''),
|
||||||
'prev' => (float) ($r->importo_default ?? 0),
|
'prev' => (float) ($r->importo_default ?? 0),
|
||||||
'cons' => (float) ($r->importo_consuntivo ?? 0),
|
'cons' => (float) ($r->importo_consuntivo ?? 0),
|
||||||
'prop' => (float) ($r->percentuale_condomino ?? 0),
|
'prop' => (float) ($r->percentuale_condomino ?? 0),
|
||||||
'inq' => (float) ($r->percentuale_inquilino ?? 0),
|
'inq' => (float) ($r->percentuale_inquilino ?? 0),
|
||||||
'attiva' => (bool) ($r->attiva ?? false),
|
'attiva' => (bool) ($r->attiva ?? false),
|
||||||
];
|
];
|
||||||
})->all();
|
})->all();
|
||||||
|
|
||||||
|
|
@ -525,11 +524,11 @@ public function getDettaglioTabellaFocus(): array
|
||||||
$totCons = (float) $records->sum(fn($r) => (float) ($r->importo_consuntivo ?? 0));
|
$totCons = (float) $records->sum(fn($r) => (float) ($r->importo_consuntivo ?? 0));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'label' => $label,
|
'label' => $label,
|
||||||
'count' => count($voci),
|
'count' => count($voci),
|
||||||
'totale_prev' => $totPrev,
|
'totale_prev' => $totPrev,
|
||||||
'totale_cons' => $totCons,
|
'totale_cons' => $totCons,
|
||||||
'voci' => $voci,
|
'voci' => $voci,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,7 +550,7 @@ public function updatedPresetRipartizione(string $value): void
|
||||||
|
|
||||||
public function updatedGestioneContabileId($value): void
|
public function updatedGestioneContabileId($value): void
|
||||||
{
|
{
|
||||||
$id = is_numeric($value) ? (int) $value : null;
|
$id = is_numeric($value) ? (int) $value : null;
|
||||||
$this->gestioneContabileId = $id && $id > 0 ? $id : null;
|
$this->gestioneContabileId = $id && $id > 0 ? $id : null;
|
||||||
$this->resetPage();
|
$this->resetPage();
|
||||||
}
|
}
|
||||||
|
|
@ -580,14 +579,14 @@ public function getGestioniOptions(): array
|
||||||
->get(['id', 'anno_gestione', 'tipo_gestione', 'stato'])
|
->get(['id', 'anno_gestione', 'tipo_gestione', 'stato'])
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$options = [];
|
$options = [];
|
||||||
$seenYears = [];
|
$seenYears = [];
|
||||||
foreach ($gestioni as $g) {
|
foreach ($gestioni as $g) {
|
||||||
$anno = (string) ($g->anno_gestione ?? '');
|
$anno = (string) ($g->anno_gestione ?? '');
|
||||||
if ($anno === '' || isset($seenYears[$anno])) {
|
if ($anno === '' || isset($seenYears[$anno])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$seenYears[$anno] = true;
|
$seenYears[$anno] = true;
|
||||||
$options[(int) $g->id] = $anno;
|
$options[(int) $g->id] = $anno;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -603,10 +602,10 @@ private function normalizeTipoGestione(string $tipo): string
|
||||||
private function getLegacyTipoValues(string $tipo): array
|
private function getLegacyTipoValues(string $tipo): array
|
||||||
{
|
{
|
||||||
return match ($tipo) {
|
return match ($tipo) {
|
||||||
'ordinaria' => ['ordinaria', 'O', 'o'],
|
'ordinaria' => ['ordinaria', 'O', 'o'],
|
||||||
'riscaldamento' => ['riscaldamento', 'R', 'r'],
|
'riscaldamento' => ['riscaldamento', 'R', 'r'],
|
||||||
'straordinaria' => ['straordinaria', 'S', 's'],
|
'straordinaria' => ['straordinaria', 'S', 's'],
|
||||||
default => [$tipo],
|
default => [$tipo],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -636,8 +635,8 @@ private function applyTipoGestioneFilter(Builder $q, string $tipoTab, bool $hasL
|
||||||
private function getTabellaGroupTitle(VoceSpesa $record): string
|
private function getTabellaGroupTitle(VoceSpesa $record): string
|
||||||
{
|
{
|
||||||
$tabella = $this->resolveTabellaForAnno($record->tabellaMillesimaleDefault, (int) $record->stabile_id);
|
$tabella = $this->resolveTabellaForAnno($record->tabellaMillesimaleDefault, (int) $record->stabile_id);
|
||||||
$code = trim((string) ($tabella?->codice_tabella ?? ''));
|
$code = trim((string) ($tabella?->codice_tabella ?? ''));
|
||||||
$name = trim((string) ($tabella?->denominazione ?? ''));
|
$name = trim((string) ($tabella?->denominazione ?? ''));
|
||||||
if ($name === '') {
|
if ($name === '') {
|
||||||
$name = trim((string) ($tabella?->nome_tabella ?? ''));
|
$name = trim((string) ($tabella?->nome_tabella ?? ''));
|
||||||
}
|
}
|
||||||
|
|
@ -724,12 +723,12 @@ private function getNordOptions(): array
|
||||||
|
|
||||||
$values = $q->selectRaw('DISTINCT ' . $expr . ' as nord_val')
|
$values = $q->selectRaw('DISTINCT ' . $expr . ' as nord_val')
|
||||||
->pluck('nord_val')
|
->pluck('nord_val')
|
||||||
->filter(fn ($v) => $v !== null && $v !== '')
|
->filter(fn($v) => $v !== null && $v !== '')
|
||||||
->unique()
|
->unique()
|
||||||
->sort()
|
->sort()
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
return $values->mapWithKeys(fn ($v) => [(string) $v => (string) $v])->all();
|
return $values->mapWithKeys(fn($v) => [(string) $v => (string) $v])->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int, string> */
|
/** @return array<int, string> */
|
||||||
|
|
@ -740,11 +739,11 @@ private function getFirstTabellaCodesToHide(int $stabileId): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$tipoTab = $this->tipoGestioneTab ?: 'ordinaria';
|
$tipoTab = $this->tipoGestioneTab ?: 'ordinaria';
|
||||||
$tipo = $this->normalizeTipoGestione($tipoTab);
|
$tipo = $this->normalizeTipoGestione($tipoTab);
|
||||||
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
||||||
$hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione');
|
$hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione');
|
||||||
$anno = AnnoGestioneContext::resolveActiveAnno($user);
|
$anno = AnnoGestioneContext::resolveActiveAnno($user);
|
||||||
|
|
||||||
$q = TabellaMillesimale::query()->where('stabile_id', $stabileId);
|
$q = TabellaMillesimale::query()->where('stabile_id', $stabileId);
|
||||||
|
|
||||||
|
|
@ -824,7 +823,7 @@ public function applyPresetRipartizione(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$categoria = $this->mapVoceCategoriaToConfedilizia((string) $record->categoria);
|
$categoria = $this->mapVoceCategoriaToConfedilizia((string) $record->categoria);
|
||||||
$default = RipartizioneSpeseInquilini::getDefaultConfedilizia($categoria);
|
$default = RipartizioneSpeseInquilini::getDefaultConfedilizia($categoria);
|
||||||
|
|
||||||
$record->update([
|
$record->update([
|
||||||
'percentuale_condomino' => (float) ($default['proprietario'] ?? 100),
|
'percentuale_condomino' => (float) ($default['proprietario'] ?? 100),
|
||||||
|
|
@ -838,23 +837,23 @@ public function applyPresetRipartizione(): void
|
||||||
private function mapVoceCategoriaToConfedilizia(string $categoriaVoce): string
|
private function mapVoceCategoriaToConfedilizia(string $categoriaVoce): string
|
||||||
{
|
{
|
||||||
return match ($categoriaVoce) {
|
return match ($categoriaVoce) {
|
||||||
'riscaldamento' => 'B',
|
'riscaldamento' => 'B',
|
||||||
'ascensore' => 'C',
|
'ascensore' => 'C',
|
||||||
'illuminazione' => 'D',
|
'illuminazione' => 'D',
|
||||||
'pulizia' => 'E',
|
'pulizia' => 'E',
|
||||||
'acqua' => 'G',
|
'acqua' => 'G',
|
||||||
'energia_elettrica' => 'L',
|
'energia_elettrica' => 'L',
|
||||||
'gas' => 'B',
|
'gas' => 'B',
|
||||||
'giardino_verde' => 'I',
|
'giardino_verde' => 'I',
|
||||||
'sicurezza' => 'N',
|
'sicurezza' => 'N',
|
||||||
'amministrazione' => 'O',
|
'amministrazione' => 'O',
|
||||||
'assicurazioni' => 'A',
|
'assicurazioni' => 'A',
|
||||||
'tasse_tributi' => 'O',
|
'tasse_tributi' => 'O',
|
||||||
'spese_legali' => 'O',
|
'spese_legali' => 'O',
|
||||||
'manutenzione_ordinaria' => 'A',
|
'manutenzione_ordinaria' => 'A',
|
||||||
'manutenzione_straordinaria' => 'A',
|
'manutenzione_straordinaria' => 'A',
|
||||||
'lavori_miglioramento' => 'A',
|
'lavori_miglioramento' => 'A',
|
||||||
default => 'A',
|
default => 'A',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -867,13 +866,13 @@ public function setTipoGestioneTab(string $tab): void
|
||||||
|
|
||||||
$selectedYear = null;
|
$selectedYear = null;
|
||||||
if ($this->gestioneContabileId && Schema::hasTable('gestioni_contabili')) {
|
if ($this->gestioneContabileId && Schema::hasTable('gestioni_contabili')) {
|
||||||
$year = GestioneContabile::query()->whereKey($this->gestioneContabileId)->value('anno_gestione');
|
$year = GestioneContabile::query()->whereKey($this->gestioneContabileId)->value('anno_gestione');
|
||||||
$selectedYear = is_numeric($year) ? (int) $year : null;
|
$selectedYear = is_numeric($year) ? (int) $year : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->tipoGestioneTab = $tab;
|
$this->tipoGestioneTab = $tab;
|
||||||
|
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$stabileId = $user instanceof User ? StabileContext::resolveActiveStabileId($user) : null;
|
$stabileId = $user instanceof User ? StabileContext::resolveActiveStabileId($user) : null;
|
||||||
if ($stabileId && $selectedYear) {
|
if ($stabileId && $selectedYear) {
|
||||||
$this->gestioneContabileId = $this->resolveGestioneIdForAnnoTipo(
|
$this->gestioneContabileId = $this->resolveGestioneIdForAnnoTipo(
|
||||||
|
|
@ -954,7 +953,7 @@ protected function getTableQuery(): Builder
|
||||||
|
|
||||||
if ($this->hideFirstTwoTabelle) {
|
if ($this->hideFirstTwoTabelle) {
|
||||||
$hidden = $this->getFirstTabellaCodesToHide($activeStabileId);
|
$hidden = $this->getFirstTabellaCodesToHide($activeStabileId);
|
||||||
if (!empty($hidden)) {
|
if (! empty($hidden)) {
|
||||||
$query->where(function (Builder $q) use ($hidden) {
|
$query->where(function (Builder $q) use ($hidden) {
|
||||||
$q->whereNull('tm.codice_tabella')->orWhereNotIn('tm.codice_tabella', $hidden);
|
$q->whereNull('tm.codice_tabella')->orWhereNotIn('tm.codice_tabella', $hidden);
|
||||||
});
|
});
|
||||||
|
|
@ -1044,7 +1043,7 @@ public function getTotaleConsuntivo(): float
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tipoTab = $this->tipoGestioneTab;
|
$tipoTab = $this->tipoGestioneTab;
|
||||||
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
||||||
|
|
||||||
$q = VoceSpesa::query()->where('voci_spesa.stabile_id', $activeStabileId);
|
$q = VoceSpesa::query()->where('voci_spesa.stabile_id', $activeStabileId);
|
||||||
|
|
@ -1125,9 +1124,9 @@ public function getTotaliPerTabella(): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$tipoTab = $this->tipoGestioneTab;
|
$tipoTab = $this->tipoGestioneTab;
|
||||||
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
||||||
$q = VoceSpesa::query()
|
$q = VoceSpesa::query()
|
||||||
->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id')
|
->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id')
|
||||||
->where('voci_spesa.stabile_id', $activeStabileId)
|
->where('voci_spesa.stabile_id', $activeStabileId)
|
||||||
->when(Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId, function (Builder $qq) {
|
->when(Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId, function (Builder $qq) {
|
||||||
|
|
@ -1145,7 +1144,7 @@ public function getTotaliPerTabella(): array
|
||||||
->orderBy('tabella')
|
->orderBy('tabella')
|
||||||
->get()
|
->get()
|
||||||
->map(fn($r) => [
|
->map(fn($r) => [
|
||||||
'tabella' => (string) $r->tabella,
|
'tabella' => (string) $r->tabella,
|
||||||
'totale_prev' => (float) $r->totale_prev,
|
'totale_prev' => (float) $r->totale_prev,
|
||||||
'totale_cons' => (float) $r->totale_cons,
|
'totale_cons' => (float) $r->totale_cons,
|
||||||
])
|
])
|
||||||
|
|
@ -1160,25 +1159,25 @@ public function table(Table $table): Table
|
||||||
Group::make('tabellaMillesimaleDefault.codice_tabella')
|
Group::make('tabellaMillesimaleDefault.codice_tabella')
|
||||||
->label('Tabella')
|
->label('Tabella')
|
||||||
->titlePrefixedWithLabel(false)
|
->titlePrefixedWithLabel(false)
|
||||||
->getTitleFromRecordUsing(fn (VoceSpesa $record): string => $this->getTabellaGroupTitle($record)),
|
->getTitleFromRecordUsing(fn(VoceSpesa $record): string => $this->getTabellaGroupTitle($record)),
|
||||||
])
|
])
|
||||||
->defaultGroup('tabellaMillesimaleDefault.codice_tabella')
|
->defaultGroup('tabellaMillesimaleDefault.codice_tabella')
|
||||||
->groupingSettingsHidden()
|
->groupingSettingsHidden()
|
||||||
->filters([
|
->filters([
|
||||||
SelectFilter::make('tabella')
|
SelectFilter::make('tabella')
|
||||||
->label('Tabella')
|
->label('Tabella')
|
||||||
->options(fn (): array => $this->getTabelleMillesimaliOptions())
|
->options(fn(): array=> $this->getTabelleMillesimaliOptions())
|
||||||
->searchable()
|
->searchable()
|
||||||
->query(function (Builder $query, array $data): Builder {
|
->query(function (Builder $query, array $data): Builder {
|
||||||
$value = $data['value'] ?? null;
|
$value = $data['value'] ?? null;
|
||||||
if (!is_numeric($value)) {
|
if (! is_numeric($value)) {
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
return $query->where('voci_spesa.tabella_millesimale_default_id', (int) $value);
|
return $query->where('voci_spesa.tabella_millesimale_default_id', (int) $value);
|
||||||
}),
|
}),
|
||||||
SelectFilter::make('nord')
|
SelectFilter::make('nord')
|
||||||
->label('NORD')
|
->label('NORD')
|
||||||
->options(fn (): array => $this->getNordOptions())
|
->options(fn(): array=> $this->getNordOptions())
|
||||||
->query(function (Builder $query, array $data): Builder {
|
->query(function (Builder $query, array $data): Builder {
|
||||||
$value = $data['value'] ?? null;
|
$value = $data['value'] ?? null;
|
||||||
if ($value === null || $value === '') {
|
if ($value === null || $value === '') {
|
||||||
|
|
@ -1230,8 +1229,8 @@ public function table(Table $table): Table
|
||||||
->hiddenLabel()
|
->hiddenLabel()
|
||||||
->icon('heroicon-o-document-chart-bar')
|
->icon('heroicon-o-document-chart-bar')
|
||||||
->tooltip('Mastrino')
|
->tooltip('Mastrino')
|
||||||
->url(fn (VoceSpesa $record) => VoceSpesaMastrino::getUrl([
|
->url(fn(VoceSpesa $record) => VoceSpesaMastrino::getUrl([
|
||||||
'record' => (int) $record->id,
|
'record' => (int) $record->id,
|
||||||
'gestione' => $this->gestioneContabileId,
|
'gestione' => $this->gestioneContabileId,
|
||||||
], panel: 'admin-filament')),
|
], panel: 'admin-filament')),
|
||||||
Action::make('modifica')
|
Action::make('modifica')
|
||||||
|
|
@ -1284,18 +1283,18 @@ public function table(Table $table): Table
|
||||||
->fillForm(function (VoceSpesa $record): array {
|
->fillForm(function (VoceSpesa $record): array {
|
||||||
return [
|
return [
|
||||||
'tabella_millesimale_default_id' => $record->tabella_millesimale_default_id,
|
'tabella_millesimale_default_id' => $record->tabella_millesimale_default_id,
|
||||||
'attiva' => $record->attiva ? 1 : 0,
|
'attiva' => $record->attiva ? 1 : 0,
|
||||||
'categoria' => $record->categoria,
|
'categoria' => $record->categoria,
|
||||||
'tipo_gestione' => $record->tipo_gestione,
|
'tipo_gestione' => $record->tipo_gestione,
|
||||||
'importo_default' => $record->importo_default,
|
'importo_default' => $record->importo_default,
|
||||||
'importo_consuntivo' => $record->importo_consuntivo,
|
'importo_consuntivo' => $record->importo_consuntivo,
|
||||||
'percentuale_condomino' => $record->percentuale_condomino,
|
'percentuale_condomino' => $record->percentuale_condomino,
|
||||||
'percentuale_inquilino' => $record->percentuale_inquilino,
|
'percentuale_inquilino' => $record->percentuale_inquilino,
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->action(function (array $data, VoceSpesa $record): void {
|
->action(function (array $data, VoceSpesa $record): void {
|
||||||
$cond = isset($data['percentuale_condomino']) ? (float) $data['percentuale_condomino'] : null;
|
$cond = isset($data['percentuale_condomino']) ? (float) $data['percentuale_condomino'] : null;
|
||||||
$inq = isset($data['percentuale_inquilino']) ? (float) $data['percentuale_inquilino'] : null;
|
$inq = isset($data['percentuale_inquilino']) ? (float) $data['percentuale_inquilino'] : null;
|
||||||
if ($cond !== null && $inq === null) {
|
if ($cond !== null && $inq === null) {
|
||||||
$inq = max(0.0, 100.0 - $cond);
|
$inq = max(0.0, 100.0 - $cond);
|
||||||
}
|
}
|
||||||
|
|
@ -1303,13 +1302,13 @@ public function table(Table $table): Table
|
||||||
'tabella_millesimale_default_id' => is_numeric($data['tabella_millesimale_default_id'] ?? null)
|
'tabella_millesimale_default_id' => is_numeric($data['tabella_millesimale_default_id'] ?? null)
|
||||||
? (int) $data['tabella_millesimale_default_id']
|
? (int) $data['tabella_millesimale_default_id']
|
||||||
: null,
|
: null,
|
||||||
'attiva' => (bool) ($data['attiva'] ?? true),
|
'attiva' => (bool) ($data['attiva'] ?? true),
|
||||||
'categoria' => $data['categoria'] ?? $record->categoria,
|
'categoria' => $data['categoria'] ?? $record->categoria,
|
||||||
'tipo_gestione' => $data['tipo_gestione'] ?? $record->tipo_gestione,
|
'tipo_gestione' => $data['tipo_gestione'] ?? $record->tipo_gestione,
|
||||||
'importo_default' => $data['importo_default'] ?? $record->importo_default,
|
'importo_default' => $data['importo_default'] ?? $record->importo_default,
|
||||||
'importo_consuntivo' => $data['importo_consuntivo'] ?? $record->importo_consuntivo,
|
'importo_consuntivo' => $data['importo_consuntivo'] ?? $record->importo_consuntivo,
|
||||||
'percentuale_condomino' => $cond ?? $record->percentuale_condomino,
|
'percentuale_condomino' => $cond ?? $record->percentuale_condomino,
|
||||||
'percentuale_inquilino' => $inq ?? $record->percentuale_inquilino,
|
'percentuale_inquilino' => $inq ?? $record->percentuale_inquilino,
|
||||||
]);
|
]);
|
||||||
$this->dispatch('$refresh');
|
$this->dispatch('$refresh');
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ public function mount(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->fornitoreId = (int) $fornitore->id;
|
$this->fornitoreId = (int) $fornitore->id;
|
||||||
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
||||||
$this->amazonConfigured = app(AmazonCreatorsApiService::class)->isConfigured();
|
$this->amazonConfigured = app(AmazonCreatorsApiService::class)->isConfigured();
|
||||||
$this->refreshRows();
|
$this->refreshRows();
|
||||||
}
|
}
|
||||||
|
|
@ -200,8 +200,8 @@ public function saveAmazonCandidate(string $asin): void
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title(! empty($result['created']) ? 'Prodotto Amazon memorizzato' : 'Prodotto Amazon aggiornato')
|
->title(! empty($result['created']) ? 'Prodotto Amazon memorizzato' : 'Prodotto Amazon aggiornato')
|
||||||
->body($product instanceof Product
|
->body($product instanceof Product
|
||||||
? 'Articolo salvato nel catalogo come candidato da verificare: ' . ((string) ($product->internal_code ?? '') !== '' ? $product->internal_code . ' · ' : '') . (string) ($product->name ?? '')
|
? 'Articolo salvato nel catalogo come candidato da verificare: ' . ((string) ($product->internal_code ?? '') !== '' ? $product->internal_code . ' · ' : '') . (string) ($product->name ?? '')
|
||||||
: 'Import completato.')
|
: 'Import completato.')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore;
|
use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore;
|
||||||
use App\Modules\Contabilita\Models\PianoConti;
|
use App\Modules\Contabilita\Models\PianoConti;
|
||||||
use App\Services\Catalog\FornitoreProductCatalogService;
|
use App\Services\Catalog\FornitoreProductCatalogService;
|
||||||
use App\Services\Catalog\ProductHubViewService;
|
|
||||||
use App\Services\Catalog\ProductAssetIngestionService;
|
use App\Services\Catalog\ProductAssetIngestionService;
|
||||||
|
use App\Services\Catalog\ProductHubViewService;
|
||||||
use App\Services\Catalog\ProductOfferService;
|
use App\Services\Catalog\ProductOfferService;
|
||||||
use App\Services\Consumi\AcquaPdfTextParser;
|
use App\Services\Consumi\AcquaPdfTextParser;
|
||||||
use App\Services\Consumi\ConsumiAcquaIngestionService;
|
use App\Services\Consumi\ConsumiAcquaIngestionService;
|
||||||
|
|
|
||||||
33
app/Filament/Pages/Mobile/DocumentiMobile.php
Normal file
33
app/Filament/Pages/Mobile/DocumentiMobile.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 DocumentiMobile extends Page
|
||||||
|
{
|
||||||
|
protected static ?string $navigationLabel = 'Documenti Mobile';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Documenti Mobile';
|
||||||
|
|
||||||
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-qr-code';
|
||||||
|
|
||||||
|
protected static UnitEnum|string|null $navigationGroup = 'Mobile';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 27;
|
||||||
|
|
||||||
|
protected static ?string $slug = 'mobile/documenti';
|
||||||
|
|
||||||
|
protected string $view = 'filament.pages.mobile.documenti-mobile';
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
return $user instanceof User
|
||||||
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -744,7 +744,7 @@ public function getGenericLabelRowsProperty(): array
|
||||||
->get()
|
->get()
|
||||||
->filter(fn(DocumentoArchivioFisicoItem $item): bool => (bool) data_get($item->metadati, 'generic_label', false))
|
->filter(fn(DocumentoArchivioFisicoItem $item): bool => (bool) data_get($item->metadati, 'generic_label', false))
|
||||||
->map(function (DocumentoArchivioFisicoItem $item): array {
|
->map(function (DocumentoArchivioFisicoItem $item): array {
|
||||||
$printOptions = $this->buildGenericLabelPrintOptions((int) $item->id);
|
$printOptions = $this->buildGenericLabelPrintOptions((int) $item->id);
|
||||||
$defaultPrintOption = ($item->modello_etichetta ?: '11354') === '99014' ? '99014' : '11354';
|
$defaultPrintOption = ($item->modello_etichetta ?: '11354') === '99014' ? '99014' : '11354';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -777,7 +777,7 @@ private function buildGenericLabelPrintOptions(int $itemId): array
|
||||||
$baseUrl = route('filament.archivio-fisico.etichetta', ['item' => $itemId]);
|
$baseUrl = route('filament.archivio-fisico.etichetta', ['item' => $itemId]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'11354' => [
|
'11354' => [
|
||||||
'label' => 'DYMO 11354 orizzontale',
|
'label' => 'DYMO 11354 orizzontale',
|
||||||
'url' => $baseUrl . '?format=11354&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=-1',
|
'url' => $baseUrl . '?format=11354&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=-1',
|
||||||
],
|
],
|
||||||
|
|
@ -785,7 +785,7 @@ private function buildGenericLabelPrintOptions(int $itemId): array
|
||||||
'label' => 'DYMO 11354 verticale',
|
'label' => 'DYMO 11354 verticale',
|
||||||
'url' => $baseUrl . '?format=11354&autoprint=1&dymo_fix=1&dymo_rot=90&dymo_dx=0&dymo_dy=0',
|
'url' => $baseUrl . '?format=11354&autoprint=1&dymo_fix=1&dymo_rot=90&dymo_dx=0&dymo_dy=0',
|
||||||
],
|
],
|
||||||
'99014' => [
|
'99014' => [
|
||||||
'label' => 'DYMO 99014 larga',
|
'label' => 'DYMO 99014 larga',
|
||||||
'url' => $baseUrl . '?format=99014&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=3.5&dymo_dy=3.5',
|
'url' => $baseUrl . '?format=99014&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=3.5&dymo_dy=3.5',
|
||||||
],
|
],
|
||||||
|
|
@ -836,7 +836,7 @@ public function getGenericLabelTemplateOptionsProperty(): array
|
||||||
public function getGenericLabelTemplateFieldOptionsProperty(): array
|
public function getGenericLabelTemplateFieldOptionsProperty(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'mnemonic_code' => 'Codice mnemonico',
|
'mnemonic_code' => 'Codice mnemonico',
|
||||||
'titolo' => 'Titolo etichetta',
|
'titolo' => 'Titolo etichetta',
|
||||||
'linea_secondaria' => 'Seconda riga',
|
'linea_secondaria' => 'Seconda riga',
|
||||||
'tipo_label' => 'Tipologia contenitore',
|
'tipo_label' => 'Tipologia contenitore',
|
||||||
|
|
@ -862,11 +862,33 @@ public function getGenericLabelStableFieldOptionsProperty(): array
|
||||||
'stabile_code' => 'Codice stabile',
|
'stabile_code' => 'Codice stabile',
|
||||||
'stabile_address' => 'Indirizzo stabile',
|
'stabile_address' => 'Indirizzo stabile',
|
||||||
'stabile_summary' => 'Stabile compatto',
|
'stabile_summary' => 'Stabile compatto',
|
||||||
'blank_line' => 'Riga bianca scrivibile',
|
|
||||||
'none' => 'Nessuna riga',
|
'none' => 'Nessuna riga',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
public function getGenericLabelLineModeOptionsProperty(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'source' => 'Campo archivio',
|
||||||
|
'text' => 'Testo libero',
|
||||||
|
'blank_line' => 'Riga bianca scrivibile',
|
||||||
|
'none' => 'Nessuna riga',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
public function getGenericLabelFontSizeOptionsProperty(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'xs' => 'Extra piccolo',
|
||||||
|
'sm' => 'Piccolo',
|
||||||
|
'md' => 'Medio',
|
||||||
|
'lg' => 'Grande',
|
||||||
|
'xl' => 'Extra grande',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function getGenericLabelPreviewProperty(): array
|
public function getGenericLabelPreviewProperty(): array
|
||||||
{
|
{
|
||||||
|
|
@ -878,10 +900,10 @@ public function getGenericLabelPreviewProperty(): array
|
||||||
$mnemonicCode = (string) ($this->mnemonicCodeHint ?? 'GER00079');
|
$mnemonicCode = (string) ($this->mnemonicCodeHint ?? 'GER00079');
|
||||||
}
|
}
|
||||||
|
|
||||||
$stabileCode = '';
|
$stabileCode = '';
|
||||||
$stabileAddress = '';
|
$stabileAddress = '';
|
||||||
if ($stabile instanceof Stabile) {
|
if ($stabile instanceof Stabile) {
|
||||||
$stabileCode = trim((string) ($stabile->codice_stabile ?? ''));
|
$stabileCode = trim((string) ($stabile->codice_stabile ?? ''));
|
||||||
$stabileAddress = trim((string) ($stabile->indirizzo_completo ?? ''));
|
$stabileAddress = trim((string) ($stabile->indirizzo_completo ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -925,14 +947,7 @@ public function getGenericLabelPreviewProperty(): array
|
||||||
'amazon_url' => $amazonUrl,
|
'amazon_url' => $amazonUrl,
|
||||||
];
|
];
|
||||||
|
|
||||||
$stableLineOneSource = (string) ($this->genericLabelForm['stable_line_one_source'] ?? 'stabile_summary');
|
$stableLines = $this->buildGenericLabelLineRows($payload);
|
||||||
$stableLineTwoSource = (string) ($this->genericLabelForm['stable_line_two_source'] ?? 'blank_line');
|
|
||||||
$blankLinesCount = max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2)));
|
|
||||||
|
|
||||||
$stableLines = [
|
|
||||||
$this->resolveGenericLabelStableLineValue($stableLineOneSource, $payload),
|
|
||||||
$this->resolveGenericLabelStableLineValue($stableLineTwoSource, $payload),
|
|
||||||
];
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'template_name' => $templateName,
|
'template_name' => $templateName,
|
||||||
|
|
@ -948,8 +963,15 @@ public function getGenericLabelPreviewProperty(): array
|
||||||
],
|
],
|
||||||
'payload' => $payload,
|
'payload' => $payload,
|
||||||
'mnemonic_code' => $mnemonicCode,
|
'mnemonic_code' => $mnemonicCode,
|
||||||
|
'font_sizes' => [
|
||||||
|
'mnemonic' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')),
|
||||||
|
'title' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')),
|
||||||
|
'subtitle' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')),
|
||||||
|
'lines' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')),
|
||||||
|
'note' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')),
|
||||||
|
],
|
||||||
'stable_lines' => $stableLines,
|
'stable_lines' => $stableLines,
|
||||||
'blank_lines' => array_fill(0, $blankLinesCount, ''),
|
'blank_lines' => [],
|
||||||
'amazon_url' => $amazonUrl,
|
'amazon_url' => $amazonUrl,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -1284,18 +1306,24 @@ public function saveGenericArchiveLabel(): void
|
||||||
'linea_secondaria' => $this->normalizeNullableText($this->genericLabelForm['linea_secondaria'] ?? null),
|
'linea_secondaria' => $this->normalizeNullableText($this->genericLabelForm['linea_secondaria'] ?? null),
|
||||||
'amazon_url' => $amazonUrl,
|
'amazon_url' => $amazonUrl,
|
||||||
'generic_label_template' => [
|
'generic_label_template' => [
|
||||||
'preset_key' => $this->normalizeNullableText($this->genericLabelForm['preset_key'] ?? null),
|
'preset_key' => $this->normalizeNullableText($this->genericLabelForm['preset_key'] ?? null),
|
||||||
'template_name' => $this->normalizeNullableText($this->genericLabelForm['template_name'] ?? null) ?: 'classic-right',
|
'template_name' => $this->normalizeNullableText($this->genericLabelForm['template_name'] ?? null) ?: 'classic-right',
|
||||||
'mnemonic_code' => $this->normalizeNullableText($this->genericLabelForm['mnemonic_code'] ?? null) ?: $this->mnemonicCodeHint,
|
'mnemonic_code' => $this->normalizeNullableText($this->genericLabelForm['mnemonic_code'] ?? null) ?: $this->mnemonicCodeHint,
|
||||||
'title_source' => $this->normalizeNullableText($this->genericLabelForm['title_source'] ?? null) ?: 'titolo',
|
'title_source' => $this->normalizeNullableText($this->genericLabelForm['title_source'] ?? null) ?: 'titolo',
|
||||||
'subtitle_source' => $this->normalizeNullableText($this->genericLabelForm['subtitle_source'] ?? null) ?: 'linea_secondaria',
|
'subtitle_source' => $this->normalizeNullableText($this->genericLabelForm['subtitle_source'] ?? null) ?: 'linea_secondaria',
|
||||||
'meta_source' => $this->normalizeNullableText($this->genericLabelForm['meta_source'] ?? null) ?: 'percorso_compatto',
|
'meta_source' => $this->normalizeNullableText($this->genericLabelForm['meta_source'] ?? null) ?: 'percorso_compatto',
|
||||||
'note_source' => $this->normalizeNullableText($this->genericLabelForm['note_source'] ?? null) ?: 'note',
|
'note_source' => $this->normalizeNullableText($this->genericLabelForm['note_source'] ?? null) ?: 'note',
|
||||||
'stable_line_one_source' => $this->normalizeNullableText($this->genericLabelForm['stable_line_one_source'] ?? null) ?: 'stabile_summary',
|
'stable_line_one_source' => $this->normalizeNullableText($this->genericLabelForm['stable_line_one_source'] ?? null) ?: 'stabile_summary',
|
||||||
'stable_line_two_source' => $this->normalizeNullableText($this->genericLabelForm['stable_line_two_source'] ?? null) ?: 'blank_line',
|
'stable_line_two_source' => $this->normalizeNullableText($this->genericLabelForm['stable_line_two_source'] ?? null) ?: 'blank_line',
|
||||||
'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))),
|
'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))),
|
||||||
'qr_position' => $this->normalizeNullableText($this->genericLabelForm['qr_position'] ?? null) ?: 'right-center',
|
'qr_position' => $this->normalizeNullableText($this->genericLabelForm['qr_position'] ?? null) ?: 'right-center',
|
||||||
'qr_scale' => $this->normalizeNullableText($this->genericLabelForm['qr_scale'] ?? null) ?: 'md',
|
'qr_scale' => $this->normalizeNullableText($this->genericLabelForm['qr_scale'] ?? null) ?: 'md',
|
||||||
|
'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')),
|
||||||
|
'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')),
|
||||||
|
'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')),
|
||||||
|
'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')),
|
||||||
|
'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')),
|
||||||
|
'line_rows' => $this->serializeGenericLabelLineRows(),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
@ -1407,19 +1435,42 @@ public function movePhysicalArchiveItem(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$source->forceFill([
|
$supportoFisico = $this->normalizeNullableText($this->physicalArchiveMoveForm['supporto_fisico'] ?? null);
|
||||||
'parent_id' => (int) $target->id,
|
$magazzino = $this->normalizeNullableText($this->physicalArchiveMoveForm['magazzino'] ?? null) ?? $target->magazzino ?? $source->magazzino;
|
||||||
'updated_by' => Auth::id(),
|
$scaffale = $this->normalizeNullableText($this->physicalArchiveMoveForm['scaffale'] ?? null) ?? $target->scaffale ?? $source->scaffale;
|
||||||
])->save();
|
$ripiano = $this->normalizeNullableText($this->physicalArchiveMoveForm['ripiano'] ?? null) ?? $target->ripiano ?? $source->ripiano;
|
||||||
|
$ubicazioneDettaglio = $this->normalizeNullableText($this->physicalArchiveMoveForm['ubicazione_dettaglio'] ?? null) ?? $target->ubicazione_dettaglio ?? $source->ubicazione_dettaglio;
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'parent_id' => (int) $target->id,
|
||||||
|
'updated_by' => Auth::id(),
|
||||||
|
'magazzino' => $magazzino,
|
||||||
|
'scaffale' => $scaffale,
|
||||||
|
'ripiano' => $ripiano,
|
||||||
|
'ubicazione_dettaglio' => $ubicazioneDettaglio,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($supportoFisico !== null) {
|
||||||
|
$payload['supporto_fisico'] = $supportoFisico;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source->forceFill($payload)->save();
|
||||||
|
|
||||||
|
$ubicazione = collect([$magazzino, $scaffale, $ripiano, $ubicazioneDettaglio])->filter()->implode(' · ');
|
||||||
|
|
||||||
$this->physicalArchiveMoveForm = [
|
$this->physicalArchiveMoveForm = [
|
||||||
'source_code' => '',
|
'source_code' => '',
|
||||||
'target_code' => '',
|
'target_code' => '',
|
||||||
|
'supporto_fisico' => '',
|
||||||
|
'magazzino' => '',
|
||||||
|
'scaffale' => '',
|
||||||
|
'ripiano' => '',
|
||||||
|
'ubicazione_dettaglio' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Movimentazione registrata')
|
->title('Movimentazione registrata')
|
||||||
->body($source->codice_univoco . ' → ' . $target->codice_univoco)
|
->body(trim($source->codice_univoco . ' → ' . $target->codice_univoco . ($ubicazione !== '' ? ' · ' . $ubicazione : '')))
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}
|
}
|
||||||
|
|
@ -1854,38 +1905,60 @@ private function initializePhysicalArchiveForms(): void
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->physicalArchiveMoveForm = [
|
$this->physicalArchiveMoveForm = [
|
||||||
'source_code' => '',
|
'source_code' => '',
|
||||||
'target_code' => '',
|
'target_code' => '',
|
||||||
|
'supporto_fisico' => '',
|
||||||
|
'magazzino' => '',
|
||||||
|
'scaffale' => '',
|
||||||
|
'ripiano' => '',
|
||||||
|
'ubicazione_dettaglio' => '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function initializeGenericLabelForm(): void
|
private function initializeGenericLabelForm(): void
|
||||||
{
|
{
|
||||||
$this->genericLabelForm = [
|
$this->genericLabelForm = [
|
||||||
'preset_key' => '',
|
'preset_key' => '',
|
||||||
'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'),
|
'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'),
|
||||||
'titolo' => '',
|
'titolo' => '',
|
||||||
'linea_secondaria' => '',
|
'linea_secondaria' => '',
|
||||||
'note' => '',
|
'note' => '',
|
||||||
'tipo_item' => 'scatola',
|
'tipo_item' => 'scatola',
|
||||||
'supporto_fisico' => 'Scatola',
|
'supporto_fisico' => 'Scatola',
|
||||||
'parent_id' => null,
|
'parent_id' => null,
|
||||||
'magazzino' => '',
|
'magazzino' => '',
|
||||||
'scaffale' => '',
|
'scaffale' => '',
|
||||||
'ripiano' => '',
|
'ripiano' => '',
|
||||||
'ubicazione_dettaglio' => '',
|
'ubicazione_dettaglio' => '',
|
||||||
'modello_etichetta' => '11354',
|
'modello_etichetta' => '11354',
|
||||||
'amazon_url' => '',
|
'amazon_url' => '',
|
||||||
'template_name' => 'archive-99014',
|
'template_name' => 'archive-99014',
|
||||||
'title_source' => 'titolo',
|
'title_source' => 'titolo',
|
||||||
'subtitle_source' => 'linea_secondaria',
|
'subtitle_source' => 'linea_secondaria',
|
||||||
'meta_source' => 'percorso_compatto',
|
'meta_source' => 'percorso_compatto',
|
||||||
'note_source' => 'note',
|
'note_source' => 'note',
|
||||||
'stable_line_one_source' => 'stabile_summary',
|
'stable_line_one_source' => 'stabile_summary',
|
||||||
'stable_line_two_source' => 'blank_line',
|
'stable_line_two_source' => 'blank_line',
|
||||||
'blank_lines_count' => 2,
|
'blank_lines_count' => 2,
|
||||||
'qr_position' => 'bottom-right',
|
'qr_position' => 'bottom-right',
|
||||||
'qr_scale' => 'lg',
|
'qr_scale' => 'lg',
|
||||||
|
'line_1_mode' => 'source',
|
||||||
|
'line_1_source' => 'stabile_summary',
|
||||||
|
'line_1_text' => '',
|
||||||
|
'line_2_mode' => 'blank_line',
|
||||||
|
'line_2_source' => 'none',
|
||||||
|
'line_2_text' => '',
|
||||||
|
'line_3_mode' => 'blank_line',
|
||||||
|
'line_3_source' => 'none',
|
||||||
|
'line_3_text' => '',
|
||||||
|
'line_4_mode' => 'none',
|
||||||
|
'line_4_source' => 'none',
|
||||||
|
'line_4_text' => '',
|
||||||
|
'mnemonic_font_size' => 'xl',
|
||||||
|
'title_font_size' => 'xl',
|
||||||
|
'subtitle_font_size' => 'md',
|
||||||
|
'lines_font_size' => 'md',
|
||||||
|
'note_font_size' => 'sm',
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->genericLabelTemplateLabel = '';
|
$this->genericLabelTemplateLabel = '';
|
||||||
|
|
@ -2045,24 +2118,39 @@ public function applyGenericLabelPreset(string $presetKey): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->genericLabelForm = array_merge($this->genericLabelForm, [
|
$this->genericLabelForm = array_merge($this->genericLabelForm, [
|
||||||
'preset_key' => $presetKey,
|
'preset_key' => $presetKey,
|
||||||
'mnemonic_code' => (string) ($preset['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079')),
|
'mnemonic_code' => (string) ($preset['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079')),
|
||||||
'titolo' => (string) ($preset['titolo'] ?? ''),
|
'titolo' => (string) ($preset['titolo'] ?? ''),
|
||||||
'linea_secondaria' => (string) ($preset['linea_secondaria'] ?? ''),
|
'linea_secondaria' => (string) ($preset['linea_secondaria'] ?? ''),
|
||||||
'tipo_item' => (string) ($preset['tipo_item'] ?? 'scatola'),
|
'tipo_item' => (string) ($preset['tipo_item'] ?? 'scatola'),
|
||||||
'supporto_fisico' => (string) ($preset['supporto_fisico'] ?? ''),
|
'supporto_fisico' => (string) ($preset['supporto_fisico'] ?? ''),
|
||||||
'modello_etichetta' => (string) ($preset['modello_etichetta'] ?? '11354'),
|
'modello_etichetta' => (string) ($preset['modello_etichetta'] ?? '11354'),
|
||||||
'template_name' => (string) ($preset['template_name'] ?? 'classic-right'),
|
'template_name' => (string) ($preset['template_name'] ?? 'classic-right'),
|
||||||
'title_source' => (string) data_get($preset, 'sources.title', 'titolo'),
|
'title_source' => (string) data_get($preset, 'sources.title', 'titolo'),
|
||||||
'subtitle_source' => (string) data_get($preset, 'sources.subtitle', 'linea_secondaria'),
|
'subtitle_source' => (string) data_get($preset, 'sources.subtitle', 'linea_secondaria'),
|
||||||
'meta_source' => (string) data_get($preset, 'sources.meta', 'percorso_compatto'),
|
'meta_source' => (string) data_get($preset, 'sources.meta', 'percorso_compatto'),
|
||||||
'note_source' => (string) data_get($preset, 'sources.note', 'note'),
|
'note_source' => (string) data_get($preset, 'sources.note', 'note'),
|
||||||
'stable_line_one_source' => (string) ($preset['stable_line_one_source'] ?? 'stabile_summary'),
|
'stable_line_one_source' => (string) ($preset['stable_line_one_source'] ?? 'stabile_summary'),
|
||||||
'stable_line_two_source' => (string) ($preset['stable_line_two_source'] ?? 'blank_line'),
|
'stable_line_two_source' => (string) ($preset['stable_line_two_source'] ?? 'blank_line'),
|
||||||
'blank_lines_count' => max(0, min(4, (int) ($preset['blank_lines_count'] ?? 2))),
|
'blank_lines_count' => max(0, min(4, (int) ($preset['blank_lines_count'] ?? 2))),
|
||||||
'qr_position' => (string) ($preset['qr_position'] ?? 'right-center'),
|
'qr_position' => (string) ($preset['qr_position'] ?? 'right-center'),
|
||||||
'qr_scale' => (string) ($preset['qr_scale'] ?? 'md'),
|
'qr_scale' => (string) ($preset['qr_scale'] ?? 'md'),
|
||||||
|
'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['mnemonic_font_size'] ?? 'xl')),
|
||||||
|
'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['title_font_size'] ?? 'xl')),
|
||||||
|
'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['subtitle_font_size'] ?? 'md')),
|
||||||
|
'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['lines_font_size'] ?? 'md')),
|
||||||
|
'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['note_font_size'] ?? 'sm')),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$lineRows = is_array($preset['line_rows'] ?? null) ? $preset['line_rows'] : [];
|
||||||
|
|
||||||
|
for ($index = 1; $index <= 4; $index++) {
|
||||||
|
$row = $lineRows[$index - 1] ?? $this->getLegacyGenericLabelLineRow($index, $preset);
|
||||||
|
|
||||||
|
$this->genericLabelForm['line_' . $index . '_mode'] = $this->normalizeGenericLabelLineMode((string) ($row['mode'] ?? 'none'));
|
||||||
|
$this->genericLabelForm['line_' . $index . '_source'] = (string) ($row['source'] ?? 'none');
|
||||||
|
$this->genericLabelForm['line_' . $index . '_text'] = (string) ($row['text'] ?? '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applySelectedGenericLabelPreset(): void
|
public function applySelectedGenericLabelPreset(): void
|
||||||
|
|
@ -2254,20 +2342,20 @@ private function getBuiltInGenericLabelPresetDefinitions(): array
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'faldone-archivio-99014' => [
|
'faldone-archivio-99014' => [
|
||||||
'label' => 'Faldone archivio 99014',
|
'label' => 'Faldone archivio 99014',
|
||||||
'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'),
|
'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'),
|
||||||
'titolo' => 'FALDONE ARCHIVIO',
|
'titolo' => 'FALDONE ARCHIVIO',
|
||||||
'linea_secondaria' => 'Contenitore operativo con note manuali',
|
'linea_secondaria' => 'Contenitore operativo con note manuali',
|
||||||
'tipo_item' => 'faldone_anelli',
|
'tipo_item' => 'faldone_anelli',
|
||||||
'supporto_fisico' => 'Faldone con anelli',
|
'supporto_fisico' => 'Faldone con anelli',
|
||||||
'modello_etichetta' => '99014',
|
'modello_etichetta' => '99014',
|
||||||
'template_name' => 'archive-99014',
|
'template_name' => 'archive-99014',
|
||||||
'stable_line_one_source' => 'stabile_summary',
|
'stable_line_one_source' => 'stabile_summary',
|
||||||
'stable_line_two_source' => 'blank_line',
|
'stable_line_two_source' => 'blank_line',
|
||||||
'blank_lines_count' => 2,
|
'blank_lines_count' => 2,
|
||||||
'qr_position' => 'bottom-right',
|
'qr_position' => 'bottom-right',
|
||||||
'qr_scale' => 'lg',
|
'qr_scale' => 'lg',
|
||||||
'sources' => [
|
'sources' => [
|
||||||
'title' => 'titolo',
|
'title' => 'titolo',
|
||||||
'subtitle' => 'linea_secondaria',
|
'subtitle' => 'linea_secondaria',
|
||||||
'meta' => 'stabile_summary',
|
'meta' => 'stabile_summary',
|
||||||
|
|
@ -2316,20 +2404,26 @@ private function makeGenericLabelTemplateKey(string $label, array $stored): stri
|
||||||
private function buildGenericLabelTemplatePayload(string $label): array
|
private function buildGenericLabelTemplatePayload(string $label): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'label' => $label,
|
'label' => $label,
|
||||||
'mnemonic_code' => trim((string) ($this->genericLabelForm['mnemonic_code'] ?? '')),
|
'mnemonic_code' => trim((string) ($this->genericLabelForm['mnemonic_code'] ?? '')),
|
||||||
'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
|
'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
|
||||||
'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')),
|
'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')),
|
||||||
'tipo_item' => (string) ($this->genericLabelForm['tipo_item'] ?? 'scatola'),
|
'tipo_item' => (string) ($this->genericLabelForm['tipo_item'] ?? 'scatola'),
|
||||||
'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')),
|
'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')),
|
||||||
'modello_etichetta' => (string) ($this->genericLabelForm['modello_etichetta'] ?? '11354'),
|
'modello_etichetta' => (string) ($this->genericLabelForm['modello_etichetta'] ?? '11354'),
|
||||||
'template_name' => (string) ($this->genericLabelForm['template_name'] ?? 'classic-right'),
|
'template_name' => (string) ($this->genericLabelForm['template_name'] ?? 'classic-right'),
|
||||||
'stable_line_one_source' => (string) ($this->genericLabelForm['stable_line_one_source'] ?? 'stabile_summary'),
|
'stable_line_one_source' => (string) ($this->genericLabelForm['stable_line_one_source'] ?? 'stabile_summary'),
|
||||||
'stable_line_two_source' => (string) ($this->genericLabelForm['stable_line_two_source'] ?? 'blank_line'),
|
'stable_line_two_source' => (string) ($this->genericLabelForm['stable_line_two_source'] ?? 'blank_line'),
|
||||||
'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))),
|
'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))),
|
||||||
'qr_position' => (string) ($this->genericLabelForm['qr_position'] ?? 'right-center'),
|
'qr_position' => (string) ($this->genericLabelForm['qr_position'] ?? 'right-center'),
|
||||||
'qr_scale' => (string) ($this->genericLabelForm['qr_scale'] ?? 'md'),
|
'qr_scale' => (string) ($this->genericLabelForm['qr_scale'] ?? 'md'),
|
||||||
'sources' => [
|
'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')),
|
||||||
|
'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')),
|
||||||
|
'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')),
|
||||||
|
'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')),
|
||||||
|
'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')),
|
||||||
|
'line_rows' => $this->serializeGenericLabelLineRows(),
|
||||||
|
'sources' => [
|
||||||
'title' => (string) ($this->genericLabelForm['title_source'] ?? 'titolo'),
|
'title' => (string) ($this->genericLabelForm['title_source'] ?? 'titolo'),
|
||||||
'subtitle' => (string) ($this->genericLabelForm['subtitle_source'] ?? 'linea_secondaria'),
|
'subtitle' => (string) ($this->genericLabelForm['subtitle_source'] ?? 'linea_secondaria'),
|
||||||
'meta' => (string) ($this->genericLabelForm['meta_source'] ?? 'percorso_compatto'),
|
'meta' => (string) ($this->genericLabelForm['meta_source'] ?? 'percorso_compatto'),
|
||||||
|
|
@ -2376,6 +2470,92 @@ private function resolveGenericLabelStableLineValue(string $fieldKey, array $pay
|
||||||
return trim((string) ($payload[$fieldKey] ?? ''));
|
return trim((string) ($payload[$fieldKey] ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $payload
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function buildGenericLabelLineRows(array $payload): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
for ($index = 1; $index <= 4; $index++) {
|
||||||
|
$mode = $this->normalizeGenericLabelLineMode((string) ($this->genericLabelForm['line_' . $index . '_mode'] ?? 'none'));
|
||||||
|
|
||||||
|
if ($mode === 'blank_line') {
|
||||||
|
$rows[] = '__BLANK_LINE__';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mode === 'text') {
|
||||||
|
$rows[] = trim((string) ($this->genericLabelForm['line_' . $index . '_text'] ?? ''));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mode === 'source') {
|
||||||
|
$rows[] = $this->resolveGenericLabelStableLineValue((string) ($this->genericLabelForm['line_' . $index . '_source'] ?? 'none'), $payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, array<string, string>> */
|
||||||
|
private function serializeGenericLabelLineRows(): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
for ($index = 1; $index <= 4; $index++) {
|
||||||
|
$rows[] = [
|
||||||
|
'mode' => $this->normalizeGenericLabelLineMode((string) ($this->genericLabelForm['line_' . $index . '_mode'] ?? 'none')),
|
||||||
|
'source' => (string) ($this->genericLabelForm['line_' . $index . '_source'] ?? 'none'),
|
||||||
|
'text' => trim((string) ($this->genericLabelForm['line_' . $index . '_text'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $preset
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function getLegacyGenericLabelLineRow(int $index, array $preset): array
|
||||||
|
{
|
||||||
|
if ($index === 1) {
|
||||||
|
return [
|
||||||
|
'mode' => 'source',
|
||||||
|
'source' => (string) ($preset['stable_line_one_source'] ?? 'stabile_summary'),
|
||||||
|
'text' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($index === 2) {
|
||||||
|
$source = (string) ($preset['stable_line_two_source'] ?? 'blank_line');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'mode' => $source === 'blank_line' ? 'blank_line' : 'source',
|
||||||
|
'source' => $source,
|
||||||
|
'text' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$blankCount = max(0, min(4, (int) ($preset['blank_lines_count'] ?? 0)));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'mode' => $index <= ($blankCount + 2) ? 'blank_line' : 'none',
|
||||||
|
'source' => 'none',
|
||||||
|
'text' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeGenericLabelLineMode(string $mode): string
|
||||||
|
{
|
||||||
|
return in_array($mode, ['source', 'text', 'blank_line', 'none'], true) ? $mode : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeGenericLabelFontSize(string $size): string
|
||||||
|
{
|
||||||
|
return in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'md';
|
||||||
|
}
|
||||||
|
|
||||||
private function getDefaultStabileId(): ?int
|
private function getDefaultStabileId(): ?int
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class PostIt extends Page
|
||||||
public ?int $assegnazioneUserId = null;
|
public ?int $assegnazioneUserId = null;
|
||||||
public ?int $assegnazioneFornitoreId = null;
|
public ?int $assegnazioneFornitoreId = null;
|
||||||
|
|
||||||
public ?int $selectedSuggerimentoId = null;
|
public ?int $selectedSuggerimentoId = null;
|
||||||
public bool $disableAutoStabileAssociation = false;
|
public bool $disableAutoStabileAssociation = false;
|
||||||
|
|
||||||
/** @var \Illuminate\Support\Collection<int, RubricaUniversale> */
|
/** @var \Illuminate\Support\Collection<int, RubricaUniversale> */
|
||||||
|
|
@ -407,26 +407,26 @@ public function getRecentiProperty()
|
||||||
->groupBy(fn(ChiamataPostIt $item): string => $this->buildRecenteGroupKey($item))
|
->groupBy(fn(ChiamataPostIt $item): string => $this->buildRecenteGroupKey($item))
|
||||||
->map(function (Collection $group): array {
|
->map(function (Collection $group): array {
|
||||||
/** @var ChiamataPostIt $primary */
|
/** @var ChiamataPostIt $primary */
|
||||||
$primary = $group->first();
|
$primary = $group->first();
|
||||||
$callerLabel = $this->resolveRecenteCallerLabel($group);
|
$callerLabel = $this->resolveRecenteCallerLabel($group);
|
||||||
$phoneLabel = $this->resolveRecentePhoneLabel($group);
|
$phoneLabel = $this->resolveRecentePhoneLabel($group);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'primary' => $primary,
|
'primary' => $primary,
|
||||||
'items' => $group->values(),
|
'items' => $group->values(),
|
||||||
'count' => $group->count(),
|
'count' => $group->count(),
|
||||||
'caller_label' => $callerLabel,
|
'caller_label' => $callerLabel,
|
||||||
'phone' => $phoneLabel,
|
'phone' => $phoneLabel,
|
||||||
'latest_at' => optional($primary->chiamata_il)->format('d/m/Y H:i'),
|
'latest_at' => optional($primary->chiamata_il)->format('d/m/Y H:i'),
|
||||||
'contains_selected' => $this->selectedPostItId
|
'contains_selected' => $this->selectedPostItId
|
||||||
? $group->contains(fn(ChiamataPostIt $item): bool => (int) $item->id === (int) $this->selectedPostItId)
|
? $group->contains(fn(ChiamataPostIt $item) : bool => (int) $item->id === (int) $this->selectedPostItId)
|
||||||
: false,
|
: false,
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
if (! $this->selectedPostItId && $grouped->isNotEmpty()) {
|
if (! $this->selectedPostItId && $grouped->isNotEmpty()) {
|
||||||
$first = $grouped->first();
|
$first = $grouped->first();
|
||||||
$this->selectedPostItId = (int) ($this->focusPostItId ?: ($first['primary']->id ?? 0));
|
$this->selectedPostItId = (int) ($this->focusPostItId ?: ($first['primary']->id ?? 0));
|
||||||
$this->loadSelectedPostItAssignment();
|
$this->loadSelectedPostItAssignment();
|
||||||
}
|
}
|
||||||
|
|
@ -550,13 +550,13 @@ private function createPostItRecord(): ChiamataPostIt
|
||||||
|
|
||||||
private function applyQueryPrefill(): void
|
private function applyQueryPrefill(): void
|
||||||
{
|
{
|
||||||
$source = trim((string) request()->query('source', ''));
|
$source = trim((string) request()->query('source', ''));
|
||||||
$this->disableAutoStabileAssociation = request()->boolean('lock_stabile') || $source === 'topbar_live_call';
|
$this->disableAutoStabileAssociation = request()->boolean('lock_stabile') || $source === 'topbar_live_call';
|
||||||
|
|
||||||
$telefono = trim((string) request()->query('telefono', ''));
|
$telefono = trim((string) request()->query('telefono', ''));
|
||||||
$nome = trim((string) request()->query('nome', ''));
|
$nome = trim((string) request()->query('nome', ''));
|
||||||
$nota = trim((string) request()->query('nota', ''));
|
$nota = trim((string) request()->query('nota', ''));
|
||||||
$oggetto = trim((string) request()->query('oggetto', ''));
|
$oggetto = trim((string) request()->query('oggetto', ''));
|
||||||
$direzione = trim((string) request()->query('direzione', ''));
|
$direzione = trim((string) request()->query('direzione', ''));
|
||||||
$rubricaId = (int) request()->query('rubrica_id', 0);
|
$rubricaId = (int) request()->query('rubrica_id', 0);
|
||||||
|
|
||||||
|
|
@ -564,7 +564,7 @@ private function applyQueryPrefill(): void
|
||||||
$this->telefono = $telefono;
|
$this->telefono = $telefono;
|
||||||
}
|
}
|
||||||
if ($nome !== '') {
|
if ($nome !== '') {
|
||||||
$this->nomeChiamante = $nome;
|
$this->nomeChiamante = $nome;
|
||||||
$this->ricercaChiamante = $nome;
|
$this->ricercaChiamante = $nome;
|
||||||
}
|
}
|
||||||
if ($nota !== '') {
|
if ($nota !== '') {
|
||||||
|
|
@ -637,25 +637,25 @@ private function createRubricaDraftFromPhone(string $phone, string $name = ''):
|
||||||
}
|
}
|
||||||
|
|
||||||
$normalizedPhone = PhoneNumber::toE164Italy($phone) ?? $digits;
|
$normalizedPhone = PhoneNumber::toE164Italy($phone) ?? $digits;
|
||||||
$adminId = $this->resolveCurrentAmministratoreId();
|
$adminId = $this->resolveCurrentAmministratoreId();
|
||||||
$cleanName = trim($name);
|
$cleanName = trim($name);
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'amministratore_id' => $adminId > 0 ? $adminId : null,
|
'amministratore_id' => $adminId > 0 ? $adminId : null,
|
||||||
'tipo_contatto' => str_contains($cleanName, ' ') ? 'persona_fisica' : 'persona_giuridica',
|
'tipo_contatto' => str_contains($cleanName, ' ') ? 'persona_fisica' : 'persona_giuridica',
|
||||||
'telefono_cellulare' => $normalizedPhone,
|
'telefono_cellulare' => $normalizedPhone,
|
||||||
'categoria' => 'altro',
|
'categoria' => 'altro',
|
||||||
'stato' => 'attivo',
|
'stato' => 'attivo',
|
||||||
'data_inserimento' => now()->toDateString(),
|
'data_inserimento' => now()->toDateString(),
|
||||||
'data_ultima_modifica' => now()->toDateString(),
|
'data_ultima_modifica' => now()->toDateString(),
|
||||||
'creato_da' => Auth::id(),
|
'creato_da' => Auth::id(),
|
||||||
'modificato_da' => Auth::id(),
|
'modificato_da' => Auth::id(),
|
||||||
'note_segreteria' => 'Bozza creata dal box Post-it in topbar per una chiamata in ingresso.',
|
'note_segreteria' => 'Bozza creata dal box Post-it in topbar per una chiamata in ingresso.',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($cleanName !== '' && str_contains($cleanName, ' ')) {
|
if ($cleanName !== '' && str_contains($cleanName, ' ')) {
|
||||||
$parts = preg_split('/\s+/', $cleanName) ?: [];
|
$parts = preg_split('/\s+/', $cleanName) ?: [];
|
||||||
$payload['nome'] = array_shift($parts) ?: null;
|
$payload['nome'] = array_shift($parts) ?: null;
|
||||||
$payload['cognome'] = $parts !== [] ? implode(' ', $parts) : null;
|
$payload['cognome'] = $parts !== [] ? implode(' ', $parts) : null;
|
||||||
} elseif ($cleanName !== '') {
|
} elseif ($cleanName !== '') {
|
||||||
$payload['ragione_sociale'] = $cleanName;
|
$payload['ragione_sociale'] = $cleanName;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||||
use App\Services\Cti\PbxRoutingService;
|
use App\Services\Cti\PbxRoutingService;
|
||||||
|
use App\Support\StabileContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
|
|
@ -18,7 +19,6 @@
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use App\Support\StabileContext;
|
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
class PostItGestione extends Page
|
class PostItGestione extends Page
|
||||||
|
|
@ -341,7 +341,7 @@ public function assegnaSelectedAppunto(): void
|
||||||
|
|
||||||
if ($this->appuntoAssegnazioneTipo === 'fornitore') {
|
if ($this->appuntoAssegnazioneTipo === 'fornitore') {
|
||||||
$ticketPayload['assegnato_a_fornitore_id'] = $this->appuntoAssegnazioneFornitoreId;
|
$ticketPayload['assegnato_a_fornitore_id'] = $this->appuntoAssegnazioneFornitoreId;
|
||||||
$ticketPayload['assegnato_a_user_id'] = null;
|
$ticketPayload['assegnato_a_user_id'] = null;
|
||||||
} else {
|
} else {
|
||||||
$ticketPayload['assegnato_a_user_id'] = $this->appuntoAssegnazioneUserId;
|
$ticketPayload['assegnato_a_user_id'] = $this->appuntoAssegnazioneUserId;
|
||||||
}
|
}
|
||||||
|
|
@ -535,9 +535,9 @@ public function getSelectedAppuntoProperty(): ?ChiamataPostIt
|
||||||
public function isPostItAssignmentReady(): bool
|
public function isPostItAssignmentReady(): bool
|
||||||
{
|
{
|
||||||
return $this->isPostItTableReady()
|
return $this->isPostItTableReady()
|
||||||
&& Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')
|
&& Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')
|
||||||
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')
|
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')
|
||||||
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id');
|
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOperatoriOptionsProperty(): array
|
public function getOperatoriOptionsProperty(): array
|
||||||
|
|
@ -1098,20 +1098,20 @@ private function loadSelectedAppuntoState(): void
|
||||||
{
|
{
|
||||||
$postIt = $this->selectedAppunto;
|
$postIt = $this->selectedAppunto;
|
||||||
if (! $postIt) {
|
if (! $postIt) {
|
||||||
$this->selectedAppuntoOggetto = null;
|
$this->selectedAppuntoOggetto = null;
|
||||||
$this->selectedAppuntoNota = null;
|
$this->selectedAppuntoNota = null;
|
||||||
$this->appuntoAssegnazioneTipo = 'operatore';
|
$this->appuntoAssegnazioneTipo = 'operatore';
|
||||||
$this->appuntoAssegnazioneUserId = null;
|
$this->appuntoAssegnazioneUserId = null;
|
||||||
$this->appuntoAssegnazioneFornitoreId = null;
|
$this->appuntoAssegnazioneFornitoreId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->selectedAppuntoOggetto = $postIt->oggetto;
|
$this->selectedAppuntoOggetto = $postIt->oggetto;
|
||||||
$this->selectedAppuntoNota = $postIt->nota;
|
$this->selectedAppuntoNota = $postIt->nota;
|
||||||
$this->appuntoAssegnazioneTipo = in_array((string) ($postIt->assegnazione_tipo ?? ''), ['operatore', 'collaboratore', 'fornitore'], true)
|
$this->appuntoAssegnazioneTipo = in_array((string) ($postIt->assegnazione_tipo ?? ''), ['operatore', 'collaboratore', 'fornitore'], true)
|
||||||
? (string) $postIt->assegnazione_tipo
|
? (string) $postIt->assegnazione_tipo
|
||||||
: 'operatore';
|
: 'operatore';
|
||||||
$this->appuntoAssegnazioneUserId = isset($postIt->assegnato_a_user_id) ? (int) $postIt->assegnato_a_user_id : null;
|
$this->appuntoAssegnazioneUserId = isset($postIt->assegnato_a_user_id) ? (int) $postIt->assegnato_a_user_id : null;
|
||||||
$this->appuntoAssegnazioneFornitoreId = isset($postIt->assegnato_a_fornitore_id) ? (int) $postIt->assegnato_a_fornitore_id : null;
|
$this->appuntoAssegnazioneFornitoreId = isset($postIt->assegnato_a_fornitore_id) ? (int) $postIt->assegnato_a_fornitore_id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1783,10 +1783,27 @@ private function startNodeRefreshJob(): void
|
||||||
'--progress-file=' . $progressPath,
|
'--progress-file=' . $progressPath,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$adminId = $this->resolveAmministratoreId();
|
||||||
|
$requireDrive = $this->shouldRequireDrivePreupdateBackup();
|
||||||
|
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
|
||||||
|
|
||||||
if ($this->gitForce) {
|
if ($this->gitForce) {
|
||||||
$args[] = '--force';
|
$args[] = '--force';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->updateSkipBackup) {
|
||||||
|
$args[] = '--skip-backup';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($driveEnabled && $adminId > 0) {
|
||||||
|
$args[] = '--drive';
|
||||||
|
$args[] = '--admin-id=' . $adminId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requireDrive) {
|
||||||
|
$args[] = '--require-drive';
|
||||||
|
}
|
||||||
|
|
||||||
$escaped = implode(' ', array_map(static fn(string $arg): string => escapeshellarg($arg), $args));
|
$escaped = implode(' ', array_map(static fn(string $arg): string => escapeshellarg($arg), $args));
|
||||||
$shell = 'nohup ' . $escaped . ' > ' . escapeshellarg($logPath) . ' 2>&1 & echo $!';
|
$shell = 'nohup ' . $escaped . ' > ' . escapeshellarg($logPath) . ' 2>&1 & echo $!';
|
||||||
$started = Process::path(base_path())->run(['bash', '-lc', $shell]);
|
$started = Process::path(base_path())->run(['bash', '-lc', $shell]);
|
||||||
|
|
|
||||||
|
|
@ -686,14 +686,14 @@ private function appendPendingWaterLibraryPhotos(): void
|
||||||
|
|
||||||
private function resetWaterDraftUploadState(): void
|
private function resetWaterDraftUploadState(): void
|
||||||
{
|
{
|
||||||
$this->newWaterPhotos = [];
|
$this->newWaterPhotos = [];
|
||||||
$this->pendingWaterPhotos = [];
|
$this->pendingWaterPhotos = [];
|
||||||
$this->pendingWaterLibraryPhotos = [];
|
$this->pendingWaterLibraryPhotos = [];
|
||||||
$this->waterPhotoDescriptions = [];
|
$this->waterPhotoDescriptions = [];
|
||||||
$this->waterUploadCodes = [];
|
$this->waterUploadCodes = [];
|
||||||
$this->waterClientMetadata = [];
|
$this->waterClientMetadata = [];
|
||||||
$this->waterDraftProtocol = 'TA-' . now()->format('Ymd-His');
|
$this->waterDraftProtocol = 'TA-' . now()->format('Ymd-His');
|
||||||
$this->waterDraftSequence = 1;
|
$this->waterDraftSequence = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncWaterDescriptionSlots(): void
|
private function syncWaterDescriptionSlots(): void
|
||||||
|
|
|
||||||
|
|
@ -131,14 +131,9 @@ public function etichetta(Request $request, Documento $documento, DocumentoArchi
|
||||||
$faldone = $this->extractFaldoneFromNote((string) ($documento->note ?? ''));
|
$faldone = $this->extractFaldoneFromNote((string) ($documento->note ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
$anno = $documento->data_documento?->format('Y') ?: now()->format('Y');
|
$anno = $documento->data_documento?->format('Y') ?: now()->format('Y');
|
||||||
$protocollo = (string) ($documento->numero_protocollo ?: ('DOC-' . $anno . '-' . str_pad((string) $documento->id, 3, '0', STR_PAD_LEFT)));
|
$protocollo = (string) ($documento->numero_protocollo ?: ('DOC-' . $anno . '-' . str_pad((string) $documento->id, 3, '0', STR_PAD_LEFT)));
|
||||||
|
$codiceUnico = $documento->resolveArchiveCode();
|
||||||
$stabileId = (int) ($documento->stabile_id ?: 0);
|
|
||||||
$codiceUnico = $stabileId > 0 ? ('STB-' . $stabileId . '-' . $protocollo) : $protocollo;
|
|
||||||
if ($faldone !== '') {
|
|
||||||
$codiceUnico .= '-F' . Str::upper($faldone);
|
|
||||||
}
|
|
||||||
|
|
||||||
$openUrl = route('filament.documenti.download', $documento);
|
$openUrl = route('filament.documenti.download', $documento);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ public function table(Table $table): Table
|
||||||
->label('Movimenti')
|
->label('Movimenti')
|
||||||
->icon('heroicon-o-receipt-percent')
|
->icon('heroicon-o-receipt-percent')
|
||||||
->url(function (DatiBancari $record): string {
|
->url(function (DatiBancari $record): string {
|
||||||
$base = CasseBancheMovimenti::getUrl(panel: 'admin-filament');
|
$base = CasseBancheMovimenti::getUrl(panel: 'admin-filament');
|
||||||
$contoId = (int) ($record->id ?? 0);
|
$contoId = (int) ($record->id ?? 0);
|
||||||
if ($contoId <= 0) {
|
if ($contoId <= 0) {
|
||||||
return $base;
|
return $base;
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,13 @@ public function createPostIt(): void
|
||||||
|
|
||||||
$this->redirect(
|
$this->redirect(
|
||||||
PostIt::getUrl([
|
PostIt::getUrl([
|
||||||
'source' => 'topbar_live_call',
|
'source' => 'topbar_live_call',
|
||||||
'telefono' => (string) ($this->liveIncomingCall['phone'] ?? ''),
|
'telefono' => (string) ($this->liveIncomingCall['phone'] ?? ''),
|
||||||
'nome' => (string) ($this->liveIncomingCall['rubrica_nome'] ?? ''),
|
'nome' => (string) ($this->liveIncomingCall['rubrica_nome'] ?? ''),
|
||||||
'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null,
|
'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null,
|
||||||
'direzione' => 'in_arrivo',
|
'direzione' => 'in_arrivo',
|
||||||
'oggetto' => 'Chiamata in arrivo da valutare',
|
'oggetto' => 'Chiamata in arrivo da valutare',
|
||||||
'nota' => (string) ($this->liveIncomingCall['line'] ?? ('Chiamata in ingresso da ' . ($this->liveIncomingCall['phone'] ?? ''))),
|
'nota' => (string) ($this->liveIncomingCall['line'] ?? ('Chiamata in ingresso da ' . ($this->liveIncomingCall['phone'] ?? ''))),
|
||||||
'lock_stabile' => 1,
|
'lock_stabile' => 1,
|
||||||
], panel: 'admin-filament'),
|
], panel: 'admin-filament'),
|
||||||
navigate: true,
|
navigate: true,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class Documento extends Model
|
class Documento extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -236,6 +237,53 @@ public function getStatoScadenzaAttribute()
|
||||||
return 'valido';
|
return 'valido';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metodi di utilità
|
||||||
|
*/
|
||||||
|
public function resolveArchiveCode(): string
|
||||||
|
{
|
||||||
|
$meta = is_array($this->metadati_archivio ?? null) ? $this->metadati_archivio : [];
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
$meta['codice_univoco_documento'] ?? null,
|
||||||
|
$meta['physical_archive_code'] ?? null,
|
||||||
|
$this->archive_reference ?? null,
|
||||||
|
] as $candidate) {
|
||||||
|
$value = trim((string) $candidate);
|
||||||
|
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$protocollo = trim((string) ($this->numero_protocollo ?? ''));
|
||||||
|
if ($protocollo === '') {
|
||||||
|
$protocollo = 'DOC-' . (int) $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stabileId = (int) ($this->stabile_id ?? 0);
|
||||||
|
$faldone = trim((string) data_get($meta, 'contenitore_numero', data_get($meta, 'faldone', '')));
|
||||||
|
$code = $stabileId > 0 ? ('STB-' . $stabileId . '-' . $protocollo) : $protocollo;
|
||||||
|
|
||||||
|
if ($faldone !== '') {
|
||||||
|
$code .= '-F' . Str::upper($faldone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolveArchiveDisplayReference(): string
|
||||||
|
{
|
||||||
|
$code = $this->resolveArchiveCode();
|
||||||
|
$reference = trim((string) ($this->archive_reference ?? ''));
|
||||||
|
|
||||||
|
if ($reference === '' || $reference === $code) {
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $code . ' · ' . $reference;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metodi di utilità
|
* Metodi di utilità
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Facades\URL;
|
use Illuminate\Support\Facades\URL;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class DocumentoArchivioFisicoItem extends Model
|
class DocumentoArchivioFisicoItem extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -49,11 +50,15 @@ class DocumentoArchivioFisicoItem extends Model
|
||||||
|
|
||||||
public const TIPI = [
|
public const TIPI = [
|
||||||
'documento' => 'Documento cartaceo',
|
'documento' => 'Documento cartaceo',
|
||||||
|
'foglio' => 'Foglio / documento singolo',
|
||||||
|
'cartellina' => 'Cartellina',
|
||||||
'busta_trasparente' => 'Busta trasparente con anelli da archiviare',
|
'busta_trasparente' => 'Busta trasparente con anelli da archiviare',
|
||||||
'cartella_tre_lembi' => 'Cartella a tre lembi',
|
'cartella_tre_lembi' => 'Cartella a tre lembi',
|
||||||
'faldone_anelli' => 'Faldone con anelli',
|
'faldone_anelli' => 'Faldone con anelli',
|
||||||
'faldone_lacci' => 'Faldone con lacci',
|
'faldone_lacci' => 'Faldone con lacci',
|
||||||
'scatola' => 'Scatola',
|
'scatola' => 'Scatola',
|
||||||
|
'scaffale' => 'Scaffale',
|
||||||
|
'posto' => 'Posto / ubicazione finale',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function stabile(): BelongsTo
|
public function stabile(): BelongsTo
|
||||||
|
|
@ -106,7 +111,7 @@ public function getPercorsoFisicoAttribute(): string
|
||||||
|
|
||||||
public function getQrCodeImageAttribute(): string
|
public function getQrCodeImageAttribute(): string
|
||||||
{
|
{
|
||||||
$data = $this->buildPublicQrUrl();
|
$data = $this->buildQrPayload();
|
||||||
|
|
||||||
if ($data !== '' && class_exists(\chillerlan\QRCode\QROptions::class) && class_exists(\chillerlan\QRCode\QRCode::class)) {
|
if ($data !== '' && class_exists(\chillerlan\QRCode\QROptions::class) && class_exists(\chillerlan\QRCode\QRCode::class)) {
|
||||||
$options = new \chillerlan\QRCode\QROptions([
|
$options = new \chillerlan\QRCode\QROptions([
|
||||||
|
|
@ -156,10 +161,34 @@ public static function generateCodice(int $stabileId): string
|
||||||
public function refreshQrCodeData(): void
|
public function refreshQrCodeData(): void
|
||||||
{
|
{
|
||||||
$this->forceFill([
|
$this->forceFill([
|
||||||
'qr_code_data' => $this->buildPublicQrUrl(),
|
'qr_code_data' => $this->buildQrPayload(),
|
||||||
])->saveQuietly();
|
])->saveQuietly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function buildQrPayload(): string
|
||||||
|
{
|
||||||
|
$parentCode = trim((string) ($this->parent?->codice_univoco ?? 'RADICE'));
|
||||||
|
$documentCode = $this->documento instanceof Documento
|
||||||
|
? trim($this->documento->resolveArchiveCode())
|
||||||
|
: '';
|
||||||
|
$location = collect([
|
||||||
|
trim((string) ($this->magazzino ?? '')),
|
||||||
|
trim((string) ($this->scaffale ?? '')),
|
||||||
|
trim((string) ($this->ripiano ?? '')),
|
||||||
|
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,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
public function buildPublicQrUrl(): string
|
public function buildPublicQrUrl(): string
|
||||||
{
|
{
|
||||||
$relativeSignedPath = URL::temporarySignedRoute(
|
$relativeSignedPath = URL::temporarySignedRoute(
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class Fornitore extends Model
|
||||||
'iban',
|
'iban',
|
||||||
'intestazione_cc_esatta',
|
'intestazione_cc_esatta',
|
||||||
'bic',
|
'bic',
|
||||||
|
'sia',
|
||||||
'modalita_pagamento_predefinita',
|
'modalita_pagamento_predefinita',
|
||||||
'escludi_righe_fe',
|
'escludi_righe_fe',
|
||||||
'fe_features',
|
'fe_features',
|
||||||
|
|
@ -97,6 +98,18 @@ public function getOperationalConfigSafeAttribute(): array
|
||||||
return is_array($this->operational_config ?? null) ? $this->operational_config : [];
|
return is_array($this->operational_config ?? null) ? $this->operational_config : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSiaEffettivoAttribute(): ?string
|
||||||
|
{
|
||||||
|
$sia = trim((string) ($this->sia ?? ''));
|
||||||
|
if ($sia !== '') {
|
||||||
|
return $sia;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallback = trim((string) data_get($this->operational_config_safe, 'payment_identifiers.sia', ''));
|
||||||
|
|
||||||
|
return $fallback !== '' ? $fallback : null;
|
||||||
|
}
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
static::creating(function (Fornitore $fornitore) {
|
static::creating(function (Fornitore $fornitore) {
|
||||||
|
|
|
||||||
15
app/Models/Impostazione.php
Normal file
15
app/Models/Impostazione.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Impostazione extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'impostazioni';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'chiave',
|
||||||
|
'valore',
|
||||||
|
'descrizione',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -14,6 +13,7 @@ class Scadenza extends Model
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'legacy_id',
|
'legacy_id',
|
||||||
'stabile_id',
|
'stabile_id',
|
||||||
|
'stabile_contratto_id',
|
||||||
'codice_stabile_legacy',
|
'codice_stabile_legacy',
|
||||||
'descrizione_sintetica',
|
'descrizione_sintetica',
|
||||||
'data_scadenza',
|
'data_scadenza',
|
||||||
|
|
@ -53,6 +53,11 @@ public function stabile()
|
||||||
return $this->belongsTo(Stabile::class, 'stabile_id');
|
return $this->belongsTo(Stabile::class, 'stabile_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function stabileContratto()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(StabileContratto::class, 'stabile_contratto_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function getTitoloCalendarioAttribute(): string
|
public function getTitoloCalendarioAttribute(): string
|
||||||
{
|
{
|
||||||
$parts = array_filter([
|
$parts = array_filter([
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class Stabile extends Model
|
class Stabile extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -156,6 +157,69 @@ class Stabile extends Model
|
||||||
'ultima_generazione_unita' => 'datetime',
|
'ultima_generazione_unita' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function hasOperationalHeating(): bool
|
||||||
|
{
|
||||||
|
if ((bool) ($this->riscaldamento_centralizzato ?? false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) ($this->millesimi_riscaldamento_separati ?? false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rateRiscaldamento = $this->rate_riscaldamento_mesi ?? [];
|
||||||
|
if (is_string($rateRiscaldamento)) {
|
||||||
|
$rateRiscaldamento = json_decode($rateRiscaldamento, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($rateRiscaldamento) && $rateRiscaldamento !== []) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) data_get($this->getOperationalConfiguration(), 'mostra_riscaldamento', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOperationalConfiguration(): array
|
||||||
|
{
|
||||||
|
return is_array($this->configurazione_avanzata ?? null) ? $this->configurazione_avanzata : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTakeoverDate(): ?Carbon
|
||||||
|
{
|
||||||
|
$raw = data_get($this->getOperationalConfiguration(), 'amministrazione.presa_in_carico_dal');
|
||||||
|
|
||||||
|
if (is_string($raw) && trim($raw) !== '') {
|
||||||
|
try {
|
||||||
|
return Carbon::parse($raw);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// ignore invalid stored values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->data_nomina instanceof Carbon ? $this->data_nomina : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOperationalYearVisible(?int $year): bool
|
||||||
|
{
|
||||||
|
if (! is_int($year) || $year <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$takeoverDate = $this->getTakeoverDate();
|
||||||
|
if (! $takeoverDate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $year >= (int) $takeoverDate->year;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNominaVerbaleDocumentoId(): ?int
|
||||||
|
{
|
||||||
|
$id = data_get($this->getOperationalConfiguration(), 'amministrazione.verbale_nomina_documento_id');
|
||||||
|
|
||||||
|
return is_numeric($id) && (int) $id > 0 ? (int) $id : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relazione con Amministratore
|
* Relazione con Amministratore
|
||||||
*/
|
*/
|
||||||
|
|
@ -241,6 +305,16 @@ public function gestioniContabili()
|
||||||
->orderBy('numero_straordinaria');
|
->orderBy('numero_straordinaria');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relazione con Contratti Hub
|
||||||
|
*/
|
||||||
|
public function contrattiHub()
|
||||||
|
{
|
||||||
|
return $this->hasMany(StabileContratto::class, 'stabile_id', 'id')
|
||||||
|
->orderByDesc('decorrenza_dal')
|
||||||
|
->orderByDesc('id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relazione con PianoRateizzazione
|
* Relazione con PianoRateizzazione
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
75
app/Models/StabileContratto.php
Normal file
75
app/Models/StabileContratto.php
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class StabileContratto extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'stabile_contratti';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'stabile_id',
|
||||||
|
'fornitore_id',
|
||||||
|
'documento_principale_id',
|
||||||
|
'titolo',
|
||||||
|
'tipo_contratto',
|
||||||
|
'categoria_impianto',
|
||||||
|
'servizio_label',
|
||||||
|
'codice_contratto',
|
||||||
|
'riferimento_esterno',
|
||||||
|
'stato',
|
||||||
|
'data_stipula',
|
||||||
|
'decorrenza_dal',
|
||||||
|
'decorrenza_al',
|
||||||
|
'frequenza_scadenze',
|
||||||
|
'giorno_scadenza',
|
||||||
|
'giorni_preavviso',
|
||||||
|
'rinnovo_automatico',
|
||||||
|
'importo_periodico',
|
||||||
|
'valuta',
|
||||||
|
'dati_contratto',
|
||||||
|
'codici_collegati',
|
||||||
|
'note',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'data_stipula' => 'date',
|
||||||
|
'decorrenza_dal' => 'date',
|
||||||
|
'decorrenza_al' => 'date',
|
||||||
|
'giorno_scadenza' => 'integer',
|
||||||
|
'giorni_preavviso' => 'integer',
|
||||||
|
'rinnovo_automatico' => 'boolean',
|
||||||
|
'importo_periodico' => 'decimal:2',
|
||||||
|
'dati_contratto' => 'array',
|
||||||
|
'codici_collegati' => 'array',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function stabile(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Stabile::class, 'stabile_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fornitore(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Fornitore::class, 'fornitore_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function documentoPrincipale(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Documento::class, 'documento_principale_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scadenze(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Scadenza::class, 'stabile_contratto_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Modules\Contabilita\Models;
|
namespace App\Modules\Contabilita\Models;
|
||||||
|
|
||||||
use App\Models\DatiBancari;
|
use App\Models\DatiBancari;
|
||||||
|
|
@ -18,6 +17,7 @@ class SaldoConto extends Model
|
||||||
'periodo_da',
|
'periodo_da',
|
||||||
'periodo_a',
|
'periodo_a',
|
||||||
'saldo',
|
'saldo',
|
||||||
|
'is_partenza_contabile',
|
||||||
'tipo_estratto',
|
'tipo_estratto',
|
||||||
'note',
|
'note',
|
||||||
'documento_stabile_id',
|
'documento_stabile_id',
|
||||||
|
|
@ -26,10 +26,11 @@ class SaldoConto extends Model
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'data_saldo' => 'date',
|
'data_saldo' => 'date',
|
||||||
'periodo_da' => 'date',
|
'periodo_da' => 'date',
|
||||||
'periodo_a' => 'date',
|
'periodo_a' => 'date',
|
||||||
'saldo' => 'decimal:2',
|
'saldo' => 'decimal:2',
|
||||||
|
'is_partenza_contabile' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function conto()
|
public function conto()
|
||||||
|
|
|
||||||
54
app/Modules/Contabilita/Services/ContiFinanziariService.php
Normal file
54
app/Modules/Contabilita/Services/ContiFinanziariService.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Modules\Contabilita\Services;
|
||||||
|
|
||||||
|
use App\Models\DatiBancari;
|
||||||
|
use App\Modules\Contabilita\Models\MovimentoBanca;
|
||||||
|
use App\Modules\Contabilita\Models\PianoConti;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class ContiFinanziariService
|
||||||
|
{
|
||||||
|
public function resolveContoFinanziarioDaMovimentoBanca(MovimentoBanca $movimento): PianoConti
|
||||||
|
{
|
||||||
|
$stabileId = (int) ($movimento->stabile_id ?? 0);
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
throw new RuntimeException('Movimento banca senza stabile_id.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conto = null;
|
||||||
|
$contoId = (int) ($movimento->conto_id ?? 0);
|
||||||
|
if ($contoId > 0) {
|
||||||
|
$conto = DatiBancari::query()->find($contoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $conto) {
|
||||||
|
$iban = is_string($movimento->iban ?? null) ? trim((string) $movimento->iban) : '';
|
||||||
|
if ($iban !== '') {
|
||||||
|
$conto = DatiBancari::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('iban', $iban)
|
||||||
|
->orderBy('id')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $conto) {
|
||||||
|
throw new RuntimeException('Il movimento banca non ha un conto finanziario risolvibile. Seleziona o collega il conto bancario corretto.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$codice = '1100B' . str_pad((string) $conto->id, 5, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$label = trim((string) ($conto->denominazione_banca ?? 'Banca'));
|
||||||
|
$iban = is_string($conto->iban ?? null) ? trim((string) $conto->iban) : '';
|
||||||
|
$numeroConto = is_string($conto->numero_conto ?? null) ? trim((string) $conto->numero_conto) : '';
|
||||||
|
|
||||||
|
$descrizione = $label !== '' ? $label : 'Conto banca';
|
||||||
|
if ($iban !== '') {
|
||||||
|
$descrizione .= ' · ' . $iban;
|
||||||
|
} elseif ($numeroConto !== '') {
|
||||||
|
$descrizione .= ' · n. ' . $numeroConto;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app(ChiusuraGestioneService::class)->resolveOrCreateConto($codice, $descrizione, 'Attività');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Modules\Contabilita\Services;
|
||||||
|
|
||||||
|
use App\Models\AffittoCanoneDovuto;
|
||||||
|
use App\Models\AffittoCanonePagato;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Modules\Contabilita\Models\MovimentoBanca;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class MovimentoBancaRiconciliazioneService
|
||||||
|
{
|
||||||
|
public function registraCanoneAffittoDaMovimento(User $user, MovimentoBanca $movimento, AffittoCanoneDovuto $canone): AffittoCanonePagato
|
||||||
|
{
|
||||||
|
$affitto = $canone->affitto;
|
||||||
|
if (! $affitto) {
|
||||||
|
throw new RuntimeException('Canone affitto senza contratto collegato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pagato = AffittoCanonePagato::query()
|
||||||
|
->where('affitto_id', (int) $canone->affitto_id)
|
||||||
|
->where('anno', (int) $canone->anno)
|
||||||
|
->where('mese', (int) $canone->mese)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $pagato) {
|
||||||
|
$descrizioni = array_filter([
|
||||||
|
$canone->descrizione_1,
|
||||||
|
$canone->descrizione_2,
|
||||||
|
$canone->descrizione_3,
|
||||||
|
$canone->descrizione_4,
|
||||||
|
], static fn($value): bool => is_string($value) && trim($value) !== '');
|
||||||
|
|
||||||
|
$pagato = new AffittoCanonePagato([
|
||||||
|
'affitto_id' => (int) $canone->affitto_id,
|
||||||
|
'anno' => (int) $canone->anno,
|
||||||
|
'mese' => (int) $canone->mese,
|
||||||
|
'mese_descrizione' => $canone->mese_descrizione,
|
||||||
|
'data_pagamento' => $movimento->data?->toDateString() ?: now()->toDateString(),
|
||||||
|
'descrizione' => trim(implode(' · ', $descrizioni)) ?: ('Incasso canone da banca del ' . ($movimento->data?->format('d/m/Y') ?? now()->format('d/m/Y'))),
|
||||||
|
'fitto' => $canone->fitto,
|
||||||
|
'istat_percentuale' => $canone->istat_percentuale,
|
||||||
|
'istat_importo' => $canone->istat_importo,
|
||||||
|
'descrizione_1' => $canone->descrizione_1,
|
||||||
|
'descrizione_2' => $canone->descrizione_2,
|
||||||
|
'importo_1' => $canone->importo_1,
|
||||||
|
'importo_2' => $canone->importo_2,
|
||||||
|
'bollo' => $canone->bollo,
|
||||||
|
'totale' => $canone->totale,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$pagato->data_pagamento = $pagato->data_pagamento ?: ($movimento->data?->toDateString() ?: now()->toDateString());
|
||||||
|
$pagato->descrizione = is_string($pagato->descrizione ?? null) && trim((string) $pagato->descrizione) !== ''
|
||||||
|
? $pagato->descrizione
|
||||||
|
: 'Incasso canone da banca';
|
||||||
|
}
|
||||||
|
|
||||||
|
$pagato->save();
|
||||||
|
|
||||||
|
$matchData = is_array($movimento->match_data ?? null) ? $movimento->match_data : [];
|
||||||
|
$matchData['riconciliato_operativo'] = true;
|
||||||
|
$matchData['riconciliazione_tipo'] = 'affitto_canone';
|
||||||
|
$matchData['riconciliazione_label'] = 'Canone affitto chiuso';
|
||||||
|
$matchData['riconciliazione_at'] = now()->toDateTimeString();
|
||||||
|
$matchData['riconciliato_da'] = (int) $user->id;
|
||||||
|
$matchData['affitto_canone_dovuto_id'] = (int) $canone->id;
|
||||||
|
$matchData['affitto_canone_pagato_id'] = (int) $pagato->id;
|
||||||
|
$matchData['affitto_id'] = (int) $canone->affitto_id;
|
||||||
|
|
||||||
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'rubrica_mittente_id') && (int) ($affitto->rubrica_inquilino_id ?? 0) > 0) {
|
||||||
|
$movimento->rubrica_mittente_id = (int) $affitto->rubrica_inquilino_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) {
|
||||||
|
$movimento->da_confermare = false;
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'match_data')) {
|
||||||
|
$movimento->match_data = $matchData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$movimento->save();
|
||||||
|
|
||||||
|
return $pagato;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Modules\Contabilita\Services;
|
namespace App\Modules\Contabilita\Services;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
|
use App\Models\RegistroRitenuteAcconto;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Modules\Contabilita\Models\FatturaFornitore;
|
||||||
use App\Modules\Contabilita\Models\Movimento;
|
use App\Modules\Contabilita\Models\Movimento;
|
||||||
use App\Modules\Contabilita\Models\MovimentoBanca;
|
use App\Modules\Contabilita\Models\MovimentoBanca;
|
||||||
use App\Modules\Contabilita\Models\PianoConti;
|
use App\Modules\Contabilita\Models\PianoConti;
|
||||||
use App\Modules\Contabilita\Models\Registrazione;
|
use App\Modules\Contabilita\Models\Registrazione;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
@ -15,7 +18,7 @@
|
||||||
class PagamentoFornitorePrimaNotaService
|
class PagamentoFornitorePrimaNotaService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param array{gestione_id?:int, conto_finanziario_id?:int, descrizione?:string, data_registrazione?:string} $override
|
* @param array{gestione_id?:int, conto_finanziario_id?:int, descrizione?:string, data_registrazione?:string, fattura_fornitore_id?:int} $override
|
||||||
*/
|
*/
|
||||||
public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento, int $fornitoreId, array $override = []): Registrazione
|
public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento, int $fornitoreId, array $override = []): Registrazione
|
||||||
{
|
{
|
||||||
|
|
@ -57,7 +60,9 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento
|
||||||
|
|
||||||
$contoFinanziarioId = (int) (($override['conto_finanziario_id'] ?? null) ?: 0);
|
$contoFinanziarioId = (int) (($override['conto_finanziario_id'] ?? null) ?: 0);
|
||||||
if ($contoFinanziarioId <= 0) {
|
if ($contoFinanziarioId <= 0) {
|
||||||
throw new RuntimeException('Seleziona un conto finanziario (banca/cassa) per registrare il pagamento.');
|
$contoFinanziarioId = (int) app(ContiFinanziariService::class)
|
||||||
|
->resolveContoFinanziarioDaMovimentoBanca($movimento)
|
||||||
|
->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$contoFinanziario = PianoConti::query()->find($contoFinanziarioId);
|
$contoFinanziario = PianoConti::query()->find($contoFinanziarioId);
|
||||||
|
|
@ -65,7 +70,7 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento
|
||||||
throw new RuntimeException('Conto finanziario non valido.');
|
throw new RuntimeException('Conto finanziario non valido.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$fornitore = Fornitore::query()->find($fornitoreId);
|
$fornitore = Fornitore::query()->find($fornitoreId);
|
||||||
$fornitoreLabel = $fornitore && is_string($fornitore->ragione_sociale ?? null)
|
$fornitoreLabel = $fornitore && is_string($fornitore->ragione_sociale ?? null)
|
||||||
? trim((string) $fornitore->ragione_sociale)
|
? trim((string) $fornitore->ragione_sociale)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -73,6 +78,21 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento
|
||||||
? trim((string) $fornitore->codice_univoco)
|
? trim((string) $fornitore->codice_univoco)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
$fattura = null;
|
||||||
|
$fatturaId = (int) (($override['fattura_fornitore_id'] ?? null) ?: 0);
|
||||||
|
if ($fatturaId > 0 && Schema::hasTable('contabilita_fatture_fornitori')) {
|
||||||
|
$fattura = FatturaFornitore::query()->find($fatturaId);
|
||||||
|
if (! $fattura) {
|
||||||
|
throw new RuntimeException('Fattura fornitore non trovata.');
|
||||||
|
}
|
||||||
|
if ((int) ($fattura->fornitore_id ?? 0) !== $fornitoreId) {
|
||||||
|
throw new RuntimeException('La fattura selezionata non appartiene al fornitore indicato.');
|
||||||
|
}
|
||||||
|
if ((int) ($fattura->gestione_id ?? 0) > 0 && $gestioneId <= 0) {
|
||||||
|
$gestioneId = (int) $fattura->gestione_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$contoDebitoFornitore = app(ContiSoggettiService::class)->resolveContoDebitoFornitore(
|
$contoDebitoFornitore = app(ContiSoggettiService::class)->resolveContoDebitoFornitore(
|
||||||
$stabileId,
|
$stabileId,
|
||||||
$fornitoreId,
|
$fornitoreId,
|
||||||
|
|
@ -90,20 +110,20 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento
|
||||||
? trim((string) $override['descrizione'])
|
? trim((string) $override['descrizione'])
|
||||||
: '';
|
: '';
|
||||||
if ($descr === '') {
|
if ($descr === '') {
|
||||||
$base = is_string($movimento->descrizione) ? trim((string) $movimento->descrizione) : '';
|
$base = is_string($movimento->descrizione) ? trim((string) $movimento->descrizione) : '';
|
||||||
$descr = $base !== '' ? $base : 'Pagamento fornitore';
|
$descr = $base !== '' ? $base : 'Pagamento fornitore';
|
||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($user, $movimento, $stabileId, $gestioneId, $data, $descr, $amount, $contoFinanziarioId, $contoDebitoFornitore) {
|
return DB::transaction(function () use ($user, $movimento, $stabileId, $gestioneId, $data, $descr, $amount, $contoFinanziarioId, $contoDebitoFornitore, $fornitoreId, $fattura) {
|
||||||
$regPayload = [
|
$regPayload = [
|
||||||
'stabile_id' => $stabileId,
|
'stabile_id' => $stabileId,
|
||||||
'gestione_id' => (Schema::hasColumn('contabilita_registrazioni', 'gestione_id')
|
'gestione_id' => (Schema::hasColumn('contabilita_registrazioni', 'gestione_id')
|
||||||
? Registrazione::resolveGestioneIdOrDefault($gestioneId, $stabileId, $data)
|
? Registrazione::resolveGestioneIdOrDefault($gestioneId, $stabileId, $data)
|
||||||
: null),
|
: null),
|
||||||
'data_registrazione' => $data,
|
'data_registrazione' => $data,
|
||||||
'descrizione' => $descr,
|
'descrizione' => $descr,
|
||||||
'created_by' => (int) $user->id,
|
'created_by' => (int) $user->id,
|
||||||
'updated_by' => (int) $user->id,
|
'updated_by' => (int) $user->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) {
|
if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) {
|
||||||
|
|
@ -116,26 +136,108 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento
|
||||||
// Dare: diminuisce il debito verso fornitore
|
// Dare: diminuisce il debito verso fornitore
|
||||||
Movimento::query()->create([
|
Movimento::query()->create([
|
||||||
'registrazione_id' => (int) $reg->id,
|
'registrazione_id' => (int) $reg->id,
|
||||||
'conto_id' => (int) $contoDebitoFornitore->id,
|
'conto_id' => (int) $contoDebitoFornitore->id,
|
||||||
'tipo' => 'dare',
|
'tipo' => 'dare',
|
||||||
'importo' => $amount,
|
'importo' => $amount,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Avere: diminuisce la banca/cassa
|
// Avere: diminuisce la banca/cassa
|
||||||
Movimento::query()->create([
|
Movimento::query()->create([
|
||||||
'registrazione_id' => (int) $reg->id,
|
'registrazione_id' => (int) $reg->id,
|
||||||
'conto_id' => $contoFinanziarioId,
|
'conto_id' => $contoFinanziarioId,
|
||||||
'tipo' => 'avere',
|
'tipo' => 'avere',
|
||||||
'importo' => $amount,
|
'importo' => $amount,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reg->assertBilanciata();
|
$reg->assertBilanciata();
|
||||||
|
|
||||||
$movimento->registrazione_id = (int) $reg->id;
|
$movimento->registrazione_id = (int) $reg->id;
|
||||||
$movimento->gestione_id = $gestioneId;
|
$movimento->gestione_id = $gestioneId;
|
||||||
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'fornitore_id')) {
|
||||||
|
$movimento->fornitore_id = $fornitoreId;
|
||||||
|
}
|
||||||
$movimento->save();
|
$movimento->save();
|
||||||
|
|
||||||
|
if ($fattura) {
|
||||||
|
$fattura->data_pagamento = $fattura->data_pagamento ?: $data;
|
||||||
|
$fattura->movimento_pagamento_id = (int) $movimento->id;
|
||||||
|
$fattura->save();
|
||||||
|
|
||||||
|
$this->syncRegistroRitenutaDaPagamento($fattura, $gestioneId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
return $reg;
|
return $reg;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function syncRegistroRitenutaDaPagamento(FatturaFornitore $fattura, int $gestioneId, string $dataPagamento): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('registro_ritenute_acconto')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ritenutaImporto = round((float) ($fattura->ritenuta_importo ?? 0), 4);
|
||||||
|
$ritenutaAliquota = round((float) ($fattura->ritenuta_aliquota ?? 0), 2);
|
||||||
|
if ($ritenutaImporto <= 0 || $ritenutaAliquota <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataPagamentoCarbon = Carbon::parse($dataPagamento)->startOfDay();
|
||||||
|
$dataCompetenza = $fattura->data_documento?->copy()
|
||||||
|
?: $fattura->data_registrazione?->copy()
|
||||||
|
?: $dataPagamentoCarbon->copy();
|
||||||
|
$scadenzaVersamento = $dataPagamentoCarbon->copy()->addMonthNoOverflow()->day(16);
|
||||||
|
|
||||||
|
$tenantId = '';
|
||||||
|
if (Schema::hasTable('gestioni_contabili')) {
|
||||||
|
$gestione = \App\Models\GestioneContabile::query()->find($gestioneId);
|
||||||
|
$tenantId = is_string($gestione?->tenant_id ?? null) ? trim((string) $gestione->tenant_id) : '';
|
||||||
|
}
|
||||||
|
if ($tenantId === '') {
|
||||||
|
$stabile = Stabile::query()
|
||||||
|
->with('amministratore:id,codice_amministratore')
|
||||||
|
->select(['id', 'amministratore_id'])
|
||||||
|
->find((int) $fattura->stabile_id);
|
||||||
|
$tenantId = is_string($stabile?->amministratore?->codice_amministratore ?? null)
|
||||||
|
? trim((string) $stabile->amministratore->codice_amministratore)
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$registro = RegistroRitenuteAcconto::query()
|
||||||
|
->where('gestione_id', $gestioneId)
|
||||||
|
->where(function ($query) use ($fattura, $dataCompetenza, $ritenutaImporto) {
|
||||||
|
$query->where('fattura_id', (int) $fattura->id)
|
||||||
|
->orWhere(function ($sub) use ($fattura, $dataCompetenza, $ritenutaImporto) {
|
||||||
|
$sub->where('fornitore_id', (int) $fattura->fornitore_id)
|
||||||
|
->whereDate('data_competenza', $dataCompetenza->toDateString())
|
||||||
|
->where('importo_ritenuta', $ritenutaImporto);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $registro) {
|
||||||
|
$registro = new RegistroRitenuteAcconto();
|
||||||
|
$registro->gestione_id = $gestioneId;
|
||||||
|
$registro->numero_progressivo = ((int) (RegistroRitenuteAcconto::query()
|
||||||
|
->where('gestione_id', $gestioneId)
|
||||||
|
->lockForUpdate()
|
||||||
|
->max('numero_progressivo') ?? 0)) + 1;
|
||||||
|
$registro->stato_versamento = 'da_versare';
|
||||||
|
$registro->fornitore_id = (int) $fattura->fornitore_id;
|
||||||
|
$registro->fattura_id = (int) $fattura->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$registro->tenant_id = $tenantId !== '' ? $tenantId : 'stabile-' . (int) $fattura->stabile_id;
|
||||||
|
$registro->data_competenza = $dataCompetenza->toDateString();
|
||||||
|
$registro->data_pagamento_fattura = $dataPagamentoCarbon->toDateString();
|
||||||
|
$registro->imponibile = round((float) ($fattura->imponibile ?? 0), 4);
|
||||||
|
$registro->aliquota_ritenuta = $ritenutaAliquota;
|
||||||
|
$registro->importo_ritenuta = $ritenutaImporto;
|
||||||
|
$registro->codice_tributo = trim((string) ($fattura->ritenuta_codice_tributo ?? '')) ?: '1040';
|
||||||
|
$registro->tipo_ritenuta = trim((string) ($fattura->ritenuta_previdenziale_codice ?? '')) ?: 'professionale';
|
||||||
|
$registro->data_scadenza_versamento = $scadenzaVersamento->toDateString();
|
||||||
|
$registro->anno_dichiarazione = (int) $dataCompetenza->format('Y');
|
||||||
|
$registro->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,41 +56,41 @@ public function panel(Panel $panel): Panel
|
||||||
->group('NetGescon')
|
->group('NetGescon')
|
||||||
->icon('heroicon-o-users')
|
->icon('heroicon-o-users')
|
||||||
->url(fn() => RubricaUniversaleArchivio::getUrl(panel: 'admin-filament'))
|
->url(fn() => RubricaUniversaleArchivio::getUrl(panel: 'admin-filament'))
|
||||||
->visible(function () {
|
->visible(function (): bool {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}),
|
}),
|
||||||
NavigationItem::make('Cellulare / WebApp')
|
NavigationItem::make('Cellulare / WebApp')
|
||||||
->group('NetGescon')
|
->group('NetGescon')
|
||||||
->icon('heroicon-o-phone')
|
->icon('heroicon-o-phone')
|
||||||
->url(fn() => route('admin.mobile'))
|
->url(fn() => route('admin.mobile'))
|
||||||
->visible(function () {
|
->visible(function (): bool {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}),
|
}),
|
||||||
NavigationItem::make('Ticket Mobile')
|
NavigationItem::make('Ticket Mobile')
|
||||||
->group('NetGescon')
|
->group('NetGescon')
|
||||||
->icon('heroicon-o-device-phone-mobile')
|
->icon('heroicon-o-device-phone-mobile')
|
||||||
->url(fn() => TicketMobile::getUrl(panel: 'admin-filament'))
|
->url(fn() => TicketMobile::getUrl(panel: 'admin-filament'))
|
||||||
->visible(function () {
|
->visible(function (): bool {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}),
|
}),
|
||||||
NavigationItem::make('Scheda Stabile')
|
NavigationItem::make('Scheda Stabile')
|
||||||
->group('NetGescon')
|
->group('NetGescon')
|
||||||
->icon('heroicon-o-building-office-2')
|
->icon('heroicon-o-building-office-2')
|
||||||
->url(fn() => StabilePage::getUrl(panel: 'admin-filament'))
|
->url(fn() => StabilePage::getUrl(panel: 'admin-filament'))
|
||||||
->visible(function () {
|
->visible(function (): bool {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}),
|
}),
|
||||||
NavigationItem::make('Catalogo Fornitore')
|
NavigationItem::make('Catalogo Fornitore')
|
||||||
->group('Fornitore')
|
->group('Fornitore')
|
||||||
|
|
@ -102,8 +102,8 @@ public function panel(Panel $panel): Panel
|
||||||
return $user instanceof User && $user->hasRole('fornitore');
|
return $user instanceof User && $user->hasRole('fornitore');
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->discoverResources(in: app_path('Filament/Resources'), for : 'App\\Filament\\Resources')
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||||
->discoverPages(in: app_path('Filament/Pages'), for : 'App\\Filament\\Pages')
|
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
||||||
->pages([
|
->pages([
|
||||||
Dashboard::class,
|
Dashboard::class,
|
||||||
SupportoDashboard::class,
|
SupportoDashboard::class,
|
||||||
|
|
@ -111,7 +111,7 @@ public function panel(Panel $panel): Panel
|
||||||
PrimaNotaModifica::class,
|
PrimaNotaModifica::class,
|
||||||
ContoMastrino::class,
|
ContoMastrino::class,
|
||||||
])
|
])
|
||||||
->discoverWidgets(in: app_path('Filament/Widgets'), for : 'App\\Filament\\Widgets')
|
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
||||||
->widgets([
|
->widgets([
|
||||||
AccountWidget::class,
|
AccountWidget::class,
|
||||||
TicketMobileOverview::class,
|
TicketMobileOverview::class,
|
||||||
|
|
@ -171,5 +171,4 @@ protected function resolveSupplierCatalogUrl(): string
|
||||||
|
|
||||||
return TicketOperativi::getUrl(panel: 'admin-filament');
|
return TicketOperativi::getUrl(panel: 'admin-filament');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ class AmazonCreatorsApiService
|
||||||
public function isConfigured(): bool
|
public function isConfigured(): bool
|
||||||
{
|
{
|
||||||
return (bool) config('services.amazon.creators_enabled', false)
|
return (bool) config('services.amazon.creators_enabled', false)
|
||||||
&& $this->credentialId() !== ''
|
&& $this->credentialId() !== ''
|
||||||
&& $this->credentialSecret() !== ''
|
&& $this->credentialSecret() !== ''
|
||||||
&& $this->tokenUrl() !== ''
|
&& $this->tokenUrl() !== ''
|
||||||
&& $this->apiBaseUrl() !== '';
|
&& $this->apiBaseUrl() !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMarketplace(): string
|
public function getMarketplace(): string
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
use App\Models\ProductIdentifier;
|
use App\Models\ProductIdentifier;
|
||||||
use App\Models\ProductMedia;
|
use App\Models\ProductMedia;
|
||||||
use App\Models\ProductOffer;
|
use App\Models\ProductOffer;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
|
|
@ -162,9 +162,9 @@ public function importItemForSupplier(Fornitore $fornitore, array $item, array $
|
||||||
throw new RuntimeException('Il risultato Amazon selezionato non contiene un ASIN valido.');
|
throw new RuntimeException('Il risultato Amazon selezionato non contiene un ASIN valido.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$ean = $this->extractEan($item);
|
$ean = $this->extractEan($item);
|
||||||
$title = $this->extractTitle($item) ?: 'Prodotto Amazon';
|
$title = $this->extractTitle($item) ?: 'Prodotto Amazon';
|
||||||
$brand = $this->extractBrand($item);
|
$brand = $this->extractBrand($item);
|
||||||
$product = $this->findExistingProduct($asin, $ean);
|
$product = $this->findExistingProduct($asin, $ean);
|
||||||
$created = false;
|
$created = false;
|
||||||
|
|
||||||
|
|
@ -180,8 +180,8 @@ public function importItemForSupplier(Fornitore $fornitore, array $item, array $
|
||||||
'track_serials' => false,
|
'track_serials' => false,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'meta' => [
|
'meta' => [
|
||||||
'source' => 'amazon_creators_api',
|
'source' => 'amazon_creators_api',
|
||||||
'catalog' => [
|
'catalog' => [
|
||||||
'publication_mode' => 'internal_only',
|
'publication_mode' => 'internal_only',
|
||||||
],
|
],
|
||||||
'verification' => [
|
'verification' => [
|
||||||
|
|
@ -189,7 +189,7 @@ public function importItemForSupplier(Fornitore $fornitore, array $item, array $
|
||||||
'channel' => 'amazon_catalog_search_ui',
|
'channel' => 'amazon_catalog_search_ui',
|
||||||
'updated_at' => now()->toIso8601String(),
|
'updated_at' => now()->toIso8601String(),
|
||||||
],
|
],
|
||||||
'amazon' => [
|
'amazon' => [
|
||||||
'asin' => $asin,
|
'asin' => $asin,
|
||||||
'ean' => $ean,
|
'ean' => $ean,
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
|
|
@ -221,10 +221,10 @@ public function importItemForSupplier(Fornitore $fornitore, array $item, array $
|
||||||
}
|
}
|
||||||
|
|
||||||
$sync = $this->syncProduct($product, [
|
$sync = $this->syncProduct($product, [
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
'source' => (string) ($options['source'] ?? 'amazon_catalog_search_ui'),
|
'source' => (string) ($options['source'] ?? 'amazon_catalog_search_ui'),
|
||||||
'marketplace' => (string) ($options['marketplace'] ?? $this->apiService->getMarketplace()),
|
'marketplace' => (string) ($options['marketplace'] ?? $this->apiService->getMarketplace()),
|
||||||
'associate_tag'=> (string) ($options['associate_tag'] ?? ($this->apiService->getAssociateTag() ?? '')),
|
'associate_tag' => (string) ($options['associate_tag'] ?? ($this->apiService->getAssociateTag() ?? '')),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
use App\Models\ProductOffer;
|
use App\Models\ProductOffer;
|
||||||
use App\Models\TicketIntervento;
|
use App\Models\TicketIntervento;
|
||||||
use App\Modules\Contabilita\Models\FatturaFornitoreRiga;
|
use App\Modules\Contabilita\Models\FatturaFornitoreRiga;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class ProductHubViewService
|
class ProductHubViewService
|
||||||
|
|
@ -49,8 +48,8 @@ public function buildRows(Fornitore $fornitore, ?int $stabileId = null, int $lim
|
||||||
}
|
}
|
||||||
|
|
||||||
$productIds = $products->pluck('id')->map(fn($value): int => (int) $value)->all();
|
$productIds = $products->pluck('id')->map(fn($value): int => (int) $value)->all();
|
||||||
$usageByProduct = $this->buildUsageSummary($fornitore, $productIds, $stabileId);
|
$usageByProduct = $this->buildUsageSummary($fornitore, $productIds, $stabileId);
|
||||||
$pricingByProduct = $this->buildPurchaseSummary($fornitore, $productIds);
|
$pricingByProduct = $this->buildPurchaseSummary($fornitore, $productIds);
|
||||||
|
|
||||||
return $products
|
return $products
|
||||||
->map(function (Product $product) use ($fornitore, $usageByProduct, $pricingByProduct): array {
|
->map(function (Product $product) use ($fornitore, $usageByProduct, $pricingByProduct): array {
|
||||||
|
|
@ -69,42 +68,42 @@ public function buildRows(Fornitore $fornitore, ?int $stabileId = null, int $lim
|
||||||
$purchaseSummary = $pricingByProduct[(int) $product->id] ?? $this->emptyPurchaseSummary();
|
$purchaseSummary = $pricingByProduct[(int) $product->id] ?? $this->emptyPurchaseSummary();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $product->id,
|
'id' => (int) $product->id,
|
||||||
'internal_code' => (string) ($product->internal_code ?? ''),
|
'internal_code' => (string) ($product->internal_code ?? ''),
|
||||||
'name' => (string) ($product->name ?? ''),
|
'name' => (string) ($product->name ?? ''),
|
||||||
'brand' => (string) ($product->brand ?? ''),
|
'brand' => (string) ($product->brand ?? ''),
|
||||||
'model' => (string) ($product->model ?? ''),
|
'model' => (string) ($product->model ?? ''),
|
||||||
'color_label' => (string) ($product->color_label ?? ''),
|
'color_label' => (string) ($product->color_label ?? ''),
|
||||||
'track_serials' => (bool) $product->track_serials,
|
'track_serials' => (bool) $product->track_serials,
|
||||||
'identifier' => (string) ($identifier->code_value ?? ''),
|
'identifier' => (string) ($identifier->code_value ?? ''),
|
||||||
'identifiers_count' => (int) ($product->identifiers_count ?? 0),
|
'identifiers_count' => (int) ($product->identifiers_count ?? 0),
|
||||||
'serials_count' => (int) ($product->serials_count ?? 0),
|
'serials_count' => (int) ($product->serials_count ?? 0),
|
||||||
'media_count' => (int) ($product->media_count ?? 0),
|
'media_count' => (int) ($product->media_count ?? 0),
|
||||||
'source' => (string) (($meta['source'] ?? 'manual') ?: 'manual'),
|
'source' => (string) (($meta['source'] ?? 'manual') ?: 'manual'),
|
||||||
'publication_mode' => (string) (($catalogMeta['publication_mode'] ?? '') ?: ''),
|
'publication_mode' => (string) (($catalogMeta['publication_mode'] ?? '') ?: ''),
|
||||||
'public_reference' => (string) (($catalogMeta['public_reference'] ?? '') ?: ($product->internal_code ?? '')),
|
'public_reference' => (string) (($catalogMeta['public_reference'] ?? '') ?: ($product->internal_code ?? '')),
|
||||||
'private_links' => count(array_filter(is_array($catalogMeta['private_source_links'] ?? null) ? $catalogMeta['private_source_links'] : [], fn($value) => filled($value))),
|
'private_links' => count(array_filter(is_array($catalogMeta['private_source_links'] ?? null) ? $catalogMeta['private_source_links'] : [], fn($value) => filled($value))),
|
||||||
'categories' => (string) (($catalogMeta['categories'] ?? '') ?: ''),
|
'categories' => (string) (($catalogMeta['categories'] ?? '') ?: ''),
|
||||||
'stock_quantity' => is_numeric($meta['stock']['quantity'] ?? null) ? (int) $meta['stock']['quantity'] : null,
|
'stock_quantity' => is_numeric($meta['stock']['quantity'] ?? null) ? (int) $meta['stock']['quantity'] : null,
|
||||||
'price_wholesale' => is_numeric($meta['pricing']['wholesale'] ?? null) ? (float) $meta['pricing']['wholesale'] : null,
|
'price_wholesale' => is_numeric($meta['pricing']['wholesale'] ?? null) ? (float) $meta['pricing']['wholesale'] : null,
|
||||||
'offers_count' => $offers->count(),
|
'offers_count' => $offers->count(),
|
||||||
'own_offer_price' => $internalOffer && filled($internalOffer->price_amount) ? (float) $internalOffer->price_amount : null,
|
'own_offer_price' => $internalOffer && filled($internalOffer->price_amount) ? (float) $internalOffer->price_amount : null,
|
||||||
'best_competitor_price' => $bestExternal && filled($bestExternal->price_amount) ? (float) $bestExternal->price_amount : null,
|
'best_competitor_price' => $bestExternal && filled($bestExternal->price_amount) ? (float) $bestExternal->price_amount : null,
|
||||||
'best_competitor_source' => $bestExternal ? (string) ($bestExternal->source_name ?? $bestExternal->source_type ?? '') : '',
|
'best_competitor_source' => $bestExternal ? (string) ($bestExternal->source_name ?? $bestExternal->source_type ?? '') : '',
|
||||||
'amazon_referral_url' => $amazonOffer ? (string) ($amazonOffer->referral_url ?? $amazonOffer->external_url ?? '') : '',
|
'amazon_referral_url' => $amazonOffer ? (string) ($amazonOffer->referral_url ?? $amazonOffer->external_url ?? '') : '',
|
||||||
'usage_count' => (int) ($usage['usage_count'] ?? 0),
|
'usage_count' => (int) ($usage['usage_count'] ?? 0),
|
||||||
'usage_quantity' => (float) ($usage['usage_quantity'] ?? 0),
|
'usage_quantity' => (float) ($usage['usage_quantity'] ?? 0),
|
||||||
'last_used_at' => (string) ($usage['last_used_at'] ?? ''),
|
'last_used_at' => (string) ($usage['last_used_at'] ?? ''),
|
||||||
'stable_usage_count' => (int) ($usage['stable_usage_count'] ?? 0),
|
'stable_usage_count' => (int) ($usage['stable_usage_count'] ?? 0),
|
||||||
'stable_usage_quantity' => (float) ($usage['stable_usage_quantity'] ?? 0),
|
'stable_usage_quantity' => (float) ($usage['stable_usage_quantity'] ?? 0),
|
||||||
'stable_last_used_at' => (string) ($usage['stable_last_used_at'] ?? ''),
|
'stable_last_used_at' => (string) ($usage['stable_last_used_at'] ?? ''),
|
||||||
'purchase_lines_count' => (int) ($purchaseSummary['purchase_lines_count'] ?? 0),
|
'purchase_lines_count' => (int) ($purchaseSummary['purchase_lines_count'] ?? 0),
|
||||||
'last_purchase_total' => $purchaseSummary['last_purchase_total'] ?? null,
|
'last_purchase_total' => $purchaseSummary['last_purchase_total'] ?? null,
|
||||||
'last_purchase_date' => (string) ($purchaseSummary['last_purchase_date'] ?? ''),
|
'last_purchase_date' => (string) ($purchaseSummary['last_purchase_date'] ?? ''),
|
||||||
'purchase_history_labels' => (array) ($purchaseSummary['purchase_history_labels'] ?? []),
|
'purchase_history_labels' => (array) ($purchaseSummary['purchase_history_labels'] ?? []),
|
||||||
'sort_score' => ((int) ($usage['stable_usage_count'] ?? 0) * 1000)
|
'sort_score' => ((int) ($usage['stable_usage_count'] ?? 0) * 1000)
|
||||||
+ ((int) round((float) ($usage['stable_usage_quantity'] ?? 0) * 100))
|
+ ((int) round((float) ($usage['stable_usage_quantity'] ?? 0) * 100))
|
||||||
+ ((int) ($usage['usage_count'] ?? 0) * 10),
|
+ ((int) ($usage['usage_count'] ?? 0) * 10),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->sort(function (array $left, array $right): int {
|
->sort(function (array $left, array $right): int {
|
||||||
|
|
@ -160,14 +159,14 @@ private function buildUsageSummary(Fornitore $fornitore, array $productIds, ?int
|
||||||
|
|
||||||
$summary[$productId]['usage_count']++;
|
$summary[$productId]['usage_count']++;
|
||||||
$summary[$productId]['usage_quantity'] += $qty;
|
$summary[$productId]['usage_quantity'] += $qty;
|
||||||
$summary[$productId]['last_used_at'] = $summary[$productId]['last_used_at'] !== ''
|
$summary[$productId]['last_used_at'] = $summary[$productId]['last_used_at'] !== ''
|
||||||
? $summary[$productId]['last_used_at']
|
? $summary[$productId]['last_used_at']
|
||||||
: (optional($intervento->updated_at)->format('d/m/Y H:i') ?: '');
|
: (optional($intervento->updated_at)->format('d/m/Y H:i') ?: '');
|
||||||
|
|
||||||
if ($stabileId !== null && $stabileId > 0 && $currentStabileId === $stabileId) {
|
if ($stabileId !== null && $stabileId > 0 && $currentStabileId === $stabileId) {
|
||||||
$summary[$productId]['stable_usage_count']++;
|
$summary[$productId]['stable_usage_count']++;
|
||||||
$summary[$productId]['stable_usage_quantity'] += $qty;
|
$summary[$productId]['stable_usage_quantity'] += $qty;
|
||||||
$summary[$productId]['stable_last_used_at'] = $summary[$productId]['stable_last_used_at'] !== ''
|
$summary[$productId]['stable_last_used_at'] = $summary[$productId]['stable_last_used_at'] !== ''
|
||||||
? $summary[$productId]['stable_last_used_at']
|
? $summary[$productId]['stable_last_used_at']
|
||||||
: (optional($intervento->updated_at)->format('d/m/Y H:i') ?: '');
|
: (optional($intervento->updated_at)->format('d/m/Y H:i') ?: '');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1035,7 +1035,7 @@ private function parseDescrizioneEstesa(string $text): array
|
||||||
$providerUpper = strtoupper(trim((string) ($res['payment_provider'] ?? $res['beneficiario'] ?? '')));
|
$providerUpper = strtoupper(trim((string) ($res['payment_provider'] ?? $res['beneficiario'] ?? '')));
|
||||||
if ($providerUpper !== '') {
|
if ($providerUpper !== '') {
|
||||||
if (str_contains($providerUpper, 'ACEA ATO2') || str_contains($providerUpper, 'ACEA ACQUA')) {
|
if (str_contains($providerUpper, 'ACEA ATO2') || str_contains($providerUpper, 'ACEA ACQUA')) {
|
||||||
$res['utility_type'] = 'acqua';
|
$res['utility_type'] = 'acqua';
|
||||||
$res['suggested_voce_spesa_code'] = 'ACQ';
|
$res['suggested_voce_spesa_code'] = 'ACQ';
|
||||||
} elseif (str_contains($providerUpper, 'A2A')) {
|
} elseif (str_contains($providerUpper, 'A2A')) {
|
||||||
$res['utility_type'] = 'utenza';
|
$res['utility_type'] = 'utenza';
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ private function buildTableSummary(Collection $voci, array $tabellaMap): array
|
||||||
return $voci
|
return $voci
|
||||||
->groupBy(fn(array $row): int => (int) ($row['tabella_millesimale_default_id'] ?? 0))
|
->groupBy(fn(array $row): int => (int) ($row['tabella_millesimale_default_id'] ?? 0))
|
||||||
->map(function (Collection $rows, int $tabellaId) use ($tabellaMap): array {
|
->map(function (Collection $rows, int $tabellaId) use ($tabellaMap): array {
|
||||||
$tabella = $tabellaMap[$tabellaId] ?? [
|
$tabella = $tabellaMap[$tabellaId] ?? [
|
||||||
'codice' => '',
|
'codice' => '',
|
||||||
'label' => '',
|
'label' => '',
|
||||||
'tipo_calcolo' => null,
|
'tipo_calcolo' => null,
|
||||||
|
|
@ -274,7 +274,7 @@ private function buildTableSummary(Collection $voci, array $tabellaMap): array
|
||||||
'voci' => $rows->values()->all(),
|
'voci' => $rows->values()->all(),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->sortBy(fn(array $row): string => sprintf('%06d|%s', (int) ($row['nord'] ?? 999999), (string) ($row['codice'] ?? '')))
|
->sortBy(fn(array $row): string => sprintf('%06d|%s', (int) ($row['nord'] ?? 999999), (string) ($row['codice'] ?? '')))
|
||||||
->mapWithKeys(fn(array $row): array=> [$row['codice'] => $row])
|
->mapWithKeys(fn(array $row): array=> [$row['codice'] => $row])
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Documenti;
|
namespace App\Services\Documenti;
|
||||||
|
|
||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
use App\Models\DocumentoArchivioCartella;
|
|
||||||
use App\Models\DocumentoStabile;
|
use App\Models\DocumentoStabile;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Support\StabileContext;
|
use App\Support\StabileContext;
|
||||||
|
|
@ -99,11 +97,11 @@ private function applyVisibilityConstraint(Builder $query, User $user, string $t
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$roles = $user->getRoleNames()->map(fn($role): string => (string) $role)->values()->all();
|
$roles = $user->getRoleNames()->map(fn($role): string => (string) $role)->values()->all();
|
||||||
$groupLabels = $this->resolveAudienceGroupsForUser($user);
|
$groupLabels = $this->resolveAudienceGroupsForUser($user);
|
||||||
$userId = (int) $user->id;
|
$userId = (int) $user->id;
|
||||||
$hasRolesColumn = $this->hasVisibilityColumn($table, 'visibility_roles');
|
$hasRolesColumn = $this->hasVisibilityColumn($table, 'visibility_roles');
|
||||||
$hasGroupsColumn = $this->hasVisibilityColumn($table, 'visibility_groups');
|
$hasGroupsColumn = $this->hasVisibilityColumn($table, 'visibility_groups');
|
||||||
$hasAllowedUsersColumn = $this->hasVisibilityColumn($table, 'allowed_user_ids');
|
$hasAllowedUsersColumn = $this->hasVisibilityColumn($table, 'allowed_user_ids');
|
||||||
|
|
||||||
$query->where(function (Builder $visibilityQuery) use ($table, $roles, $groupLabels, $userId, $hasRolesColumn, $hasGroupsColumn, $hasAllowedUsersColumn): void {
|
$query->where(function (Builder $visibilityQuery) use ($table, $roles, $groupLabels, $userId, $hasRolesColumn, $hasGroupsColumn, $hasAllowedUsersColumn): void {
|
||||||
|
|
|
||||||
|
|
@ -548,13 +548,13 @@ private function updateContabilitaFornituraDaPdf(FatturaElettronica $fattura): v
|
||||||
}
|
}
|
||||||
|
|
||||||
$fullPath = $disk->path($resolvedPath);
|
$fullPath = $disk->path($resolvedPath);
|
||||||
$text = $this->extractPdfText($fullPath);
|
$text = $this->extractPdfText($fullPath);
|
||||||
if (! is_string($text) || trim($text) === '') {
|
if (! is_string($text) || trim($text) === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$gasData = $this->extractGasFornituraDataFromPdfText($text);
|
$gasData = $this->extractGasFornituraDataFromPdfText($text);
|
||||||
$waterData = $this->extractAcquaFornituraDataFromPdfText($text);
|
$waterData = $this->extractAcquaFornituraDataFromPdfText($text);
|
||||||
$paymentData = $this->extractPaymentReferenceDataFromPdfText($text, $fattura);
|
$paymentData = $this->extractPaymentReferenceDataFromPdfText($text, $fattura);
|
||||||
|
|
||||||
$data = is_array($waterData) && $waterData !== [] ? $waterData : $gasData;
|
$data = is_array($waterData) && $waterData !== [] ? $waterData : $gasData;
|
||||||
|
|
@ -686,11 +686,11 @@ private function extractAcquaFornituraDataFromPdfText(string $text): ?array
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [];
|
$codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [];
|
||||||
$contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [];
|
$contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [];
|
||||||
$consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : [];
|
$consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : [];
|
||||||
$generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [];
|
$generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [];
|
||||||
$quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [];
|
$quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [];
|
||||||
|
|
||||||
$hasIdentifiers = count(array_filter([
|
$hasIdentifiers = count(array_filter([
|
||||||
$codici['utenza'] ?? null,
|
$codici['utenza'] ?? null,
|
||||||
|
|
@ -705,19 +705,19 @@ private function extractAcquaFornituraDataFromPdfText(string $text): ?array
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'type' => 'acqua',
|
'type' => 'acqua',
|
||||||
'voce_spesa_code' => 'ACQ',
|
'voce_spesa_code' => 'ACQ',
|
||||||
'codici' => [
|
'codici' => [
|
||||||
'utenza' => $codici['utenza'] ?? null,
|
'utenza' => $codici['utenza'] ?? null,
|
||||||
'cliente' => $codici['cliente'] ?? null,
|
'cliente' => $codici['cliente'] ?? null,
|
||||||
'contratto' => $codici['contratto'] ?? null,
|
'contratto' => $codici['contratto'] ?? null,
|
||||||
],
|
],
|
||||||
'contatore' => [
|
'contatore' => [
|
||||||
'matricola' => $contatore['matricola'] ?? null,
|
'matricola' => $contatore['matricola'] ?? null,
|
||||||
],
|
],
|
||||||
'generale' => $generale,
|
'generale' => $generale,
|
||||||
'quadro_dettaglio' => $quadro,
|
'quadro_dettaglio' => $quadro,
|
||||||
'consumi' => array_values($consumi),
|
'consumi' => array_values($consumi),
|
||||||
];
|
];
|
||||||
|
|
||||||
return array_filter($payload, fn($value) => $value !== null && $value !== '');
|
return array_filter($payload, fn($value) => $value !== null && $value !== '');
|
||||||
|
|
@ -727,8 +727,8 @@ private function extractAcquaFornituraDataFromPdfText(string $text): ?array
|
||||||
private function extractPaymentReferenceDataFromPdfText(string $text, FatturaElettronica $fattura): array
|
private function extractPaymentReferenceDataFromPdfText(string $text, FatturaElettronica $fattura): array
|
||||||
{
|
{
|
||||||
$provider = trim((string) ($fattura->fornitore_denominazione ?? ''));
|
$provider = trim((string) ($fattura->fornitore_denominazione ?? ''));
|
||||||
$cbill = null;
|
$cbill = null;
|
||||||
$sia = null;
|
$sia = null;
|
||||||
|
|
||||||
if (preg_match('/\bCBILL\b(?:\s*N\.)?\s*\.?\s*([0-9]{9,})/i', $text, $m)) {
|
if (preg_match('/\bCBILL\b(?:\s*N\.)?\s*\.?\s*([0-9]{9,})/i', $text, $m)) {
|
||||||
$cbill = trim((string) ($m[1] ?? ''));
|
$cbill = trim((string) ($m[1] ?? ''));
|
||||||
|
|
@ -744,8 +744,8 @@ private function extractPaymentReferenceDataFromPdfText(string $text, FatturaEle
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'provider' => $provider !== '' ? $provider : null,
|
'provider' => $provider !== '' ? $provider : null,
|
||||||
'cbill' => $cbill,
|
'cbill' => $cbill,
|
||||||
'sia' => $sia,
|
'sia' => $sia,
|
||||||
];
|
];
|
||||||
|
|
||||||
return array_filter($payload, fn($value) => $value !== null && $value !== '');
|
return array_filter($payload, fn($value) => $value !== null && $value !== '');
|
||||||
|
|
@ -776,18 +776,18 @@ private function enrichWaterInvoiceFromParsedData(FatturaElettronica $fattura, a
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sum = 0.0;
|
$sum = 0.0;
|
||||||
$hasNumeric = false;
|
$hasNumeric = false;
|
||||||
foreach ($consumi as $consumo) {
|
foreach ($consumi as $consumo) {
|
||||||
if (! is_array($consumo) || ! is_numeric($consumo['valore'] ?? null)) {
|
if (! is_array($consumo) || ! is_numeric($consumo['valore'] ?? null)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$sum += (float) $consumo['valore'];
|
$sum += (float) $consumo['valore'];
|
||||||
$hasNumeric = true;
|
$hasNumeric = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$reference = null;
|
$reference = null;
|
||||||
$first = is_array($consumi[0] ?? null) ? $consumi[0] : [];
|
$first = is_array($consumi[0] ?? null) ? $consumi[0] : [];
|
||||||
if (is_string($first['dal'] ?? null) && is_string($first['al'] ?? null) && $first['dal'] !== '' && $first['al'] !== '') {
|
if (is_string($first['dal'] ?? null) && is_string($first['al'] ?? null) && $first['dal'] !== '' && $first['al'] !== '') {
|
||||||
$reference = $first['dal'] . ' - ' . $first['al'];
|
$reference = $first['dal'] . ' - ' . $first['al'];
|
||||||
}
|
}
|
||||||
|
|
@ -795,19 +795,19 @@ private function enrichWaterInvoiceFromParsedData(FatturaElettronica $fattura, a
|
||||||
$dirty = false;
|
$dirty = false;
|
||||||
if ($hasNumeric && (! is_numeric($fattura->consumo_valore) || (float) $fattura->consumo_valore <= 0)) {
|
if ($hasNumeric && (! is_numeric($fattura->consumo_valore) || (float) $fattura->consumo_valore <= 0)) {
|
||||||
$fattura->consumo_valore = $sum;
|
$fattura->consumo_valore = $sum;
|
||||||
$dirty = true;
|
$dirty = true;
|
||||||
}
|
}
|
||||||
if ((! is_string($fattura->consumo_unita ?? null) || trim((string) $fattura->consumo_unita) === '') && $hasNumeric) {
|
if ((! is_string($fattura->consumo_unita ?? null) || trim((string) $fattura->consumo_unita) === '') && $hasNumeric) {
|
||||||
$fattura->consumo_unita = 'mc';
|
$fattura->consumo_unita = 'mc';
|
||||||
$dirty = true;
|
$dirty = true;
|
||||||
}
|
}
|
||||||
if ($reference !== null && (! is_string($fattura->consumo_riferimento ?? null) || trim((string) $fattura->consumo_riferimento) === '')) {
|
if ($reference !== null && (! is_string($fattura->consumo_riferimento ?? null) || trim((string) $fattura->consumo_riferimento) === '')) {
|
||||||
$fattura->consumo_riferimento = $reference;
|
$fattura->consumo_riferimento = $reference;
|
||||||
$dirty = true;
|
$dirty = true;
|
||||||
}
|
}
|
||||||
if (! is_string($fattura->consumo_raw ?? null) || trim((string) $fattura->consumo_raw) === '') {
|
if (! is_string($fattura->consumo_raw ?? null) || trim((string) $fattura->consumo_raw) === '') {
|
||||||
$fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
$dirty = true;
|
$dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($dirty) {
|
if ($dirty) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\FattureElettroniche;
|
namespace App\Services\FattureElettroniche;
|
||||||
|
|
||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
|
|
@ -32,17 +31,17 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
||||||
->findOrFail((int) $fattura->stabile_id);
|
->findOrFail((int) $fattura->stabile_id);
|
||||||
|
|
||||||
$dataDoc = $fattura->data_fattura;
|
$dataDoc = $fattura->data_fattura;
|
||||||
$anno = $dataDoc ? (int) $dataDoc->format('Y') : (int) now()->format('Y');
|
$anno = $dataDoc ? (int) $dataDoc->format('Y') : (int) now()->format('Y');
|
||||||
$ym = $dataDoc ? $dataDoc->format('Y/m') : now()->format('Y/m');
|
$ym = $dataDoc ? $dataDoc->format('Y/m') : now()->format('Y/m');
|
||||||
|
|
||||||
$adminCode = $stabile->amministratore?->codice_amministratore;
|
$adminCode = $stabile->amministratore?->codice_amministratore;
|
||||||
$stabileCode = $stabile->codice_stabile;
|
$stabileCode = $stabile->codice_stabile;
|
||||||
$adminFolder = $adminCode ?: ('ID-' . (int) $stabile->amministratore_id);
|
$adminFolder = $adminCode ?: ('ID-' . (int) $stabile->amministratore_id);
|
||||||
$stabileFolder = $stabileCode ?: ('ID-' . (int) $fattura->stabile_id);
|
$stabileFolder = $stabileCode ?: ('ID-' . (int) $fattura->stabile_id);
|
||||||
|
|
||||||
$stabileBase = ArchivioPaths::stabileBase($stabile, $stabile->amministratore);
|
$stabileBase = ArchivioPaths::stabileBase($stabile, $stabile->amministratore);
|
||||||
$gestione = $this->resolveGestioneForYear($stabile, $anno);
|
$gestione = $this->resolveGestioneForYear($stabile, $anno);
|
||||||
$pdfBase = ArchivioPaths::fatturePdfGestionePath($stabile, $gestione, $anno, 'ordinaria', $stabile->amministratore)
|
$pdfBase = ArchivioPaths::fatturePdfGestionePath($stabile, $gestione, $anno, 'ordinaria', $stabile->amministratore)
|
||||||
?: $this->basePath($adminFolder, $stabileFolder, $anno);
|
?: $this->basePath($adminFolder, $stabileFolder, $anno);
|
||||||
$xmlBase = ArchivioPaths::fattureXmlYearPath($stabile, $anno, $stabile->amministratore)
|
$xmlBase = ArchivioPaths::fattureXmlYearPath($stabile, $anno, $stabile->amministratore)
|
||||||
?: $this->basePath($adminFolder, $stabileFolder, $anno);
|
?: $this->basePath($adminFolder, $stabileFolder, $anno);
|
||||||
|
|
@ -80,7 +79,7 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
||||||
$found = $this->findFirstFileStartingWith($dir, $fattura->allegato_pdf_hash . '-');
|
$found = $this->findFirstFileStartingWith($dir, $fattura->allegato_pdf_hash . '-');
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$fattura->allegato_pdf_path = $found;
|
$fattura->allegato_pdf_path = $found;
|
||||||
$pdfPath = $found;
|
$pdfPath = $found;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,11 +95,11 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
||||||
: ($pdfPathNow !== '' ? basename($pdfPathNow) : '');
|
: ($pdfPathNow !== '' ? basename($pdfPathNow) : '');
|
||||||
|
|
||||||
$isProtocollato = ($pdfNameNow !== '' && preg_match('/^FT-\d{4}\b/i', $pdfNameNow) === 1);
|
$isProtocollato = ($pdfNameNow !== '' && preg_match('/^FT-\d{4}\b/i', $pdfNameNow) === 1);
|
||||||
$supplierToken = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? ''));
|
$supplierToken = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? ''));
|
||||||
$supplierToken = trim((string) $supplierToken);
|
$supplierToken = trim((string) $supplierToken);
|
||||||
|
|
||||||
if ($isProtocollato && $supplierToken !== '' && mb_strlen($supplierToken) >= 4) {
|
if ($isProtocollato && $supplierToken !== '' && mb_strlen($supplierToken) >= 4) {
|
||||||
$nameUpper = mb_strtoupper($pdfNameNow);
|
$nameUpper = mb_strtoupper($pdfNameNow);
|
||||||
$tokenUpper = mb_strtoupper($supplierToken);
|
$tokenUpper = mb_strtoupper($supplierToken);
|
||||||
if (! str_contains($nameUpper, $tokenUpper)) {
|
if (! str_contains($nameUpper, $tokenUpper)) {
|
||||||
$fattura->allegato_pdf_path = null;
|
$fattura->allegato_pdf_path = null;
|
||||||
|
|
@ -135,7 +134,7 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
||||||
}
|
}
|
||||||
if (! $protocollo) {
|
if (! $protocollo) {
|
||||||
if ($protocolCol) {
|
if ($protocolCol) {
|
||||||
$next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno);
|
$next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno);
|
||||||
$protocollo = 'FT-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT);
|
$protocollo = 'FT-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT);
|
||||||
} else {
|
} else {
|
||||||
// Se la tabella documenti non ha una colonna protocollo, assicuriamo comunque un nome unico per i file.
|
// Se la tabella documenti non ha una colonna protocollo, assicuriamo comunque un nome unico per i file.
|
||||||
|
|
@ -158,7 +157,7 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if ($protocolCol) {
|
if ($protocolCol) {
|
||||||
$next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno);
|
$next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno);
|
||||||
$protocollo = 'FT-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT);
|
$protocollo = 'FT-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT);
|
||||||
} else {
|
} else {
|
||||||
$protocollo = 'FT-' . str_pad((string) ((int) $fattura->id), 6, '0', STR_PAD_LEFT);
|
$protocollo = 'FT-' . str_pad((string) ((int) $fattura->id), 6, '0', STR_PAD_LEFT);
|
||||||
|
|
@ -168,13 +167,13 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
||||||
}
|
}
|
||||||
|
|
||||||
$sanFornitore = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? 'FORNITORE'));
|
$sanFornitore = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? 'FORNITORE'));
|
||||||
$sanNumero = $this->sanitizeFilename((string) ($fattura->numero_fattura ?? ('ID-' . $fattura->id)));
|
$sanNumero = $this->sanitizeFilename((string) ($fattura->numero_fattura ?? ('ID-' . $fattura->id)));
|
||||||
$dateForName = $dataDoc ? $dataDoc->format('Ymd') : now()->format('Ymd');
|
$dateForName = $dataDoc ? $dataDoc->format('Ymd') : now()->format('Ymd');
|
||||||
|
|
||||||
// Se manca il PDF (ma abbiamo almeno XML/metadata), generiamo un prospetto PDF archiviabile.
|
// Se manca il PDF (ma abbiamo almeno XML/metadata), generiamo un prospetto PDF archiviabile.
|
||||||
$pdfPathNow = is_string($fattura->allegato_pdf_path) ? $fattura->allegato_pdf_path : '';
|
$pdfPathNow = is_string($fattura->allegato_pdf_path) ? $fattura->allegato_pdf_path : '';
|
||||||
$hasPdfNow = is_string($pdfPathNow) && $pdfPathNow !== '' && Storage::disk('local')->exists($pdfPathNow);
|
$hasPdfNow = is_string($pdfPathNow) && $pdfPathNow !== '' && Storage::disk('local')->exists($pdfPathNow);
|
||||||
$hasXmlNow = (is_string($fattura->xml_content) && trim($fattura->xml_content) !== '')
|
$hasXmlNow = (is_string($fattura->xml_content) && trim($fattura->xml_content) !== '')
|
||||||
|| (is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path));
|
|| (is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path));
|
||||||
if (! $hasPdfNow && $hasXmlNow) {
|
if (! $hasPdfNow && $hasXmlNow) {
|
||||||
$targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.pdf';
|
$targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.pdf';
|
||||||
|
|
@ -219,19 +218,19 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
||||||
$fattura->save();
|
$fattura->save();
|
||||||
|
|
||||||
$docData = [
|
$docData = [
|
||||||
'utente_id' => $userId,
|
'utente_id' => $userId,
|
||||||
'nome' => $protocollo . ' - ' . $sanFornitore,
|
'nome' => $protocollo . ' - ' . $sanFornitore,
|
||||||
'tipologia' => 'fattura',
|
'tipologia' => 'fattura',
|
||||||
'tipo_documento' => 'fattura',
|
'tipo_documento' => 'fattura',
|
||||||
'fornitore' => $fattura->fornitore_denominazione,
|
'fornitore' => $fattura->fornitore_denominazione,
|
||||||
'data_documento' => $fattura->data_fattura,
|
'data_documento' => $fattura->data_fattura,
|
||||||
'data_scadenza' => $fattura->data_scadenza,
|
'data_scadenza' => $fattura->data_scadenza,
|
||||||
'importo_collegato' => $fattura->totale,
|
'importo_collegato' => $fattura->totale,
|
||||||
'numero_protocollo' => $protocollo,
|
'numero_protocollo' => $protocollo,
|
||||||
'protocollo' => $protocollo,
|
'protocollo' => $protocollo,
|
||||||
'nome_file' => $fattura->allegato_pdf_nome ?: ($fattura->nome_file_xml ?: null),
|
'nome_file' => $fattura->allegato_pdf_nome ?: ($fattura->nome_file_xml ?: null),
|
||||||
'path_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null),
|
'path_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null),
|
||||||
'percorso_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null),
|
'percorso_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Compatibilità: alcune installazioni non hanno (più) certi campi su documenti.
|
// Compatibilità: alcune installazioni non hanno (più) certi campi su documenti.
|
||||||
|
|
@ -257,12 +256,12 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
||||||
|
|
||||||
$createData = array_merge($docData, [
|
$createData = array_merge($docData, [
|
||||||
'documentable_type' => FatturaElettronica::class,
|
'documentable_type' => FatturaElettronica::class,
|
||||||
'documentable_id' => $fattura->id,
|
'documentable_id' => $fattura->id,
|
||||||
'stabile_id' => (int) $fattura->stabile_id,
|
'stabile_id' => (int) $fattura->stabile_id,
|
||||||
'data_upload' => now(),
|
'data_upload' => now(),
|
||||||
'approvato' => false,
|
'approvato' => false,
|
||||||
'archiviato' => false,
|
'archiviato' => false,
|
||||||
'is_demo' => false,
|
'is_demo' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach (array_keys($createData) as $col) {
|
foreach (array_keys($createData) as $col) {
|
||||||
|
|
@ -310,7 +309,7 @@ private function resolveGestioneForYear(Stabile $stabile, int $year): ?GestioneC
|
||||||
|
|
||||||
private function getDocumentoProtocolColumn(): ?string
|
private function getDocumentoProtocolColumn(): ?string
|
||||||
{
|
{
|
||||||
static $cached = null;
|
static $cached = null;
|
||||||
static $resolved = false;
|
static $resolved = false;
|
||||||
|
|
||||||
if ($resolved) {
|
if ($resolved) {
|
||||||
|
|
@ -339,22 +338,33 @@ private function getDocumentoProtocolColumn(): ?string
|
||||||
|
|
||||||
private function generateFallbackPdfForFattura(FatturaElettronica $fattura, string $protocollo): string
|
private function generateFallbackPdfForFattura(FatturaElettronica $fattura, string $protocollo): string
|
||||||
{
|
{
|
||||||
$lines = [];
|
$lines = [];
|
||||||
$lines[] = 'NETGESCON - Prospetto fattura (generato automaticamente)';
|
$lines[] = 'NETGESCON - Prospetto fattura (generato automaticamente)';
|
||||||
$lines[] = 'Protocollo: ' . $protocollo;
|
$lines[] = 'Protocollo: ' . $protocollo;
|
||||||
$lines[] = '';
|
$lines[] = '';
|
||||||
|
|
||||||
$fornitore = trim((string) ($fattura->fornitore_denominazione ?? ''));
|
$fornitore = trim((string) ($fattura->fornitore_denominazione ?? ''));
|
||||||
$num = trim((string) ($fattura->numero_fattura ?? ''));
|
$num = trim((string) ($fattura->numero_fattura ?? ''));
|
||||||
$data = $fattura->data_fattura ? $fattura->data_fattura->format('d/m/Y') : '';
|
$data = $fattura->data_fattura ? $fattura->data_fattura->format('d/m/Y') : '';
|
||||||
$tot = is_numeric($fattura->totale ?? null) ? number_format((float) $fattura->totale, 2, ',', '.') : '';
|
$tot = is_numeric($fattura->totale ?? null) ? number_format((float) $fattura->totale, 2, ',', '.') : '';
|
||||||
$piva = trim((string) ($fattura->fornitore_piva ?? ''));
|
$piva = trim((string) ($fattura->fornitore_piva ?? ''));
|
||||||
$cf = trim((string) ($fattura->fornitore_cf ?? ''));
|
$cf = trim((string) ($fattura->fornitore_cf ?? ''));
|
||||||
|
|
||||||
if ($fornitore !== '') $lines[] = 'Fornitore: ' . $fornitore;
|
if ($fornitore !== '') {
|
||||||
if ($piva !== '' || $cf !== '') $lines[] = 'P.IVA/CF: ' . ($piva !== '' ? $piva : '—') . ' / ' . ($cf !== '' ? $cf : '—');
|
$lines[] = 'Fornitore: ' . $fornitore;
|
||||||
if ($num !== '' || $data !== '') $lines[] = 'Fattura: ' . ($num !== '' ? $num : '—') . ' - ' . ($data !== '' ? $data : '—');
|
}
|
||||||
if ($tot !== '') $lines[] = 'Totale: EUR ' . $tot;
|
|
||||||
|
if ($piva !== '' || $cf !== '') {
|
||||||
|
$lines[] = 'P.IVA/CF: ' . ($piva !== '' ? $piva : '—') . ' / ' . ($cf !== '' ? $cf : '—');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($num !== '' || $data !== '') {
|
||||||
|
$lines[] = 'Fattura: ' . ($num !== '' ? $num : '—') . ' - ' . ($data !== '' ? $data : '—');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tot !== '') {
|
||||||
|
$lines[] = 'Totale: EUR ' . $tot;
|
||||||
|
}
|
||||||
|
|
||||||
$lines[] = '';
|
$lines[] = '';
|
||||||
$lines[] = 'Nota: il PDF originale non era disponibile; questo documento serve solo per archiviazione e protocollazione.';
|
$lines[] = 'Nota: il PDF originale non era disponibile; questo documento serve solo per archiviazione e protocollazione.';
|
||||||
|
|
@ -451,7 +461,7 @@ private function transformFatturaPaXmlToHtmlWithAssosoftwareXsl(string $xml): ?s
|
||||||
|
|
||||||
private function injectProtocolloIntoHtml(string $html, string $protocollo): string
|
private function injectProtocolloIntoHtml(string $html, string $protocollo): string
|
||||||
{
|
{
|
||||||
$safe = htmlspecialchars($protocollo, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
$safe = htmlspecialchars($protocollo, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
$badge = '<div style="font-size:12px;margin:10px 0 0 0;color:#333;"><strong>Protocollo:</strong> ' . $safe . '</div>';
|
$badge = '<div style="font-size:12px;margin:10px 0 0 0;color:#333;"><strong>Protocollo:</strong> ' . $safe . '</div>';
|
||||||
|
|
||||||
// Inserisce subito dopo l'apertura del body, se possibile.
|
// Inserisce subito dopo l'apertura del body, se possibile.
|
||||||
|
|
@ -527,23 +537,23 @@ private function buildSimplePdf(array $lines): string
|
||||||
}
|
}
|
||||||
$content .= "\nET";
|
$content .= "\nET";
|
||||||
|
|
||||||
$objects = [];
|
$objects = [];
|
||||||
$objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
|
$objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
|
||||||
$objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
|
$objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
|
||||||
$objects[] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n";
|
$objects[] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n";
|
||||||
$objects[] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n";
|
$objects[] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n";
|
||||||
$objects[] = "5 0 obj\n<< /Length " . strlen($content) . " >>\nstream\n" . $content . "\nendstream\nendobj\n";
|
$objects[] = "5 0 obj\n<< /Length " . strlen($content) . " >>\nstream\n" . $content . "\nendstream\nendobj\n";
|
||||||
|
|
||||||
$pdf = "%PDF-1.4\n";
|
$pdf = "%PDF-1.4\n";
|
||||||
$offsets = [0];
|
$offsets = [0];
|
||||||
foreach ($objects as $obj) {
|
foreach ($objects as $obj) {
|
||||||
$offsets[] = strlen($pdf);
|
$offsets[] = strlen($pdf);
|
||||||
$pdf .= $obj;
|
$pdf .= $obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
$xrefPos = strlen($pdf);
|
$xrefPos = strlen($pdf);
|
||||||
$pdf .= "xref\n0 " . (count($objects) + 1) . "\n";
|
$pdf .= "xref\n0 " . (count($objects) + 1) . "\n";
|
||||||
$pdf .= "0000000000 65535 f \n";
|
$pdf .= "0000000000 65535 f \n";
|
||||||
for ($i = 1; $i <= count($objects); $i++) {
|
for ($i = 1; $i <= count($objects); $i++) {
|
||||||
$pdf .= str_pad((string) $offsets[$i], 10, '0', STR_PAD_LEFT) . " 00000 n \n";
|
$pdf .= str_pad((string) $offsets[$i], 10, '0', STR_PAD_LEFT) . " 00000 n \n";
|
||||||
}
|
}
|
||||||
|
|
@ -600,8 +610,8 @@ private function syncDocumentoAndPaths(FatturaElettronica $fattura, Documento $d
|
||||||
|
|
||||||
if ($fatturaPath && (! is_string($docPath) || $docPath === '' || ! Storage::disk('local')->exists($docPath))) {
|
if ($fatturaPath && (! is_string($docPath) || $docPath === '' || ! Storage::disk('local')->exists($docPath))) {
|
||||||
$doc->forceFill([
|
$doc->forceFill([
|
||||||
'nome_file' => $fatturaName,
|
'nome_file' => $fatturaName,
|
||||||
'path_file' => $fatturaPath,
|
'path_file' => $fatturaPath,
|
||||||
'percorso_file' => $fatturaPath,
|
'percorso_file' => $fatturaPath,
|
||||||
])->save();
|
])->save();
|
||||||
}
|
}
|
||||||
|
|
@ -624,10 +634,10 @@ private function recoverMissingPathsFromBase(FatturaElettronica $fattura, string
|
||||||
|
|
||||||
$files = Storage::disk('local')->files($base);
|
$files = Storage::disk('local')->files($base);
|
||||||
|
|
||||||
$needleNumero = trim((string) $this->sanitizeFilename((string) ($fattura->numero_fattura ?? '')));
|
$needleNumero = trim((string) $this->sanitizeFilename((string) ($fattura->numero_fattura ?? '')));
|
||||||
$needleFornitore = trim((string) $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? '')));
|
$needleFornitore = trim((string) $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? '')));
|
||||||
$needlePiva = trim((string) ($fattura->fornitore_piva ?? ''));
|
$needlePiva = trim((string) ($fattura->fornitore_piva ?? ''));
|
||||||
$needleCf = trim((string) ($fattura->fornitore_cf ?? ''));
|
$needleCf = trim((string) ($fattura->fornitore_cf ?? ''));
|
||||||
|
|
||||||
// Evita agganci sbagliati: se il numero è vuoto o troppo corto (es. "1", "2"), non proviamo recovery.
|
// Evita agganci sbagliati: se il numero è vuoto o troppo corto (es. "1", "2"), non proviamo recovery.
|
||||||
// In quei casi è più sicuro rigenerare da XML.
|
// In quei casi è più sicuro rigenerare da XML.
|
||||||
|
|
@ -646,14 +656,14 @@ private function recoverMissingPathsFromBase(FatturaElettronica $fattura, string
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
$name = basename($file);
|
$name = basename($file);
|
||||||
|
|
||||||
$nameUpper = mb_strtoupper($name);
|
$nameUpper = mb_strtoupper($name);
|
||||||
$numUpper = mb_strtoupper($needleNumero);
|
$numUpper = mb_strtoupper($needleNumero);
|
||||||
$fornUpper = mb_strtoupper($needleFornitore);
|
$fornUpper = mb_strtoupper($needleFornitore);
|
||||||
$matchNumero = str_contains($nameUpper, $numUpper);
|
$matchNumero = str_contains($nameUpper, $numUpper);
|
||||||
$matchFornitore = str_contains($nameUpper, $fornUpper);
|
$matchFornitore = str_contains($nameUpper, $fornUpper);
|
||||||
$matchPiva = ($needlePiva !== '' && str_contains($nameUpper, $needlePiva));
|
$matchPiva = ($needlePiva !== '' && str_contains($nameUpper, $needlePiva));
|
||||||
$matchCf = ($needleCf !== '' && str_contains($nameUpper, $needleCf));
|
$matchCf = ($needleCf !== '' && str_contains($nameUpper, $needleCf));
|
||||||
$strongMatch = $matchNumero && ($matchFornitore || $matchPiva || $matchCf);
|
$strongMatch = $matchNumero && ($matchFornitore || $matchPiva || $matchCf);
|
||||||
|
|
||||||
if (! $pdfOk && str_ends_with(strtolower($name), '.pdf')) {
|
if (! $pdfOk && str_ends_with(strtolower($name), '.pdf')) {
|
||||||
if ($strongMatch) {
|
if ($strongMatch) {
|
||||||
|
|
@ -671,15 +681,15 @@ private function recoverMissingPathsFromBase(FatturaElettronica $fattura, string
|
||||||
if (! $pdfOk && count($pdfCandidates) === 1) {
|
if (! $pdfOk && count($pdfCandidates) === 1) {
|
||||||
$fattura->allegato_pdf_path = $pdfCandidates[0]['path'];
|
$fattura->allegato_pdf_path = $pdfCandidates[0]['path'];
|
||||||
$fattura->allegato_pdf_nome = $pdfCandidates[0]['name'];
|
$fattura->allegato_pdf_nome = $pdfCandidates[0]['name'];
|
||||||
$pdfOk = true;
|
$pdfOk = true;
|
||||||
$needsSave = true;
|
$needsSave = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $xmlOk && count($xmlCandidates) === 1) {
|
if (! $xmlOk && count($xmlCandidates) === 1) {
|
||||||
$fattura->xml_path = $xmlCandidates[0]['path'];
|
$fattura->xml_path = $xmlCandidates[0]['path'];
|
||||||
$fattura->nome_file_xml = $xmlCandidates[0]['name'];
|
$fattura->nome_file_xml = $xmlCandidates[0]['name'];
|
||||||
$xmlOk = true;
|
$xmlOk = true;
|
||||||
$needsSave = true;
|
$needsSave = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($needsSave) {
|
if ($needsSave) {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
|
|
||||||
use App\Models\Amministratore;
|
use App\Models\Amministratore;
|
||||||
use App\Models\DatiBancari;
|
use App\Models\DatiBancari;
|
||||||
use App\Models\GestioneContabile;
|
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
|
use App\Models\GestioneContabile;
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ public static function gestioneFolderName(?GestioneContabile $gestione, ?int $fa
|
||||||
return $prefix . $year;
|
return $prefix . $year;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fattureXmlYearPath(Stabile $stabile, int|string $year, ?Amministratore $admin = null): ?string
|
public static function fattureXmlYearPath(Stabile $stabile, int | string $year, ?Amministratore $admin = null): ?string
|
||||||
{
|
{
|
||||||
$base = self::stabileBase($stabile, $admin);
|
$base = self::stabileBase($stabile, $admin);
|
||||||
if (! is_string($base) || $base === '') {
|
if (! is_string($base) || $base === '') {
|
||||||
|
|
@ -113,11 +113,11 @@ public static function bankAccountFolderName(DatiBancari $conto): string
|
||||||
|
|
||||||
$bankCode = match (true) {
|
$bankCode = match (true) {
|
||||||
str_contains($bankLabel, 'monte') || str_contains($bankLabel, 'paschi') || str_contains($bankLabel, 'mps') => 'MPS',
|
str_contains($bankLabel, 'monte') || str_contains($bankLabel, 'paschi') || str_contains($bankLabel, 'mps') => 'MPS',
|
||||||
str_contains($bankLabel, 'intesa') => 'INTESA',
|
str_contains($bankLabel, 'intesa') => 'INTESA',
|
||||||
str_contains($bankLabel, 'unicredit') => 'UNICREDIT',
|
str_contains($bankLabel, 'unicredit') => 'UNICREDIT',
|
||||||
str_contains($bankLabel, 'bper') => 'BPER',
|
str_contains($bankLabel, 'bper') => 'BPER',
|
||||||
str_contains($bankLabel, 'bcc') => 'BCC',
|
str_contains($bankLabel, 'bcc') => 'BCC',
|
||||||
default => strtoupper(self::sanitizeFolder((string) ($conto->denominazione_banca ?: 'BANCA'))),
|
default => strtoupper(self::sanitizeFolder((string) ($conto->denominazione_banca ?: 'BANCA'))),
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($bankCode === '') {
|
if ($bankCode === '') {
|
||||||
|
|
@ -133,7 +133,7 @@ public static function bankAccountFolderName(DatiBancari $conto): string
|
||||||
$identifier = self::sanitizeFolder((string) ($conto->legacy_cod_cassa ?? ''));
|
$identifier = self::sanitizeFolder((string) ($conto->legacy_cod_cassa ?? ''));
|
||||||
}
|
}
|
||||||
if ($identifier === '') {
|
if ($identifier === '') {
|
||||||
$iban = strtoupper(preg_replace('/[^A-Z0-9]+/', '', (string) ($conto->iban ?? '')) ?? '');
|
$iban = strtoupper(preg_replace('/[^A-Z0-9]+/', '', (string) ($conto->iban ?? '')) ?? '');
|
||||||
$identifier = $iban !== '' ? substr($iban, -8) : '';
|
$identifier = $iban !== '' ? substr($iban, -8) : '';
|
||||||
}
|
}
|
||||||
if ($identifier === '') {
|
if ($identifier === '') {
|
||||||
|
|
@ -143,7 +143,7 @@ public static function bankAccountFolderName(DatiBancari $conto): string
|
||||||
return $bankCode . '-' . $typeCode . '-' . $identifier;
|
return $bankCode . '-' . $typeCode . '-' . $identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function bankYearPath(Stabile $stabile, DatiBancari $conto, int|string $year, ?Amministratore $admin = null): ?string
|
public static function bankYearPath(Stabile $stabile, DatiBancari $conto, int | string $year, ?Amministratore $admin = null): ?string
|
||||||
{
|
{
|
||||||
$base = self::stabileBase($stabile, $admin);
|
$base = self::stabileBase($stabile, $admin);
|
||||||
if (! is_string($base) || $base === '') {
|
if (! is_string($base) || $base === '') {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
use Illuminate\Support\ServiceProvider; // <-- La riga che risolve l'errore
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
// <-- La riga che risolve l'errore
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'NetGescon'),
|
'name' => env('APP_NAME', 'NetGescon'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -19,7 +20,7 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'env' => env('APP_ENV', 'production'),
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -27,7 +28,7 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'debug' => (bool) env('APP_DEBUG', false),
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -35,11 +36,11 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'url' => env('APP_URL', 'http://localhost'),
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')),
|
'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')),
|
||||||
|
|
||||||
'asset_url' => env('ASSET_URL'),
|
'asset_url' => env('ASSET_URL'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -47,7 +48,7 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'UTC',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -55,11 +56,11 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'locale' => 'it',
|
'locale' => 'it',
|
||||||
|
|
||||||
'fallback_locale' => 'it',
|
'fallback_locale' => 'it',
|
||||||
|
|
||||||
'faker_locale' => 'en_US',
|
'faker_locale' => 'en_US',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -67,9 +68,9 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'key' => env('APP_KEY'),
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
'cipher' => 'AES-256-CBC',
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -77,7 +78,7 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'maintenance' => [
|
'maintenance' => [
|
||||||
'driver' => 'file',
|
'driver' => 'file',
|
||||||
// 'store' => 'redis',
|
// 'store' => 'redis',
|
||||||
],
|
],
|
||||||
|
|
@ -88,7 +89,7 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'providers' => ServiceProvider::defaultProviders()->merge([
|
'providers' => ServiceProvider::defaultProviders()->merge([
|
||||||
/*
|
/*
|
||||||
* Application Service Providers...
|
* Application Service Providers...
|
||||||
*/
|
*/
|
||||||
|
|
@ -101,14 +102,13 @@
|
||||||
|
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Class Aliases
|
| Class Aliases
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'aliases' => Facade::defaultAliases()->merge([
|
'aliases' => Facade::defaultAliases()->merge([
|
||||||
// 'Example' => App\Facades\Example::class,
|
// 'Example' => App\Facades\Example::class,
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,49 +3,49 @@
|
||||||
return [
|
return [
|
||||||
// Conti tecnici usati per chiusure/aperture automatiche.
|
// Conti tecnici usati per chiusure/aperture automatiche.
|
||||||
// Il codice del conto è già alfanumerico (string): puoi usare anche valori tipo "CHIUSURA".
|
// Il codice del conto è già alfanumerico (string): puoi usare anche valori tipo "CHIUSURA".
|
||||||
'conti_speciali' => [
|
'conti_speciali' => [
|
||||||
'chiusura_codice' => env('CONTABILITA_CONTO_CHIUSURA', '9999'),
|
'chiusura_codice' => env('CONTABILITA_CONTO_CHIUSURA', '9999'),
|
||||||
'chiusura_descrizione' => env('CONTABILITA_CONTO_CHIUSURA_DESC', 'Conto chiusura esercizio'),
|
'chiusura_descrizione' => env('CONTABILITA_CONTO_CHIUSURA_DESC', 'Conto chiusura esercizio'),
|
||||||
'chiusura_tipo' => env('CONTABILITA_CONTO_CHIUSURA_TIPO', 'Patrimonio Netto'),
|
'chiusura_tipo' => env('CONTABILITA_CONTO_CHIUSURA_TIPO', 'Patrimonio Netto'),
|
||||||
|
|
||||||
'apertura_codice' => env('CONTABILITA_CONTO_APERTURA', '9998'),
|
'apertura_codice' => env('CONTABILITA_CONTO_APERTURA', '9998'),
|
||||||
'apertura_descrizione' => env('CONTABILITA_CONTO_APERTURA_DESC', 'Conto apertura esercizio'),
|
'apertura_descrizione' => env('CONTABILITA_CONTO_APERTURA_DESC', 'Conto apertura esercizio'),
|
||||||
'apertura_tipo' => env('CONTABILITA_CONTO_APERTURA_TIPO', 'Patrimonio Netto'),
|
'apertura_tipo' => env('CONTABILITA_CONTO_APERTURA_TIPO', 'Patrimonio Netto'),
|
||||||
|
|
||||||
// Incassi rate: banca/cassa a Dare, crediti condòmini a Avere.
|
// Incassi rate: banca/cassa a Dare, crediti condòmini a Avere.
|
||||||
'crediti_condomini_codice' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI', '1100'),
|
'crediti_condomini_codice' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI', '1100'),
|
||||||
'crediti_condomini_descrizione' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI_DESC', 'Crediti verso condomini'),
|
'crediti_condomini_descrizione' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI_DESC', 'Crediti verso condomini'),
|
||||||
'crediti_condomini_tipo' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI_TIPO', 'Attività'),
|
'crediti_condomini_tipo' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI_TIPO', 'Attività'),
|
||||||
|
|
||||||
// Fatture fornitori: costi (righe) + IVA a credito a Dare; debiti fornitori + ritenute a Avere.
|
// Fatture fornitori: costi (righe) + IVA a credito a Dare; debiti fornitori + ritenute a Avere.
|
||||||
'debiti_fornitori_codice' => env('CONTABILITA_CONTO_DEBITI_FORNITORI', '2000'),
|
'debiti_fornitori_codice' => env('CONTABILITA_CONTO_DEBITI_FORNITORI', '2000'),
|
||||||
'debiti_fornitori_descrizione' => env('CONTABILITA_CONTO_DEBITI_FORNITORI_DESC', 'Debiti verso fornitori'),
|
'debiti_fornitori_descrizione' => env('CONTABILITA_CONTO_DEBITI_FORNITORI_DESC', 'Debiti verso fornitori'),
|
||||||
'debiti_fornitori_tipo' => env('CONTABILITA_CONTO_DEBITI_FORNITORI_TIPO', 'Passività'),
|
'debiti_fornitori_tipo' => env('CONTABILITA_CONTO_DEBITI_FORNITORI_TIPO', 'Passività'),
|
||||||
|
|
||||||
'iva_credito_codice' => env('CONTABILITA_CONTO_IVA_CREDITO', '1210'),
|
'iva_credito_codice' => env('CONTABILITA_CONTO_IVA_CREDITO', '1210'),
|
||||||
'iva_credito_descrizione' => env('CONTABILITA_CONTO_IVA_CREDITO_DESC', 'IVA a credito'),
|
'iva_credito_descrizione' => env('CONTABILITA_CONTO_IVA_CREDITO_DESC', 'IVA a credito'),
|
||||||
'iva_credito_tipo' => env('CONTABILITA_CONTO_IVA_CREDITO_TIPO', 'Attività'),
|
'iva_credito_tipo' => env('CONTABILITA_CONTO_IVA_CREDITO_TIPO', 'Attività'),
|
||||||
|
|
||||||
'ritenute_da_versare_codice' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE', '2010'),
|
'ritenute_da_versare_codice' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE', '2010'),
|
||||||
'ritenute_da_versare_descrizione' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_DESC', 'Erario c/ritenute da versare'),
|
'ritenute_da_versare_descrizione' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_DESC', 'Erario c/ritenute da versare'),
|
||||||
'ritenute_da_versare_tipo' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_TIPO', 'Passività'),
|
'ritenute_da_versare_tipo' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_TIPO', 'Passività'),
|
||||||
|
|
||||||
// Bucket patrimoniali e di quadratura da usare nella riconciliazione operativa.
|
// Bucket patrimoniali e di quadratura da usare nella riconciliazione operativa.
|
||||||
'fondo_riserva_codice' => env('CONTABILITA_CONTO_FONDO_RISERVA', '3100'),
|
'fondo_riserva_codice' => env('CONTABILITA_CONTO_FONDO_RISERVA', '3100'),
|
||||||
'fondo_riserva_descrizione' => env('CONTABILITA_CONTO_FONDO_RISERVA_DESC', 'Fondo di riserva'),
|
'fondo_riserva_descrizione' => env('CONTABILITA_CONTO_FONDO_RISERVA_DESC', 'Fondo di riserva'),
|
||||||
'fondo_riserva_tipo' => env('CONTABILITA_CONTO_FONDO_RISERVA_TIPO', 'Patrimonio Netto'),
|
'fondo_riserva_tipo' => env('CONTABILITA_CONTO_FONDO_RISERVA_TIPO', 'Patrimonio Netto'),
|
||||||
|
|
||||||
'accantonamenti_codice' => env('CONTABILITA_CONTO_ACCANTONAMENTI', '3110'),
|
'accantonamenti_codice' => env('CONTABILITA_CONTO_ACCANTONAMENTI', '3110'),
|
||||||
'accantonamenti_descrizione' => env('CONTABILITA_CONTO_ACCANTONAMENTI_DESC', 'Accantonamenti da destinare'),
|
'accantonamenti_descrizione' => env('CONTABILITA_CONTO_ACCANTONAMENTI_DESC', 'Accantonamenti da destinare'),
|
||||||
'accantonamenti_tipo' => env('CONTABILITA_CONTO_ACCANTONAMENTI_TIPO', 'Patrimonio Netto'),
|
'accantonamenti_tipo' => env('CONTABILITA_CONTO_ACCANTONAMENTI_TIPO', 'Patrimonio Netto'),
|
||||||
|
|
||||||
'rimborsi_da_distribuire_codice' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE', '2105'),
|
'rimborsi_da_distribuire_codice' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE', '2105'),
|
||||||
'rimborsi_da_distribuire_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_DESC', 'Rimborsi da distribuire'),
|
'rimborsi_da_distribuire_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_DESC', 'Rimborsi da distribuire'),
|
||||||
'rimborsi_da_distribuire_tipo' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_TIPO', 'Passività'),
|
'rimborsi_da_distribuire_tipo' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_TIPO', 'Passività'),
|
||||||
|
|
||||||
'rimborsi_da_utilizzare_codice' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE', '1115'),
|
'rimborsi_da_utilizzare_codice' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE', '1115'),
|
||||||
'rimborsi_da_utilizzare_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_DESC', 'Rimborsi da utilizzare'),
|
'rimborsi_da_utilizzare_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_DESC', 'Rimborsi da utilizzare'),
|
||||||
'rimborsi_da_utilizzare_tipo' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_TIPO', 'Attività'),
|
'rimborsi_da_utilizzare_tipo' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_TIPO', 'Attività'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'fatture_elettroniche' => [
|
'fatture_elettroniche' => [
|
||||||
|
|
|
||||||
|
|
@ -49,25 +49,25 @@
|
||||||
],
|
],
|
||||||
|
|
||||||
'amazon' => [
|
'amazon' => [
|
||||||
'referral_tag' => env('AMAZON_REFERRAL_TAG', ''),
|
'referral_tag' => env('AMAZON_REFERRAL_TAG', ''),
|
||||||
'marketplace' => env('AMAZON_MARKETPLACE', 'www.amazon.it'),
|
'marketplace' => env('AMAZON_MARKETPLACE', 'www.amazon.it'),
|
||||||
'creators_enabled' => (bool) env('AMAZON_CREATORS_ENABLED', false),
|
'creators_enabled' => (bool) env('AMAZON_CREATORS_ENABLED', false),
|
||||||
'creators_api_base_url' => env('AMAZON_CREATORS_API_BASE_URL', ''),
|
'creators_api_base_url' => env('AMAZON_CREATORS_API_BASE_URL', ''),
|
||||||
'creators_token_url' => env('AMAZON_CREATORS_TOKEN_URL', ''),
|
'creators_token_url' => env('AMAZON_CREATORS_TOKEN_URL', ''),
|
||||||
'creators_credential_id' => env('AMAZON_CREATORS_CREDENTIAL_ID', ''),
|
'creators_credential_id' => env('AMAZON_CREATORS_CREDENTIAL_ID', ''),
|
||||||
'creators_credential_secret' => env('AMAZON_CREATORS_CREDENTIAL_SECRET', ''),
|
'creators_credential_secret' => env('AMAZON_CREATORS_CREDENTIAL_SECRET', ''),
|
||||||
'creators_credential_version' => env('AMAZON_CREATORS_CREDENTIAL_VERSION', 'v3.2'),
|
'creators_credential_version' => env('AMAZON_CREATORS_CREDENTIAL_VERSION', 'v3.2'),
|
||||||
'creators_timeout' => (int) env('AMAZON_CREATORS_TIMEOUT', 20),
|
'creators_timeout' => (int) env('AMAZON_CREATORS_TIMEOUT', 20),
|
||||||
'creators_paths' => [
|
'creators_paths' => [
|
||||||
'searchItems' => env('AMAZON_CREATORS_SEARCH_PATH', ''),
|
'searchItems' => env('AMAZON_CREATORS_SEARCH_PATH', ''),
|
||||||
'getItems' => env('AMAZON_CREATORS_GET_ITEMS_PATH', ''),
|
'getItems' => env('AMAZON_CREATORS_GET_ITEMS_PATH', ''),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'telegram' => [
|
'telegram' => [
|
||||||
'offers_bot_token' => env('TELEGRAM_OFFERS_BOT_TOKEN', ''),
|
'offers_bot_token' => env('TELEGRAM_OFFERS_BOT_TOKEN', ''),
|
||||||
'offers_chat_id' => env('TELEGRAM_OFFERS_CHAT_ID', ''),
|
'offers_chat_id' => env('TELEGRAM_OFFERS_CHAT_ID', ''),
|
||||||
'offers_channel_name' => env('TELEGRAM_OFFERS_CHANNEL_NAME', ''),
|
'offers_channel_name' => env('TELEGRAM_OFFERS_CHANNEL_NAME', ''),
|
||||||
'offers_webhook_secret' => env('TELEGRAM_OFFERS_WEBHOOK_SECRET', ''),
|
'offers_webhook_secret' => env('TELEGRAM_OFFERS_WEBHOOK_SECRET', ''),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('contabilita_saldi_conti', function (Blueprint $table): void {
|
||||||
|
if (! Schema::hasColumn('contabilita_saldi_conti', 'is_partenza_contabile')) {
|
||||||
|
$table->boolean('is_partenza_contabile')
|
||||||
|
->default(false)
|
||||||
|
->after('saldo')
|
||||||
|
->index();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('contabilita_saldi_conti', function (Blueprint $table): void {
|
||||||
|
if (Schema::hasColumn('contabilita_saldi_conti', 'is_partenza_contabile')) {
|
||||||
|
$table->dropIndex(['is_partenza_contabile']);
|
||||||
|
$table->dropColumn('is_partenza_contabile');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('affitti_immobili', function (Blueprint $table) {
|
||||||
|
if (! Schema::hasColumn('affitti_immobili', 'presa_in_carico_operativa_dal')) {
|
||||||
|
$table->date('presa_in_carico_operativa_dal')
|
||||||
|
->nullable()
|
||||||
|
->after('inizio_contratto');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('affitti_immobili', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('affitti_immobili', 'presa_in_carico_operativa_dal')) {
|
||||||
|
$table->dropColumn('presa_in_carico_operativa_dal');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('fornitori') || Schema::hasColumn('fornitori', 'sia')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('fornitori', function (Blueprint $table): void {
|
||||||
|
$table->string('sia', 32)->nullable()->after('bic')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('fornitori') || ! Schema::hasColumn('fornitori', 'sia')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('fornitori', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('sia');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('stabile_contratti', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete();
|
||||||
|
$table->foreignId('fornitore_id')->nullable()->constrained('fornitori')->nullOnDelete();
|
||||||
|
$table->foreignId('documento_principale_id')->nullable()->constrained('documenti')->nullOnDelete();
|
||||||
|
$table->string('titolo');
|
||||||
|
$table->string('tipo_contratto', 60)->index();
|
||||||
|
$table->string('categoria_impianto', 60)->nullable()->index();
|
||||||
|
$table->string('servizio_label')->nullable();
|
||||||
|
$table->string('codice_contratto', 80)->nullable()->index();
|
||||||
|
$table->string('riferimento_esterno', 120)->nullable();
|
||||||
|
$table->string('stato', 30)->default('attivo')->index();
|
||||||
|
$table->date('data_stipula')->nullable();
|
||||||
|
$table->date('decorrenza_dal')->nullable()->index();
|
||||||
|
$table->date('decorrenza_al')->nullable()->index();
|
||||||
|
$table->string('frequenza_scadenze', 30)->nullable();
|
||||||
|
$table->unsignedTinyInteger('giorno_scadenza')->nullable();
|
||||||
|
$table->unsignedSmallInteger('giorni_preavviso')->default(30);
|
||||||
|
$table->boolean('rinnovo_automatico')->default(false);
|
||||||
|
$table->decimal('importo_periodico', 12, 2)->nullable();
|
||||||
|
$table->string('valuta', 8)->default('EUR');
|
||||||
|
$table->json('dati_contratto')->nullable();
|
||||||
|
$table->json('codici_collegati')->nullable();
|
||||||
|
$table->text('note')->nullable();
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('stabile_contratti');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('scadenze') || Schema::hasColumn('scadenze', 'stabile_contratto_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('scadenze', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('stabile_contratto_id')->nullable()->after('stabile_id')->constrained('stabile_contratti')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('scadenze') || ! Schema::hasColumn('scadenze', 'stabile_contratto_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('scadenze', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('stabile_contratto_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
81
docker/docker-compose.node-local.yml
Normal file
81
docker/docker-compose.node-local.yml
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: netgescon/app:${NETGESCON_VERSION:-latest}
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.prod
|
||||||
|
container_name: ${COMPOSE_PROJECT_NAME:-netgescon_node}_app
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env.node-local
|
||||||
|
environment:
|
||||||
|
DB_CONNECTION: mysql
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 3306
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
RUN_MIGRATIONS: ${RUN_MIGRATIONS:-true}
|
||||||
|
MIGRATIONS_STRICT: ${MIGRATIONS_STRICT:-true}
|
||||||
|
RUN_FILAMENT_ASSETS: ${RUN_FILAMENT_ASSETS:-false}
|
||||||
|
FIX_PERMISSIONS: ${FIX_PERMISSIONS:-true}
|
||||||
|
WAIT_FOR_DB: ${WAIT_FOR_DB:-true}
|
||||||
|
DISABLE_SCHEMA_LOAD: ${DISABLE_SCHEMA_LOAD:-true}
|
||||||
|
volumes:
|
||||||
|
- node_storage:/var/www/storage
|
||||||
|
- node_cache:/var/www/bootstrap/cache
|
||||||
|
- node_public:/var/www/public
|
||||||
|
- ${LEGACY_ARCHIVES_PATH:-/opt/netgescon/legacy-archives}:/mnt/gescon-archives/gescon:ro
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- netgescon_node_local
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: ${COMPOSE_PROJECT_NAME:-netgescon_node}_nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${NODE_HTTP_PORT:-8080}:80"
|
||||||
|
volumes:
|
||||||
|
- node_public:/var/www/public:ro
|
||||||
|
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
networks:
|
||||||
|
- netgescon_node_local
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: ${COMPOSE_PROJECT_NAME:-netgescon_node}_db
|
||||||
|
restart: unless-stopped
|
||||||
|
command: --log-bin-trust-function-creators=1
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: ${DB_DATABASE}
|
||||||
|
MYSQL_USER: ${DB_USERNAME}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- node_db:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- netgescon_node_local
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
container_name: ${COMPOSE_PROJECT_NAME:-netgescon_node}_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- node_redis:/data
|
||||||
|
networks:
|
||||||
|
- netgescon_node_local
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node_db:
|
||||||
|
node_storage:
|
||||||
|
node_cache:
|
||||||
|
node_public:
|
||||||
|
node_redis:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
netgescon_node_local:
|
||||||
|
driver: bridge
|
||||||
82
docker/install-node-local.sh
Normal file
82
docker/install-node-local.sh
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOCKER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$DOCKER_DIR/.." && pwd)"
|
||||||
|
ENV_FILE="${ENV_FILE:-$DOCKER_DIR/.env.node-local}"
|
||||||
|
COMPOSE_FILE="$DOCKER_DIR/docker-compose.node-local.yml"
|
||||||
|
|
||||||
|
resolve_compose_cmd() {
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
echo "docker compose"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
echo "docker-compose"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Errore: non trovo né 'docker compose' né 'docker-compose'." >&2
|
||||||
|
exit 127
|
||||||
|
}
|
||||||
|
|
||||||
|
upsert_env_var() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
if grep -qE "^${key}=" "$ENV_FILE"; then
|
||||||
|
sed -i "s#^${key}=.*#${key}=${value}#" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
printf '\n%s=%s\n' "$key" "$value" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
COMPOSE_CMD="$(resolve_compose_cmd)"
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "Env mancante: $ENV_FILE"
|
||||||
|
echo "Crea prima il file: cp $DOCKER_DIR/.env.node-local.example $ENV_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "$ENV_FILE"
|
||||||
|
|
||||||
|
if [[ -z "${APP_KEY:-}" ]]; then
|
||||||
|
GENERATED_APP_KEY="base64:$(openssl rand -base64 32 | tr -d '\n')"
|
||||||
|
upsert_env_var "APP_KEY" "$GENERATED_APP_KEY"
|
||||||
|
APP_KEY="$GENERATED_APP_KEY"
|
||||||
|
echo "APP_KEY generata automaticamente in $ENV_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${LEGACY_ARCHIVES_PATH:-}" ]]; then
|
||||||
|
mkdir -p "$LEGACY_ARCHIVES_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ROOT_DIR/storage/backups"
|
||||||
|
|
||||||
|
echo "[1/6] Validazione compose"
|
||||||
|
$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" config >/tmp/netgescon-node-local.config
|
||||||
|
|
||||||
|
echo "[2/6] Build immagine applicativa"
|
||||||
|
$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build app
|
||||||
|
|
||||||
|
echo "[3/6] Avvio database e redis"
|
||||||
|
$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d db redis
|
||||||
|
|
||||||
|
echo "[4/6] Avvio applicazione e nginx"
|
||||||
|
$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d app nginx
|
||||||
|
|
||||||
|
echo "[5/6] Creazione database import legacy"
|
||||||
|
$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T db sh -lc \
|
||||||
|
"mysql -uroot -p\"$DB_ROOT_PASSWORD\" -e \"CREATE DATABASE IF NOT EXISTS gescon_import; GRANT ALL PRIVILEGES ON gescon_import.* TO '$DB_USERNAME'@'%'; FLUSH PRIVILEGES;\""
|
||||||
|
|
||||||
|
echo "[6/6] Stato finale"
|
||||||
|
$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" ps
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Installazione nodo locale completata."
|
||||||
|
echo "URL applicazione: ${APP_URL:-http://localhost:${NODE_HTTP_PORT:-8080}}"
|
||||||
|
echo "Backup/restore: usa docker/restore-node-backup.sh dopo aver copiato dump SQL e archivio storage sulla VM."
|
||||||
103
docker/restore-node-backup.sh
Normal file
103
docker/restore-node-backup.sh
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOCKER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${ENV_FILE:-$DOCKER_DIR/.env.node-local}"
|
||||||
|
COMPOSE_FILE="$DOCKER_DIR/docker-compose.node-local.yml"
|
||||||
|
SQL_DUMP=""
|
||||||
|
STORAGE_ARCHIVE=""
|
||||||
|
|
||||||
|
resolve_compose_cmd() {
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
echo "docker compose"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
echo "docker-compose"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Errore: non trovo né 'docker compose' né 'docker-compose'." >&2
|
||||||
|
exit 127
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Uso:
|
||||||
|
$(basename "$0") --sql /percorso/backup.sql[.gz] [--storage /percorso/storage.tar.gz]
|
||||||
|
|
||||||
|
Opzioni:
|
||||||
|
--sql Dump MySQL del sito da ripristinare.
|
||||||
|
--storage Archivio tar.gz della cartella storage da ripristinare sopra /var/www/storage.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--sql)
|
||||||
|
SQL_DUMP="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--storage)
|
||||||
|
STORAGE_ARCHIVE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Argomento non riconosciuto: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$SQL_DUMP" ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "Env mancante: $ENV_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$SQL_DUMP" ]]; then
|
||||||
|
echo "Dump SQL non trovato: $SQL_DUMP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$STORAGE_ARCHIVE" && ! -f "$STORAGE_ARCHIVE" ]]; then
|
||||||
|
echo "Archivio storage non trovato: $STORAGE_ARCHIVE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMPOSE_CMD="$(resolve_compose_cmd)"
|
||||||
|
source "$ENV_FILE"
|
||||||
|
|
||||||
|
echo "[1/4] Avvio stack"
|
||||||
|
$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d db redis app nginx
|
||||||
|
|
||||||
|
echo "[2/4] Ripristino database principale"
|
||||||
|
if [[ "$SQL_DUMP" == *.gz ]]; then
|
||||||
|
gzip -dc "$SQL_DUMP" | $COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T db sh -lc "mysql -uroot -p\"$DB_ROOT_PASSWORD\" \"$DB_DATABASE\""
|
||||||
|
else
|
||||||
|
cat "$SQL_DUMP" | $COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T db sh -lc "mysql -uroot -p\"$DB_ROOT_PASSWORD\" \"$DB_DATABASE\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[3/4] Ripristino storage applicativo"
|
||||||
|
if [[ -n "$STORAGE_ARCHIVE" ]]; then
|
||||||
|
cat "$STORAGE_ARCHIVE" | $COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T app sh -lc "tar -xzf - -C /var/www"
|
||||||
|
else
|
||||||
|
echo "Nessun archivio storage fornito: salto questo passaggio."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[4/4] Pulizia cache Laravel"
|
||||||
|
$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T app php artisan optimize:clear || true
|
||||||
|
|
||||||
|
echo "Restore completato. Verifica login, storage documenti e connessioni locali del nodo."
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
# Installazione Ubuntu 24.04 automatica
|
||||||
|
|
||||||
|
Questa famiglia di script separa tre problemi diversi:
|
||||||
|
|
||||||
|
1. `scripts/install/ubuntu2404-laravel-base.sh`
|
||||||
|
Prepara una macchina Ubuntu 24.04 nuova per progetti Laravel in generale.
|
||||||
|
Installa stack di base: PHP, Composer, Node.js, MySQL, Nginx, Redis, Supervisor.
|
||||||
|
Se non disattivato, installa anche XFCE, XRDP e Visual Studio Code.
|
||||||
|
|
||||||
|
2. `scripts/install/laravel-site-bootstrap.sh`
|
||||||
|
Pubblica un singolo progetto Laravel su una macchina gia pronta.
|
||||||
|
Usa chiavi univoche per sito, file Nginx, cron e Supervisor.
|
||||||
|
Di default si rifiuta di sovrascrivere directory, vhost e database gia esistenti.
|
||||||
|
|
||||||
|
3. `scripts/install/netgescon-node-bootstrap.sh`
|
||||||
|
Prende una macchina gia pronta con lo stack Laravel e installa un nodo NetGescon pronto per il login iniziale e per la pagina `admin-filament/supporto/aggiornamento-nodo`.
|
||||||
|
|
||||||
|
## Installazione via curl
|
||||||
|
|
||||||
|
Se il nodo che espone l'applicazione e raggiungibile, puoi usare direttamente gli endpoint pubblici in `public/installers`.
|
||||||
|
|
||||||
|
Indice rapido browser:
|
||||||
|
|
||||||
|
- `http://192.168.0.200:8000/installers/`
|
||||||
|
- `https://staging.netgescon.it/installers/`
|
||||||
|
|
||||||
|
Script base macchina:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL http://192.168.0.200:8000/installers/ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh
|
||||||
|
sudo bash /tmp/ubuntu2404-laravel-base.sh
|
||||||
|
|
||||||
|
curl -fsSL https://staging.netgescon.it/installers/ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh
|
||||||
|
sudo bash /tmp/ubuntu2404-laravel-base.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Versione server-only senza XFCE, VS Code e XRDP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL http://192.168.0.200:8000/installers/ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh
|
||||||
|
sudo bash /tmp/ubuntu2404-laravel-base.sh --without-xfce --without-vscode --without-xrdp
|
||||||
|
```
|
||||||
|
|
||||||
|
Bootstrap sito Laravel generico:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL http://192.168.0.200:8000/installers/laravel-site-bootstrap.php -o /tmp/laravel-site-bootstrap.sh
|
||||||
|
sudo bash /tmp/laravel-site-bootstrap.sh \
|
||||||
|
--site-key demo-app \
|
||||||
|
--project-name "Demo App" \
|
||||||
|
--repo-url git@your-gitea:studio/demo-app.git \
|
||||||
|
--branch main \
|
||||||
|
--app-url http://demo.example.local \
|
||||||
|
--server-name demo.example.local \
|
||||||
|
--db-password 'StrongDbPass123!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Bootstrap nodo NetGescon:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL http://192.168.0.200:8000/installers/netgescon-node-bootstrap.php -o /tmp/netgescon-node-bootstrap.sh
|
||||||
|
sudo bash /tmp/netgescon-node-bootstrap.sh \
|
||||||
|
--site-key netgescon-prod \
|
||||||
|
--repo-url git@your-gitea:studio/netgescon.git \
|
||||||
|
--branch main \
|
||||||
|
--app-url http://192.168.0.200 \
|
||||||
|
--server-name 192.168.0.200 \
|
||||||
|
--db-password 'StrongDbPass123!' \
|
||||||
|
--admin-email admin@netgescon.local \
|
||||||
|
--admin-password 'StrongAdminPass123!'
|
||||||
|
```
|
||||||
|
|
||||||
|
La stessa chiamata su staging puo diventare:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://staging.netgescon.it/installers/netgescon-node-bootstrap.php -o /tmp/netgescon-node-bootstrap.sh
|
||||||
|
sudo bash /tmp/netgescon-node-bootstrap.sh \
|
||||||
|
--site-key netgescon-staging \
|
||||||
|
--repo-url git@your-gitea:studio/netgescon.git \
|
||||||
|
--branch main \
|
||||||
|
--app-url https://staging.netgescon.it \
|
||||||
|
--server-name staging.netgescon.it \
|
||||||
|
--db-password 'StrongDbPass123!' \
|
||||||
|
--admin-email admin@netgescon.local \
|
||||||
|
--admin-password 'StrongAdminPass123!'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Base machine Laravel
|
||||||
|
|
||||||
|
Esecuzione consigliata:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/install/ubuntu2404-laravel-base.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Varianti utili:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/install/ubuntu2404-laravel-base.sh --without-xfce --without-vscode
|
||||||
|
sudo bash scripts/install/ubuntu2404-laravel-base.sh --projects-dir /srv/laravel-projects
|
||||||
|
```
|
||||||
|
|
||||||
|
Cosa fa:
|
||||||
|
|
||||||
|
- aggiorna Ubuntu 24.04
|
||||||
|
- mostra una schermata iniziale con stato corrente macchina e componenti gia presenti
|
||||||
|
- chiede prima di partire quali blocchi installare o saltare
|
||||||
|
- installa PHP 8.3 e moduli Laravel comuni
|
||||||
|
- installa Composer e Node.js
|
||||||
|
- installa MySQL, Nginx, Redis e Supervisor
|
||||||
|
- apre firewall per SSH, HTTP, HTTPS e XRDP se richiesto
|
||||||
|
- crea una base di lavoro per progetti Laravel in `/srv/laravel-projects`
|
||||||
|
- opzionalmente installa XFCE, XRDP e VS Code
|
||||||
|
|
||||||
|
Se vuoi saltare la schermata interattiva e usare lo script in automazione, puoi aggiungere `--non-interactive` al comando.
|
||||||
|
|
||||||
|
## 2. Bootstrap sito Laravel generico
|
||||||
|
|
||||||
|
Esempio completo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/install/laravel-site-bootstrap.sh \
|
||||||
|
--site-key frontoffice-demo \
|
||||||
|
--project-name "Frontoffice Demo" \
|
||||||
|
--repo-url git@your-gitea:studio/frontoffice-demo.git \
|
||||||
|
--branch main \
|
||||||
|
--app-url https://frontoffice-demo.example.it \
|
||||||
|
--server-name frontoffice-demo.example.it \
|
||||||
|
--db-password 'StrongDbPass123!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Cosa fa:
|
||||||
|
|
||||||
|
- clona il repository applicativo in una directory dedicata, di default `/var/www/<site-key>`
|
||||||
|
- crea un database e un utente MySQL dedicati al sito
|
||||||
|
- prepara `.env`, chiave applicativa e cache Laravel
|
||||||
|
- installa dipendenze Composer e, se presenti, builda gli asset frontend
|
||||||
|
- crea un file Nginx dedicato e separato dagli altri siti pubblicati
|
||||||
|
- configura cron scheduler e, se richiesto, un worker Supervisor dedicato
|
||||||
|
- rifiuta per default di sovrascrivere directory applicative, database o vhost gia esistenti
|
||||||
|
|
||||||
|
Flag utili per riuso controllato:
|
||||||
|
|
||||||
|
- `--allow-existing-app-dir` permette di riusare una worktree git Laravel gia esistente
|
||||||
|
- `--allow-existing-database` permette di riusare database e utente MySQL gia presenti
|
||||||
|
- `--replace-nginx-site` permette di sostituire esplicitamente un vhost gia presente
|
||||||
|
- `--with-queue-worker` abilita un worker Supervisor dedicato al sito
|
||||||
|
|
||||||
|
Questo e il pezzo da usare per avere 2 o 3 siti diversi sulla stessa macchina, per esempio frontoffice, demo e produzione, senza collisioni sui file di servizio.
|
||||||
|
|
||||||
|
## 3. Bootstrap nodo NetGescon
|
||||||
|
|
||||||
|
Esempio completo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/install/netgescon-node-bootstrap.sh \
|
||||||
|
--site-key netgescon-prod \
|
||||||
|
--repo-url git@your-gitea:studio/netgescon.git \
|
||||||
|
--branch main \
|
||||||
|
--app-url https://netgescon.example.it \
|
||||||
|
--server-name netgescon.example.it \
|
||||||
|
--db-password 'StrongDbPass123!' \
|
||||||
|
--admin-email admin@netgescon.example.it \
|
||||||
|
--admin-password 'StrongAdminPass123!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Cosa fa:
|
||||||
|
|
||||||
|
- clona il repository applicativo in `/var/www/netgescon`
|
||||||
|
- clona una seconda copia Git sorgente in `/var/www/netgescon-git-source`
|
||||||
|
- collega il repository sorgente a `storage/app/support/git-source`
|
||||||
|
- configura `.env` con `NETGESCON_GIT_SYNC_SOURCE_REPO`
|
||||||
|
- crea database e utente MySQL
|
||||||
|
- installa dipendenze Composer e, se presenti, builda gli asset frontend
|
||||||
|
- esegue migrazioni
|
||||||
|
- crea un utente iniziale con ruoli `super-admin` e `admin`
|
||||||
|
- configura Nginx, cron scheduler e Supervisor queue worker
|
||||||
|
- lascia il nodo pronto per aprire `admin-filament/supporto/aggiornamento-nodo`
|
||||||
|
|
||||||
|
Guard-rail aggiunti:
|
||||||
|
|
||||||
|
- usa `--site-key` per rendere univoci file Nginx, cron e Supervisor
|
||||||
|
- non sovrascrive piu automaticamente directory, database o vhost gia esistenti
|
||||||
|
- per riusare una installazione esistente servono flag espliciti come `--allow-existing-app-dir`, `--allow-existing-database` e `--replace-nginx-site`
|
||||||
|
|
||||||
|
## Note operative
|
||||||
|
|
||||||
|
- Gli script sono pensati per Ubuntu 24.04.
|
||||||
|
- Lo script NetGescon presuppone che la macchina abbia gia PHP, Composer, Node, MySQL e Nginx installati.
|
||||||
|
- Lo script Laravel generico e quello da riusare per altri progetti non NetGescon sulla stessa macchina.
|
||||||
|
- Se il repository Gitea usa SSH, la macchina deve avere gia la chiave deploy o la chiave utente autorizzata.
|
||||||
|
- Se vuoi una macchina solo server senza desktop, usa la base machine con `--without-xfce --without-vscode --without-xrdp`.
|
||||||
|
- Dopo il bootstrap del nodo conviene configurare subito HTTPS e backup automatici.
|
||||||
|
- Gli endpoint curl leggono direttamente gli script versionati nel repository, quindi dopo pull/deploy espongono sempre la versione aggiornata senza copiare file a mano.
|
||||||
177
docs/INSTALLAZIONE-NODO-LOCALE-0.9.md
Normal file
177
docs/INSTALLAZIONE-NODO-LOCALE-0.9.md
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
# NetGescon 0.9 - Installazione nodo locale da macchina vuota
|
||||||
|
|
||||||
|
Questo documento descrive la prova che ha senso fare adesso: una macchina Ubuntu nuova che installa NetGescon come nodo locale singolo, senza dipendere dai servizi cloud pubblici per funzionare nel quotidiano.
|
||||||
|
|
||||||
|
Obiettivo pratico:
|
||||||
|
|
||||||
|
1. preparare una VM o macchina Ubuntu pulita;
|
||||||
|
2. installare Docker e lo stack applicativo locale;
|
||||||
|
3. avviare NetGescon come nodo singolo;
|
||||||
|
4. ripristinare un backup proveniente dal sito;
|
||||||
|
5. verificare che il nodo lavori in autonomia con DB, storage e servizi locali.
|
||||||
|
|
||||||
|
## Cosa e incluso nel repository
|
||||||
|
|
||||||
|
Per questa prova sono pronti i seguenti file:
|
||||||
|
|
||||||
|
- `docker/docker-compose.node-local.yml`
|
||||||
|
- `docker/.env.node-local.example`
|
||||||
|
- `docker/install-node-local.sh`
|
||||||
|
- `docker/restore-node-backup.sh`
|
||||||
|
- `docker/provision-vm.sh`
|
||||||
|
|
||||||
|
Questo stack non usa il multisito `www/app/demo`. Porta su un solo nodo applicativo con:
|
||||||
|
|
||||||
|
- `app` PHP-FPM Laravel
|
||||||
|
- `nginx`
|
||||||
|
- `mysql`
|
||||||
|
- `redis`
|
||||||
|
|
||||||
|
## Cosa serve dalla macchina remota
|
||||||
|
|
||||||
|
Per procedere senza perdere tempo mi servono solo queste informazioni:
|
||||||
|
|
||||||
|
1. IP della macchina oppure hostname raggiungibile in LAN/VPN.
|
||||||
|
2. Utente SSH e porta SSH.
|
||||||
|
3. Conferma versione Ubuntu (`22.04` o `24.04` va bene).
|
||||||
|
4. RAM, CPU e spazio disco disponibili.
|
||||||
|
5. Path in cui vuoi installare il progetto, ad esempio `/opt/netgescon`.
|
||||||
|
6. Se il nodo dovra rispondere solo in LAN o anche con DNS dedicato.
|
||||||
|
7. Se hai gia pronto il backup da ripristinare:
|
||||||
|
- dump SQL del DB applicativo;
|
||||||
|
- archivio `storage` del sito;
|
||||||
|
- eventuale archivio legacy MDB/Access da montare in `LEGACY_ARCHIVES_PATH`.
|
||||||
|
|
||||||
|
## Bootstrap da macchina vuota
|
||||||
|
|
||||||
|
### 1. Copia repository sulla macchina
|
||||||
|
|
||||||
|
Se la macchina vede Gitea:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt
|
||||||
|
git clone ssh://git@192.168.0.53:2222/michele/netgescon-day0.git netgescon-day0
|
||||||
|
cd /opt/netgescon-day0
|
||||||
|
```
|
||||||
|
|
||||||
|
Se non vuoi usare Git direttamente sul nodo, puoi copiare un archivio del repository gia allineato.
|
||||||
|
|
||||||
|
### 2. Provision base Ubuntu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/netgescon-day0/docker
|
||||||
|
chmod +x provision-vm.sh install-node-local.sh restore-node-backup.sh
|
||||||
|
./provision-vm.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Poi fai logout/login per applicare il gruppo Docker.
|
||||||
|
|
||||||
|
### 3. Prepara env locale del nodo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/netgescon-day0
|
||||||
|
cp docker/.env.node-local.example docker/.env.node-local
|
||||||
|
nano docker/.env.node-local
|
||||||
|
```
|
||||||
|
|
||||||
|
Campi da impostare subito:
|
||||||
|
|
||||||
|
- `APP_URL`
|
||||||
|
- `NODE_HTTP_PORT`
|
||||||
|
- `DB_PASSWORD`
|
||||||
|
- `DB_ROOT_PASSWORD`
|
||||||
|
- `LEGACY_ARCHIVES_PATH`
|
||||||
|
|
||||||
|
`APP_KEY` puo restare vuota: viene generata automaticamente da `install-node-local.sh` al primo avvio.
|
||||||
|
|
||||||
|
### 4. Installa e avvia lo stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/netgescon-day0/docker
|
||||||
|
./install-node-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Lo script:
|
||||||
|
|
||||||
|
1. valida il compose;
|
||||||
|
2. genera `APP_KEY` se manca;
|
||||||
|
3. builda l immagine applicativa;
|
||||||
|
4. avvia DB e Redis;
|
||||||
|
5. avvia app e nginx;
|
||||||
|
6. crea anche il database `gescon_import` per gli import legacy.
|
||||||
|
|
||||||
|
## Restore backup del sito
|
||||||
|
|
||||||
|
Dopo aver copiato sul nodo il dump del database e l archivio storage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/netgescon-day0/docker
|
||||||
|
./restore-node-backup.sh --sql /percorso/backup.sql.gz --storage /percorso/storage.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Note operative:
|
||||||
|
|
||||||
|
- il dump SQL puo essere `.sql` oppure `.sql.gz`;
|
||||||
|
- lo storage e opzionale, ma senza storage il nodo partira senza documenti/allegati storici;
|
||||||
|
- l archivio storage deve essere un `tar.gz` estraibile con radice compatibile con `/var/www/storage`.
|
||||||
|
|
||||||
|
## Verifiche minime dopo restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/netgescon-day0
|
||||||
|
|
||||||
|
docker compose --env-file docker/.env.node-local -f docker/docker-compose.node-local.yml ps
|
||||||
|
docker compose --env-file docker/.env.node-local -f docker/docker-compose.node-local.yml logs -f --tail=100
|
||||||
|
```
|
||||||
|
|
||||||
|
Controlli applicativi minimi:
|
||||||
|
|
||||||
|
1. login backoffice;
|
||||||
|
2. apertura dashboard;
|
||||||
|
3. apertura di almeno una pagina contabile;
|
||||||
|
4. verifica documenti/allegati storici;
|
||||||
|
5. verifica accesso al DB applicativo;
|
||||||
|
6. verifica che il nodo non dipenda da servizi cloud per il funzionamento base.
|
||||||
|
|
||||||
|
## Spiegazione del messaggio visto su staging
|
||||||
|
|
||||||
|
Il pannello `Supporto > Aggiornamento Nodo` mostra due cose diverse:
|
||||||
|
|
||||||
|
1. lo stato Git noto al nodo;
|
||||||
|
2. l eventuale esecuzione dell update applicativo.
|
||||||
|
|
||||||
|
Per questo:
|
||||||
|
|
||||||
|
- `Ultima sync Git` indica quando il nodo ha registrato l ultima sync Git;
|
||||||
|
- `Ultimo update` resta `-` se non e mai stato lanciato un update applicativo da quella pagina.
|
||||||
|
|
||||||
|
Dopo il push su Gitea di oggi, il nodo che ho verificato mostra gia:
|
||||||
|
|
||||||
|
- branch `main`
|
||||||
|
- commit `b3e626b`
|
||||||
|
- tracking `ahead 0 / behind 0`
|
||||||
|
- working tree `pulito`
|
||||||
|
|
||||||
|
Quindi il problema non era un guasto Git: era solo assenza di un update applicativo registrato, mentre la parte Git ora e allineata.
|
||||||
|
|
||||||
|
## Limiti attuali da tenere presenti
|
||||||
|
|
||||||
|
Questa prova locale serve per installazione e ripristino operativo. Restano fuori, salvo configurazione dedicata:
|
||||||
|
|
||||||
|
- manifest distribution pubblico;
|
||||||
|
- licensing per canali;
|
||||||
|
- backup cloud esterni;
|
||||||
|
- servizi Google/Gmail/Drive lato studio;
|
||||||
|
- reverse proxy TLS pubblico multi-dominio.
|
||||||
|
|
||||||
|
## Prossimo passo corretto
|
||||||
|
|
||||||
|
Appena mi dai i dati della macchina remota, facciamo in ordine:
|
||||||
|
|
||||||
|
1. verifica accesso SSH e risorse;
|
||||||
|
2. provisioning Docker;
|
||||||
|
3. copia repository o clone da Gitea;
|
||||||
|
4. creazione `docker/.env.node-local`;
|
||||||
|
5. avvio stack;
|
||||||
|
6. restore backup del sito;
|
||||||
|
7. checklist di collaudo del nodo.
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Integrazione contabile operativa
|
||||||
|
|
||||||
|
## Riconciliazione banca MPS
|
||||||
|
|
||||||
|
La pagina Contabilita > Casse e banche > Movimenti non e piu solo una vista di suggerimenti.
|
||||||
|
La coda di riconciliazione ora puo chiudere due casi operativi direttamente dal movimento banca:
|
||||||
|
|
||||||
|
- Incasso affitto: il movimento positivo puo essere collegato a un canone aperto del modulo affitti. L'azione crea o completa il record in affitti_canoni_pagati e marca il movimento come riconciliato operativamente.
|
||||||
|
- Pagamento fornitore: il movimento negativo puo essere collegato a una fattura del modulo contabilita_fatture_fornitori. L'azione genera la prima nota del pagamento, collega il movimento alla fattura e aggiorna o apre la riga nel registro_ritenute_acconto quando la fattura ha ritenuta.
|
||||||
|
|
||||||
|
## Regole applicate
|
||||||
|
|
||||||
|
- I canoni affitto vengono proposti in base a importo, data, inquilino e coerenza con il conto bancario associato all'affitto.
|
||||||
|
- Le fatture fornitore vengono proposte usando il netto da pagare quando presente, cosi il match resta coerente anche in presenza di ritenuta d'acconto.
|
||||||
|
- I movimenti riconciliati nel dominio operativo vengono marcati in match_data con un esito esplicito, cosi escono dalla coda anche quando non generano subito una registrazione contabile autonoma.
|
||||||
|
|
||||||
|
## Base per moduli dichiarativi
|
||||||
|
|
||||||
|
Questa slice segue la direzione dei moduli dichiarativi:
|
||||||
|
|
||||||
|
- il modulo banca espone una coda operativa e i suoi candidati;
|
||||||
|
- il modulo affitti chiude i canoni nel proprio dominio senza duplicare logica nella UI;
|
||||||
|
- il modulo contabilita fornitore mantiene la responsabilita di prima nota, debito fornitore e registro RA.
|
||||||
|
|
||||||
|
La regola da mantenere nelle prossime slice e semplice: la pagina di regia riconosce il caso e delega la chiusura all'unita funzionale proprietaria del dominio.
|
||||||
116
docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md
Normal file
116
docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
## Slice da fare subito
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Convenzioni archivio da fissare
|
||||||
|
|
||||||
|
### Codice categoria archivio
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
### Regole comuni
|
||||||
|
|
||||||
|
- Il prefisso categoria va salvato anche nella voce archivio su database.
|
||||||
|
- Il valore deve essere in formato Linux-safe: maiuscolo, senza spazi, uniforme.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Data nei file
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### 1. Gestione file base
|
||||||
|
|
||||||
|
Una Blade dedicata deve permettere di registrare il singolo file o una raccolta di fogli con:
|
||||||
|
|
||||||
|
- titolo
|
||||||
|
- descrizione
|
||||||
|
- note
|
||||||
|
- tag
|
||||||
|
- categoria come tag strutturato
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### 2. Primo contenitore fisico
|
||||||
|
|
||||||
|
Dal file base bisogna poter scegliere il primo supporto:
|
||||||
|
|
||||||
|
- busta ad anelli
|
||||||
|
- faldone a tre lembi
|
||||||
|
- raccoglitore
|
||||||
|
- altro supporto scelto a catalogo
|
||||||
|
|
||||||
|
Il record deve restare collegato al documento digitale per consentire nuove stampe dell'etichetta in qualsiasi momento.
|
||||||
|
|
||||||
|
### 3. Contenitori successivi e movimentazione
|
||||||
|
|
||||||
|
Serve una gestione separata dove il materiale gia preparato venga caricato nei contenitori successivi:
|
||||||
|
|
||||||
|
- cartellina
|
||||||
|
- busta
|
||||||
|
- faldone
|
||||||
|
- scatola
|
||||||
|
- scaffale
|
||||||
|
- posto finale
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Plugin locale / bridge dispositivi
|
||||||
|
|
||||||
|
Serve un plugin HTTP locale o agent Windows per passare dati hardware fuori dal browser:
|
||||||
|
|
||||||
|
- TWAIN/WIA scanner
|
||||||
|
- lettori barcode e QR
|
||||||
|
- NFC
|
||||||
|
- seriali
|
||||||
|
- eventuali periferiche di stampa/lettura
|
||||||
|
|
||||||
|
La webapp deve restare il pannello operativo, mentre il bridge locale si occupa della parte hardware.
|
||||||
|
|
||||||
|
## Backlog successivo
|
||||||
|
|
||||||
|
1. Foto ubicazione finale come nel ticket.
|
||||||
|
2. Vista modifica/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.
|
||||||
110
public/installers/index.php
Normal file
110
public/installers/index.php
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
$scheme = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
|
$host = $_SERVER['HTTP_HOST'] ?? '192.168.0.200:8000';
|
||||||
|
$baseUrl = $scheme . '://' . $host . '/installers';
|
||||||
|
|
||||||
|
header('Content-Type: text/html; charset=UTF-8');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NetGescon Installers</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #111827;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
code, pre {
|
||||||
|
font-family: "Fira Code", monospace;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #020617;
|
||||||
|
border: 1px solid #1e293b;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #7dd3fc;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>NetGescon Installers</h1>
|
||||||
|
<p class="muted">Da qui puoi lanciare gli script di installazione direttamente con curl, usando staging.netgescon.it oppure il nodo locale 192.168.0.200.</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>1. Macchina base Ubuntu 24.04</h2>
|
||||||
|
<p>Installa stack Laravel, MySQL, Nginx, Redis, Supervisor e opzionalmente XFCE, XRDP e VS Code.</p>
|
||||||
|
<pre>curl -fsSL <?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh
|
||||||
|
sudo bash /tmp/ubuntu2404-laravel-base.sh</pre>
|
||||||
|
<p class="muted">Versione server-only:</p>
|
||||||
|
<pre>curl -fsSL <?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh
|
||||||
|
sudo bash /tmp/ubuntu2404-laravel-base.sh --without-xfce --without-vscode --without-xrdp</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>2. Bootstrap sito Laravel</h2>
|
||||||
|
<p>Pubblica un progetto Laravel su una macchina gia pronta senza sovrascrivere directory, database o vhost esistenti se non richiesto esplicitamente.</p>
|
||||||
|
<pre>curl -fsSL <?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/laravel-site-bootstrap.php -o /tmp/laravel-site-bootstrap.sh
|
||||||
|
sudo bash /tmp/laravel-site-bootstrap.sh \
|
||||||
|
--site-key demo-app \
|
||||||
|
--project-name "Demo App" \
|
||||||
|
--repo-url git@your-gitea:studio/demo-app.git \
|
||||||
|
--branch main \
|
||||||
|
--app-url <?php echo htmlspecialchars($scheme . '://' . $host, ENT_QUOTES, 'UTF-8') ?> \
|
||||||
|
--server-name <?php echo htmlspecialchars($host, ENT_QUOTES, 'UTF-8') ?> \
|
||||||
|
--db-password 'StrongDbPass123!'</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>3. Bootstrap nodo NetGescon</h2>
|
||||||
|
<p>Clona il repository applicativo, prepara il git-source locale per la sync da browser e lascia il nodo pronto per admin-filament/supporto/aggiornamento-nodo.</p>
|
||||||
|
<pre>curl -fsSL <?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/netgescon-node-bootstrap.php -o /tmp/netgescon-node-bootstrap.sh
|
||||||
|
sudo bash /tmp/netgescon-node-bootstrap.sh \
|
||||||
|
--site-key netgescon-prod \
|
||||||
|
--repo-url git@your-gitea:studio/netgescon.git \
|
||||||
|
--branch main \
|
||||||
|
--app-url <?php echo htmlspecialchars($scheme . '://' . $host, ENT_QUOTES, 'UTF-8') ?> \
|
||||||
|
--server-name <?php echo htmlspecialchars($host, ENT_QUOTES, 'UTF-8') ?> \
|
||||||
|
--db-password 'StrongDbPass123!' \
|
||||||
|
--admin-email admin@netgescon.local \
|
||||||
|
--admin-password 'StrongAdminPass123!'</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Endpoint diretti</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="<?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/ubuntu2404-laravel-base.php"><?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/ubuntu2404-laravel-base.php</a></li>
|
||||||
|
<li><a href="<?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/laravel-site-bootstrap.php"><?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/laravel-site-bootstrap.php</a></li>
|
||||||
|
<li><a href="<?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/netgescon-node-bootstrap.php"><?php echo htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') ?>/netgescon-node-bootstrap.php</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
public/installers/laravel-site-bootstrap.php
Normal file
19
public/installers/laravel-site-bootstrap.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
$scriptPath = dirname(__DIR__, 2) . '/scripts/install/laravel-site-bootstrap.sh';
|
||||||
|
|
||||||
|
if (! is_file($scriptPath) || ! is_readable($scriptPath)) {
|
||||||
|
http_response_code(404);
|
||||||
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
|
echo "Installer script not found.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
|
header('Content-Disposition: inline; filename="laravel-site-bootstrap.sh"');
|
||||||
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||||
|
header('Pragma: no-cache');
|
||||||
|
|
||||||
|
readfile($scriptPath);
|
||||||
19
public/installers/netgescon-node-bootstrap.php
Normal file
19
public/installers/netgescon-node-bootstrap.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
$scriptPath = dirname(__DIR__, 2) . '/scripts/install/netgescon-node-bootstrap.sh';
|
||||||
|
|
||||||
|
if (! is_file($scriptPath) || ! is_readable($scriptPath)) {
|
||||||
|
http_response_code(404);
|
||||||
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
|
echo "Installer script not found.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
|
header('Content-Disposition: inline; filename="netgescon-node-bootstrap.sh"');
|
||||||
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||||
|
header('Pragma: no-cache');
|
||||||
|
|
||||||
|
readfile($scriptPath);
|
||||||
19
public/installers/ubuntu2404-laravel-base.php
Normal file
19
public/installers/ubuntu2404-laravel-base.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
$scriptPath = dirname(__DIR__, 2) . '/scripts/install/ubuntu2404-laravel-base.sh';
|
||||||
|
|
||||||
|
if (! is_file($scriptPath) || ! is_readable($scriptPath)) {
|
||||||
|
http_response_code(404);
|
||||||
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
|
echo "Installer script not found.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
|
header('Content-Disposition: inline; filename="ubuntu2404-laravel-base.sh"');
|
||||||
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||||
|
header('Pragma: no-cache');
|
||||||
|
|
||||||
|
readfile($scriptPath);
|
||||||
|
|
@ -53,6 +53,52 @@ class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-semi
|
||||||
</button>
|
</button>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pt-2 border-t border-blue-200 dark:border-blue-800">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Presa in carico amministrazione:</dt>
|
||||||
|
<dd class="mt-2 space-y-3">
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
wire:model.defer="presaInCaricoAmministrazione"
|
||||||
|
class="w-full rounded-lg border-gray-300 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<x-filament::button size="sm" color="primary" wire:click="saveAmministrazioneSetup">
|
||||||
|
Salva data
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Le gestioni operative vengono filtrate dall'anno di presa in carico in avanti.
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Verbale di nomina:</dt>
|
||||||
|
<dd class="mt-2 space-y-3">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
wire:model="verbaleNominaUpload"
|
||||||
|
class="block w-full text-sm text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<x-filament::button size="sm" color="gray" wire:click="uploadVerbaleNomina">
|
||||||
|
Carica verbale
|
||||||
|
</x-filament::button>
|
||||||
|
@if($this->getVerbaleNominaDocumento())
|
||||||
|
<a
|
||||||
|
href="{{ $this->getVerbaleNominaDocumento()->url_view ?? $this->getVerbaleNominaDocumento()->url_download }}"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-flex items-center rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-1.5 text-xs font-semibold text-emerald-800 dark:border-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-100"
|
||||||
|
>
|
||||||
|
Allegato presente
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<span class="inline-flex items-center rounded-lg border border-amber-200 bg-amber-50 px-3 py-1.5 text-xs font-semibold text-amber-800 dark:border-amber-800 dark:bg-amber-900/30 dark:text-amber-100">
|
||||||
|
Nessun allegato caricato
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-amber-200 bg-gradient-to-r from-amber-50 via-white to-sky-50 p-4"
|
||||||
|
x-data="{
|
||||||
|
scannerActive: false,
|
||||||
|
scannerError: '',
|
||||||
|
stream: null,
|
||||||
|
detector: null,
|
||||||
|
rafId: null,
|
||||||
|
baseUrl: '/admin-filament/strumenti/documenti?tab=lettore-codici',
|
||||||
|
openReader(rawValue) {
|
||||||
|
const value = (rawValue || this.$refs.archiveCode?.value || '').trim();
|
||||||
|
if (value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = `${this.baseUrl}&code=${encodeURIComponent(value)}`;
|
||||||
|
},
|
||||||
|
async startScanner() {
|
||||||
|
this.scannerError = '';
|
||||||
|
if (!('BarcodeDetector' in window)) {
|
||||||
|
this.scannerError = 'BarcodeDetector non disponibile in questo browser. Usa il lettore esterno, il telefono o un tag NFC in input testo.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.detector = new BarcodeDetector({ formats: ['qr_code'] });
|
||||||
|
this.stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
|
||||||
|
this.$refs.video.srcObject = this.stream;
|
||||||
|
await this.$refs.video.play();
|
||||||
|
this.scannerActive = true;
|
||||||
|
this.scanFrame();
|
||||||
|
} catch (error) {
|
||||||
|
this.scannerError = 'Impossibile avviare la camera: ' + (error?.message || 'permesso negato');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopScanner() {
|
||||||
|
this.scannerActive = false;
|
||||||
|
if (this.rafId) {
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
}
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.getTracks().forEach((track) => track.stop());
|
||||||
|
this.stream = null;
|
||||||
|
}
|
||||||
|
if (this.$refs.video) {
|
||||||
|
this.$refs.video.srcObject = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async scanFrame() {
|
||||||
|
if (!this.scannerActive || !this.detector || !this.$refs.video) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const codes = await this.detector.detect(this.$refs.video);
|
||||||
|
if (codes.length > 0 && (codes[0]?.rawValue || '') !== '') {
|
||||||
|
this.$refs.archiveCode.value = codes[0].rawValue;
|
||||||
|
this.openReader(codes[0].rawValue);
|
||||||
|
this.stopScanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.scannerError = 'Errore lettura QR: ' + (error?.message || 'lettura non disponibile');
|
||||||
|
this.stopScanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.rafId = requestAnimationFrame(() => this.scanFrame());
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">{{ $title ?? 'Lettore QR archivio' }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-600">{{ $description ?? 'Scansiona un QR archivio e apri subito il contenuto del nodo selezionato.' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-slate-700 shadow-sm">Visualizza contenuto senza aprire il contenitore</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-[minmax(0,1fr)_auto_auto]">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice archivio / payload QR / NFC</label>
|
||||||
|
<input x-ref="archiveCode" type="text" class="w-full rounded-lg border-slate-200 bg-white text-sm shadow-sm" placeholder="Es. AF-20-0001 oppure NGDOC|AF:AF-20-0001|..." x-on:keydown.enter.prevent="openReader()" />
|
||||||
|
</div>
|
||||||
|
<button type="button" class="inline-flex items-center self-end rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50" x-on:click="openReader()">Apri lettore</button>
|
||||||
|
<button type="button" class="inline-flex items-center self-end rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-slate-800" x-on:click="scannerActive ? stopScanner() : startScanner()">
|
||||||
|
<span x-text="scannerActive ? 'Ferma camera' : 'Leggi QR'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 rounded-lg border border-dashed border-slate-300 bg-white/80 p-3" x-show="scannerActive || scannerError !== ''" x-cloak>
|
||||||
|
<template x-if="scannerActive">
|
||||||
|
<video x-ref="video" class="aspect-video w-full rounded-lg border border-slate-300 bg-slate-900" playsinline muted></video>
|
||||||
|
</template>
|
||||||
|
<template x-if="scannerError !== ''">
|
||||||
|
<div class="text-xs text-amber-800" x-text="scannerError"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -24,4 +24,30 @@
|
||||||
.fi-sidebar-item {
|
.fi-sidebar-item {
|
||||||
margin-block: 0 !important;
|
margin-block: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 63.998rem), (hover: none) and (pointer: coarse) {
|
||||||
|
.fi-body.fi-body-has-navigation .fi-sidebar {
|
||||||
|
border-top: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
position: static !important;
|
||||||
|
inset: auto !important;
|
||||||
|
max-height: none !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fi-body.fi-body-has-navigation .fi-topbar,
|
||||||
|
.fi-body.fi-body-has-navigation .fi-main,
|
||||||
|
.fi-body.fi-body-has-navigation .fi-main-ctn {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fi-body.fi-body-has-navigation .fi-main-ctn {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
overflow-x: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -8,30 +8,9 @@
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((bool) ($stabile->riscaldamento_centralizzato ?? false)) {
|
return method_exists($stabile, 'hasOperationalHeating')
|
||||||
return true;
|
? $stabile->hasOperationalHeating()
|
||||||
}
|
: false;
|
||||||
|
|
||||||
$rateRiscaldamento = $stabile->rate_riscaldamento_mesi ?? [];
|
|
||||||
if (is_string($rateRiscaldamento)) {
|
|
||||||
$rateRiscaldamento = json_decode($rateRiscaldamento, true);
|
|
||||||
}
|
|
||||||
if (is_array($rateRiscaldamento) && $rateRiscaldamento !== []) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = $stabile->configurazione_avanzata ?? [];
|
|
||||||
if (is_string($config)) {
|
|
||||||
$config = json_decode($config, true);
|
|
||||||
}
|
|
||||||
if (is_array($config) && array_key_exists('mostra_riscaldamento', $config) && $config['mostra_riscaldamento']) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return \Illuminate\Support\Facades\DB::table('gestioni_contabili')
|
|
||||||
->where('stabile_id', $stabile->id)
|
|
||||||
->where('tipo_gestione', 'riscaldamento')
|
|
||||||
->exists();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$stabileLabel = static function (?\App\Models\Stabile $stabile): string {
|
$stabileLabel = static function (?\App\Models\Stabile $stabile): string {
|
||||||
|
|
@ -64,20 +43,5 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($stabili->count() > 1)
|
<div class="text-xs text-gray-500">Selettore stabile temporaneamente disattivato.</div>
|
||||||
<form method="POST" action="{{ route('admin-filament.stabile-attivo') }}" class="flex items-center">
|
|
||||||
@csrf
|
|
||||||
<select
|
|
||||||
name="stabile_id"
|
|
||||||
class="fi-input block w-full min-w-[220px]"
|
|
||||||
onchange="this.form.submit()"
|
|
||||||
>
|
|
||||||
@foreach($stabili as $stabile)
|
|
||||||
<option value="{{ $stabile->id }}" @selected($activeId === $stabile->id)>
|
|
||||||
{{ $stabileLabel($stabile) }} — {{ $stabile->denominazione }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,11 @@
|
||||||
<label class="text-xs text-gray-500">Inizio contratto</label>
|
<label class="text-xs text-gray-500">Inizio contratto</label>
|
||||||
<input type="date" wire:model="form.inizio_contratto" class="w-full rounded-md border-gray-300" />
|
<input type="date" wire:model="form.inizio_contratto" class="w-full rounded-md border-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Presa in carico operativa dal</label>
|
||||||
|
<input type="date" wire:model="form.presa_in_carico_operativa_dal" class="w-full rounded-md border-gray-300" />
|
||||||
|
<div class="mt-1 text-[11px] text-gray-500">I canoni precedenti restano fuori dal gestionale operativo e dalla riconciliazione bancaria.</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-500">Ultimo rinnovo</label>
|
<label class="text-xs text-gray-500">Ultimo rinnovo</label>
|
||||||
<input type="date" wire:model="form.ultimo_rinnovo" class="w-full rounded-md border-gray-300" />
|
<input type="date" wire:model="form.ultimo_rinnovo" class="w-full rounded-md border-gray-300" />
|
||||||
|
|
|
||||||
148
resources/views/filament/pages/condomini/contratti-hub.blade.php
Normal file
148
resources/views/filament/pages/condomini/contratti-hub.blade.php
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
<x-filament-panels::page>
|
||||||
|
@php($stats = $this->stats)
|
||||||
|
@php($contratti = $this->contrattiRows)
|
||||||
|
@php($scadenze = $this->upcomingScadenze)
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-gradient-to-r from-cyan-50 via-white to-emerald-50 p-5">
|
||||||
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500">Stabile</div>
|
||||||
|
<h2 class="mt-2 text-2xl font-semibold text-slate-900">HUB Contratti {{ $this->stabileAttivo?->denominazione ? '· ' . $this->stabileAttivo->denominazione : '' }}</h2>
|
||||||
|
<p class="mt-2 max-w-3xl text-sm text-slate-600">Qui registriamo il contratto, i riferimenti tecnici e le scadenze operative da inviare anche al calendario. Il collegamento documentale e contabile parte da qui.</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-white/70 bg-white/80 px-4 py-3 shadow-sm">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Collegamenti</div>
|
||||||
|
<div class="mt-2 text-sm text-slate-700">Fornitore, manutentore, servizio, utenza, documento e codici tecnici nello stesso record.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Contratti</div>
|
||||||
|
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $stats['totale'] ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Attivi</div>
|
||||||
|
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $stats['attivi'] ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Scadenze 45 gg</div>
|
||||||
|
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $stats['scadenze'] ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Documenti contratto</div>
|
||||||
|
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $stats['documenti'] ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.4fr)_minmax(320px,0.6fr)]">
|
||||||
|
<x-filament::section>
|
||||||
|
<x-slot name="heading">Registro contratti</x-slot>
|
||||||
|
<x-slot name="description">MVP: contratto, riferimenti essenziali, documento principale e generazione scadenze.</x-slot>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
@forelse($contratti as $contratto)
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||||
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<div class="text-lg font-semibold text-slate-900">{{ $contratto->titolo }}</div>
|
||||||
|
<span class="inline-flex items-center rounded-full bg-{{ $this->contractBadgeColor($contratto->stato) }}-50 px-2.5 py-1 text-xs font-semibold text-{{ $this->contractBadgeColor($contratto->stato) }}-700">
|
||||||
|
{{ strtoupper($contratto->stato) }}
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-700">
|
||||||
|
{{ $this->contractTypeLabel($contratto->tipo_contratto) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-slate-600">
|
||||||
|
{{ $contratto->servizio_label ?: 'Servizio non specificato' }}
|
||||||
|
@if($contratto->fornitore)
|
||||||
|
· {{ $contratto->fornitore->ragione_sociale ?: trim(($contratto->fornitore->nome ?? '') . ' ' . ($contratto->fornitore->cognome ?? '')) }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-500 lg:text-right">
|
||||||
|
<div>Decorrenza: {{ $contratto->decorrenza_dal?->format('d/m/Y') ?: '—' }}</div>
|
||||||
|
<div>Fine: {{ $contratto->decorrenza_al?->format('d/m/Y') ?: ($contratto->rinnovo_automatico ? 'rinnovo automatico' : '—') }}</div>
|
||||||
|
<div>Frequenza: {{ $contratto->frequenza_scadenze ? str_replace('_', ' ', $contratto->frequenza_scadenze) : '—' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Codice</div>
|
||||||
|
<div class="mt-1 font-medium text-slate-900">{{ $contratto->codice_contratto ?: '—' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Riferimento</div>
|
||||||
|
<div class="mt-1 font-medium text-slate-900">{{ $contratto->riferimento_esterno ?: '—' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Importo</div>
|
||||||
|
<div class="mt-1 font-medium text-slate-900">{{ $contratto->importo_periodico !== null ? '€ ' . number_format((float) $contratto->importo_periodico, 2, ',', '.') : '—' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Documento</div>
|
||||||
|
<div class="mt-1 font-medium text-slate-900">{{ $contratto->documentoPrincipale?->numero_protocollo ?: ($contratto->documentoPrincipale?->nome ?: '—') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($contratto->codici_collegati))
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
@foreach($contratto->codici_collegati as $code)
|
||||||
|
<span class="inline-flex items-center rounded-full bg-cyan-50 px-3 py-1 text-xs font-medium text-cyan-800">
|
||||||
|
{{ $code['chiave'] ?? 'Dato' }}: {{ $code['valore'] ?? '—' }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($contratto->note)
|
||||||
|
<div class="mt-4 rounded-lg bg-slate-50 px-3 py-2 text-sm text-slate-600">{{ $contratto->note }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-xl border border-dashed bg-white px-4 py-10 text-center text-sm text-slate-500">Nessun contratto registrato per lo stabile attivo.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<x-filament::section>
|
||||||
|
<x-slot name="heading">Scadenze programmate</x-slot>
|
||||||
|
<x-slot name="description">Le nuove scadenze vengono registrate sulla tabella calendario operativa.</x-slot>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
@forelse($scadenze as $scadenza)
|
||||||
|
<div class="rounded-xl border bg-white px-4 py-3">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">{{ $scadenza->stabileContratto?->titolo ?: $scadenza->descrizione_sintetica }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">{{ $scadenza->stabileContratto?->tipo_contratto ? $this->contractTypeLabel($scadenza->stabileContratto->tipo_contratto) : 'Scadenza contratto' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">{{ $scadenza->data_scadenza?->format('d/m/Y') ?: '—' }}</div>
|
||||||
|
</div>
|
||||||
|
@if(!empty($scadenza->legacy_payload['codice_contratto']))
|
||||||
|
<div class="mt-2 text-xs text-slate-500">Codice: {{ $scadenza->legacy_payload['codice_contratto'] }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-xl border border-dashed bg-white px-4 py-6 text-sm text-slate-500">Nessuna scadenza contrattuale registrata.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
<x-filament::section>
|
||||||
|
<x-slot name="heading">Copertura dati</x-slot>
|
||||||
|
<div class="space-y-3 text-sm text-slate-600">
|
||||||
|
<div class="rounded-xl border bg-white px-4 py-3">Per acqua, energia e altri impianti usa i codici collegati per registrare POD, PDR, matricole, codice cliente, numero utenza e riferimenti bolletta.</div>
|
||||||
|
<div class="rounded-xl border bg-white px-4 py-3">Il SIA resta sul fornitore; qui teniamo i dati specifici del singolo contratto dello stabile.</div>
|
||||||
|
<div class="rounded-xl border bg-white px-4 py-3">Il documento principale può essere un PDF già registrato nell’archivio documentale con categoria contratto.</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
<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">Gestione documentale mobile</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-600">Pagina dedicata alla lettura QR, al codice mnemonico e all'apertura rapida del contenuto archivistico da telefono o tablet.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@include('filament.components.archive-code-launcher', [
|
||||||
|
'title' => 'Lettore documenti mobile',
|
||||||
|
'description' => 'Scansiona il QR o inserisci il codice mnemonico per aprire il contenuto della cartella, del faldone o della scatola nel lettore archivio.',
|
||||||
|
])
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-4"
|
||||||
|
x-data="{
|
||||||
|
nfcSupported: 'NDEFReader' in window,
|
||||||
|
nfcReading: false,
|
||||||
|
nfcMessage: '',
|
||||||
|
async startNfc() {
|
||||||
|
if (!this.nfcSupported) {
|
||||||
|
this.nfcMessage = 'Web NFC non disponibile in questo browser. Su iPhone Safari al momento non e esposto: usa QR o codice mnemonico.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const reader = new NDEFReader();
|
||||||
|
await reader.scan();
|
||||||
|
this.nfcReading = true;
|
||||||
|
this.nfcMessage = 'Avvicina il tag NFC al telefono.';
|
||||||
|
reader.onreading = ({ message }) => {
|
||||||
|
const record = message.records?.[0];
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value = '';
|
||||||
|
if (record.recordType === 'text') {
|
||||||
|
value = new TextDecoder(record.encoding || 'utf-8').decode(record.data);
|
||||||
|
} else {
|
||||||
|
value = new TextDecoder('utf-8').decode(record.data);
|
||||||
|
}
|
||||||
|
window.location.href = `/admin-filament/strumenti/documenti?tab=lettore-codici&code=${encodeURIComponent(value)}`;
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.nfcReading = false;
|
||||||
|
this.nfcMessage = 'Lettura NFC non avviata: ' + (error?.message || 'permesso negato');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Prova NFC da browser</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-600">Facciamo test progressivi: sui browser compatibili puoi avviare la lettura del tag NFC direttamente dalla pagina e aprire il lettore archivio con il codice letto.</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700" x-text="nfcSupported ? 'Web NFC disponibile' : 'Web NFC non disponibile'"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<button type="button" x-on:click="startNfc()" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Avvia lettura NFC</button>
|
||||||
|
<a href="{{ \App\Filament\Pages\Strumenti\DocumentiArchivio::getUrl(panel: 'admin-filament') }}?tab=lettore-codici" class="inline-flex items-center rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-slate-800">Apri lettore completo</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 rounded-lg border border-dashed border-slate-300 bg-slate-50 px-3 py-3 text-xs text-slate-600" x-text="nfcMessage !== '' ? nfcMessage : 'Sugli iPhone via Safari il Web NFC normalmente non e disponibile: per ora il percorso affidabile resta QR o codice mnemonico.'"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 lg:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Contenuto archivio</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-600">Apri il lettore archivio per vedere catena contenitori, documenti interni e ubicazione del nodo scansionato.</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ \App\Filament\Pages\Strumenti\DocumentiArchivio::getUrl(panel: 'admin-filament') }}?tab=lettore-codici" 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 contenuto</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Archivio digitale</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-600">Apri l'archivio digitale per vedere la lista documenti collegata al contenitore o al protocollo trovato.</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ \App\Filament\Pages\Strumenti\DocumentiArchivio::getUrl(panel: 'admin-filament') }}?tab=archivio-digitale" 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 archivio digitale</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Movimentazione</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-600">Dal lettore puoi poi passare alla movimentazione per spostare il contenitore in altro faldone, scaffale o postazione.</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ \App\Filament\Pages\Strumenti\DocumentiArchivio::getUrl(panel: 'admin-filament') }}?tab=movimentazione-codici" class="inline-flex items-center rounded-md bg-amber-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-600">Apri movimentazione</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
'scansione-documenti' => 'Scansione documenti',
|
'scansione-documenti' => 'Scansione documenti',
|
||||||
'etichette-generiche' => 'Etichette generiche',
|
'etichette-generiche' => 'Etichette generiche',
|
||||||
'movimentazione-codici' => 'Movimentazione per codice',
|
'movimentazione-codici' => 'Movimentazione per codice',
|
||||||
|
'lettore-codici' => 'Lettore codici',
|
||||||
'template-drive' => 'Template Drive',
|
'template-drive' => 'Template Drive',
|
||||||
'archivio-fisico' => 'Archivio fisico',
|
'archivio-fisico' => 'Archivio fisico',
|
||||||
'permessi' => 'Permessi e cartelle',
|
'permessi' => 'Permessi e cartelle',
|
||||||
|
|
@ -72,7 +73,7 @@
|
||||||
@if($this->hubTab === 'protocollo-comunicazioni')
|
@if($this->hubTab === 'protocollo-comunicazioni')
|
||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<x-slot name="heading">Protocollo comunicazioni</x-slot>
|
<x-slot name="heading">Protocollo comunicazioni</x-slot>
|
||||||
<x-slot name="description">Prima tab operativa per l'amministratore: ultimi documenti protocollati, filtri rapidi e vista a box con anteprima PDF o lista compatta.</x-slot>
|
<x-slot name="description">Prima tab operativa per l'amministratore: ultimi documenti protocollati, filtri rapidi e vista a box universale per PDF, immagini, Office e allegati testuali.</x-slot>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
|
@ -82,9 +83,9 @@
|
||||||
<div class="mt-1 text-sm text-slate-500">Documenti non fattura nello stabile attivo.</div>
|
<div class="mt-1 text-sm text-slate-500">Documenti non fattura nello stabile attivo.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="text-xs uppercase tracking-wide text-slate-500">Con PDF</div>
|
<div class="text-xs uppercase tracking-wide text-slate-500">Con allegato</div>
|
||||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $protocolloStats['con_pdf'] ?? 0 }}</div>
|
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $protocolloStats['con_file'] ?? 0 }}</div>
|
||||||
<div class="mt-1 text-sm text-slate-500">Apribili in anteprima o stampa.</div>
|
<div class="mt-1 text-sm text-slate-500">Apribili inline o scaricabili secondo il tipo file.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="text-xs uppercase tracking-wide text-slate-500">Questo mese</div>
|
<div class="text-xs uppercase tracking-wide text-slate-500">Questo mese</div>
|
||||||
|
|
@ -144,39 +145,47 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($this->protocolloView === 'cards')
|
@if($this->protocolloView === 'cards')
|
||||||
<div class="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
@forelse($this->protocolloComunicazioniRows as $row)
|
@forelse($this->protocolloComunicazioniRows as $row)
|
||||||
<article class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
<article class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||||
<div class="space-y-3 p-4">
|
<div class="space-y-3 p-4">
|
||||||
<div class="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
<div class="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
||||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-slate-700">{{ $row['protocollo'] }}</span>
|
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-slate-700">{{ $row['protocollo'] }}</span>
|
||||||
<span>{{ $row['categoria'] }}</span>
|
<span>{{ $row['categoria'] }}</span>
|
||||||
|
<span class="rounded-full bg-sky-50 px-2.5 py-1 text-sky-700">{{ $row['file_badge'] }}</span>
|
||||||
<span>{{ $row['data'] }}</span>
|
<span>{{ $row['data'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-base font-semibold text-slate-900">{{ $row['titolo'] }}</h3>
|
<h3 class="line-clamp-2 text-sm font-semibold text-slate-900">{{ $row['titolo'] }}</h3>
|
||||||
<p class="mt-1 text-sm text-slate-600">{{ $row['descrizione'] !== '' ? $row['descrizione'] : 'Nessuna descrizione registrata.' }}</p>
|
<p class="mt-1 line-clamp-3 text-xs text-slate-600">{{ $row['descrizione'] !== '' ? $row['descrizione'] : 'Nessuna descrizione registrata.' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1 text-xs text-slate-500">
|
<div class="space-y-1 text-[11px] text-slate-500">
|
||||||
<div>Cartella: {{ $row['cartella'] }}</div>
|
<div>Cartella: {{ $row['cartella'] }}</div>
|
||||||
@if($row['fornitore'] !== '')
|
@if($row['fornitore'] !== '')
|
||||||
<div>Ente / referente: {{ $row['fornitore'] }}</div>
|
<div>Ente / referente: {{ $row['fornitore'] }}</div>
|
||||||
@endif
|
@endif
|
||||||
@if($row['archive_reference'] !== '')
|
@if($row['archive_code'] !== '')
|
||||||
|
<div>Codice archivio: {{ $row['archive_code'] }}</div>
|
||||||
|
@endif
|
||||||
|
@if($row['archive_reference'] !== '' && $row['archive_reference'] !== $row['archive_code'])
|
||||||
<div>Rif. archivio: {{ $row['archive_reference'] }}</div>
|
<div>Rif. archivio: {{ $row['archive_reference'] }}</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2 pt-1">
|
<div class="flex flex-wrap gap-2 pt-1">
|
||||||
<a href="{{ $row['preview_url'] }}" target="_blank" class="inline-flex items-center rounded-lg bg-slate-900 px-3 py-2 text-xs font-medium text-white hover:bg-slate-800">Apri</a>
|
@if($row['has_file'] && $row['preview_url'])
|
||||||
<a href="{{ $row['download_url'] }}" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 hover:bg-slate-50">Scarica</a>
|
<a href="{{ $row['preview_url'] }}" target="_blank" class="inline-flex items-center rounded-lg bg-slate-900 px-3 py-2 text-xs font-medium text-white hover:bg-slate-800">Apri</a>
|
||||||
@if($row['has_pdf'])
|
<a href="{{ $row['download_url'] }}" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 hover:bg-slate-50">Scarica</a>
|
||||||
|
@else
|
||||||
|
<span class="inline-flex items-center rounded-lg border border-dashed border-slate-300 bg-slate-50 px-3 py-2 text-xs font-medium text-slate-500">Solo metadati</span>
|
||||||
|
@endif
|
||||||
|
@if($row['can_print'] && $row['print_url'])
|
||||||
<a href="{{ $row['print_url'] }}" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 hover:bg-slate-50">Stampa</a>
|
<a href="{{ $row['print_url'] }}" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 hover:bg-slate-50">Stampa</a>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@empty
|
@empty
|
||||||
<div class="rounded-xl border border-dashed border-slate-300 bg-white px-4 py-10 text-sm text-slate-500 md:col-span-2 2xl:col-span-3">
|
<div class="rounded-xl border border-dashed border-slate-300 bg-white px-4 py-10 text-sm text-slate-500 md:col-span-2 xl:col-span-4">
|
||||||
Nessun documento protocollato trovato con i filtri correnti.
|
Nessun documento protocollato trovato con i filtri correnti.
|
||||||
</div>
|
</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
|
|
@ -199,8 +208,11 @@
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="truncate font-semibold text-slate-900">{{ $row['titolo'] }}</div>
|
<div class="truncate font-semibold text-slate-900">{{ $row['titolo'] }}</div>
|
||||||
<div class="mt-1 text-xs text-slate-500">{{ $row['descrizione'] !== '' ? $row['descrizione'] : 'Nessuna descrizione registrata.' }}</div>
|
<div class="mt-1 text-xs text-slate-500">{{ $row['descrizione'] !== '' ? $row['descrizione'] : 'Nessuna descrizione registrata.' }}</div>
|
||||||
@if($row['archive_reference'] !== '')
|
@if($row['archive_code'] !== '')
|
||||||
<div class="mt-1 text-xs text-sky-700">{{ $row['archive_reference'] }}</div>
|
<div class="mt-1 text-xs text-sky-700">{{ $row['archive_code'] }}</div>
|
||||||
|
@endif
|
||||||
|
@if($row['archive_reference'] !== '' && $row['archive_reference'] !== $row['archive_code'])
|
||||||
|
<div class="mt-1 text-[11px] text-slate-500">{{ $row['archive_reference'] }}</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -208,8 +220,10 @@
|
||||||
<div class="mt-1 text-xs text-slate-500">{{ $row['cartella'] }}</div>
|
<div class="mt-1 text-xs text-slate-500">{{ $row['cartella'] }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<a href="{{ $row['preview_url'] }}" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50">Apri</a>
|
@if($row['has_file'] && $row['preview_url'])
|
||||||
@if($row['has_pdf'])
|
<a href="{{ $row['preview_url'] }}" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50">Apri</a>
|
||||||
|
@endif
|
||||||
|
@if($row['can_print'] && $row['print_url'])
|
||||||
<a href="{{ $row['print_url'] }}" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50">Stampa</a>
|
<a href="{{ $row['print_url'] }}" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50">Stampa</a>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -437,143 +451,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
@elseif($this->hubTab === 'etichette-generiche')
|
@elseif($this->hubTab === 'etichette-generiche')
|
||||||
<x-filament::section>
|
@include('filament.pages.strumenti.partials.generic-label-builder')
|
||||||
<x-slot name="heading">Etichette generiche per contenitori</x-slot>
|
|
||||||
<x-slot name="description">Tab dedicata alla stampa rapida di etichette per faldoni, scatole, raccoglitori e buste. Il QR resta obbligatorio e il codice generato diventa il riferimento stabile per annidamento e scansione.</x-slot>
|
|
||||||
|
|
||||||
<div class="grid gap-4 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
|
|
||||||
<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">Nuova etichetta generica</div>
|
|
||||||
<div class="mt-1 text-xs text-slate-500">Crea il contenitore, stampa subito e poi usalo come nodo della catena QR: fattura, cartella, dock, scatola, magazzino, scaffale.</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">QR incluso</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Titolo etichetta</label>
|
|
||||||
<input type="text" wire:model.defer="genericLabelForm.titolo" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. SCATOLA CONTRATTI ASCENSORI" />
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Seconda riga</label>
|
|
||||||
<input type="text" wire:model.defer="genericLabelForm.linea_secondaria" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Stabile Roma 12 · 2024/2026" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Tipologia</label>
|
|
||||||
<select wire:model.defer="genericLabelForm.tipo_item" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
|
||||||
@foreach($this->physicalArchiveTypeOptions as $typeKey => $typeLabel)
|
|
||||||
@if($typeKey !== 'documento')
|
|
||||||
<option value="{{ $typeKey }}">{{ $typeLabel }}</option>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Modello etichetta</label>
|
|
||||||
<select wire:model.defer="genericLabelForm.modello_etichetta" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
|
||||||
@foreach($this->getModelliEtichettaOptions() as $labelKey => $labelText)
|
|
||||||
<option value="{{ $labelKey }}">{{ $labelText }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Dentro contenitore</label>
|
|
||||||
<select wire:model.defer="genericLabelForm.parent_id" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
|
||||||
<option value="">Radice archivio</option>
|
|
||||||
@foreach($this->physicalArchiveParentOptions as $parentId => $parentLabel)
|
|
||||||
<option value="{{ $parentId }}">{{ $parentLabel }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Supporto</label>
|
|
||||||
<input type="text" wire:model.defer="genericLabelForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Scatola o raccoglitore" />
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Link acquisto / Amazon</label>
|
|
||||||
<input type="text" wire:model.defer="genericLabelForm.amazon_url" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="URL completo o ASIN Amazon" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Magazzino</label>
|
|
||||||
<input type="text" wire:model.defer="genericLabelForm.magazzino" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Deposito EUR" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Scaffale</label>
|
|
||||||
<input type="text" wire:model.defer="genericLabelForm.scaffale" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. B2" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Ripiano</label>
|
|
||||||
<input type="text" wire:model.defer="genericLabelForm.ripiano" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. 4" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Dettaglio ubicazione</label>
|
|
||||||
<input type="text" wire:model.defer="genericLabelForm.ubicazione_dettaglio" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. corridoio destro" />
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Nota breve</label>
|
|
||||||
<textarea wire:model.defer="genericLabelForm.note" rows="3" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Contenuto, periodo, riferimenti operativi..."></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
|
||||||
<button type="button" wire:click="saveGenericArchiveLabel" class="inline-flex items-center rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-slate-800">Crea e prepara la stampa</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="rounded-xl border bg-white p-4">
|
|
||||||
<div class="text-sm font-semibold text-slate-900">Standard operativo</div>
|
|
||||||
<div class="mt-3 grid gap-3 text-sm text-slate-600 md:grid-cols-2">
|
|
||||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Il codice univoco viene generato automaticamente e diventa il nodo da scansionare.</div>
|
|
||||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Il QR usa lo stesso schema pubblico gia host-agnostic del resto dell'archivio fisico.</div>
|
|
||||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Il link Amazon puo essere salvato come URL o come ASIN, con tag affiliato aggiunto dal super-admin.</div>
|
|
||||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Questa tab rende visibile la nuova etichetta senza doverla cercare dentro il registro generale.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-xl border bg-white p-4">
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
|
||||||
<div class="text-sm font-semibold text-slate-900">Ultime etichette generiche</div>
|
|
||||||
<div class="text-xs text-slate-500">Stampa immediata 11354 / 99014</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 space-y-3">
|
|
||||||
@forelse($this->genericLabelRows as $row)
|
|
||||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">
|
|
||||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
|
||||||
<div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="rounded-full bg-slate-900 px-2.5 py-1 text-[11px] font-semibold text-white">{{ $row['codice_univoco'] }}</span>
|
|
||||||
<span class="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-slate-700">{{ $row['tipo'] }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-sm font-semibold text-slate-900">{{ $row['titolo'] }}</div>
|
|
||||||
@if($row['linea_secondaria'] !== '')
|
|
||||||
<div class="mt-1 text-xs text-slate-500">{{ $row['linea_secondaria'] }}</div>
|
|
||||||
@endif
|
|
||||||
<div class="mt-1 text-xs text-slate-500">Percorso: {{ $row['percorso'] }}</div>
|
|
||||||
@if($row['ubicazione'] !== '')
|
|
||||||
<div class="mt-1 text-xs text-slate-500">Ubicazione: {{ $row['ubicazione'] }}</div>
|
|
||||||
@endif
|
|
||||||
@if($row['amazon_url'] !== '')
|
|
||||||
<div class="mt-2"><a href="{{ $row['amazon_url'] }}" target="_blank" class="text-xs font-medium text-sky-700 underline decoration-sky-300 underline-offset-2">Apri link acquisto</a></div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
|
||||||
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=11354&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=-1" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">11354</a>
|
|
||||||
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=11354&autoprint=1&dymo_fix=1&dymo_rot=90&dymo_dx=0&dymo_dy=0" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">11354 verticale</a>
|
|
||||||
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=99014&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=3.5&dymo_dy=3.5" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">99014</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@empty
|
|
||||||
<div class="rounded-lg border border-dashed border-slate-300 bg-white px-4 py-6 text-sm text-slate-500">Nessuna etichetta generica creata ancora per lo stabile attivo.</div>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
@elseif($this->hubTab === 'movimentazione-codici')
|
@elseif($this->hubTab === 'movimentazione-codici')
|
||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<x-slot name="heading">Movimentazione per codice</x-slot>
|
<x-slot name="heading">Movimentazione per codice</x-slot>
|
||||||
|
|
@ -583,7 +461,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="text-sm font-semibold text-slate-900">Sposta per codice</div>
|
<div class="text-sm font-semibold text-slate-900">Sposta per codice</div>
|
||||||
<div class="mt-1 text-xs text-slate-500">Scansiona o inserisci codice sorgente e codice destinazione. Il contenitore padre viene aggiornato senza dover aprire il registro generale.</div>
|
<div class="mt-1 text-xs text-slate-500">Scansiona o inserisci codice sorgente e codice destinazione. Qui aggiorni anche supporto e ubicazione operativa senza rientrare nella scheda di censimento.</div>
|
||||||
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice sorgente</label>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice sorgente</label>
|
||||||
|
|
@ -594,6 +472,42 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
||||||
<input type="text" wire:model.defer="physicalArchiveMoveForm.target_code" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. AF-20-0010" />
|
<input type="text" wire:model.defer="physicalArchiveMoveForm.target_code" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. AF-20-0010" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Ubicazione dopo lo spostamento</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Se lasci i campi vuoti, il sistema eredita la posizione del contenitore destinazione e conserva il supporto attuale.</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-full bg-white px-3 py-1 text-[11px] font-semibold text-slate-600">Operativo</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Supporto operativo</label>
|
||||||
|
<select wire:model.defer="physicalArchiveMoveForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||||
|
<option value="">Mantieni attuale</option>
|
||||||
|
@foreach($this->archivioFisicoTypes as $type)
|
||||||
|
<option value="{{ $type }}">{{ $type }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Magazzino</label>
|
||||||
|
<input type="text" wire:model.defer="physicalArchiveMoveForm.magazzino" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Eredita dal contenitore" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Scaffale</label>
|
||||||
|
<input type="text" wire:model.defer="physicalArchiveMoveForm.scaffale" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Eredita dal contenitore" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Ripiano</label>
|
||||||
|
<input type="text" wire:model.defer="physicalArchiveMoveForm.ripiano" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Eredita dal contenitore" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2 xl:col-span-2">
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Dettaglio ubicazione</label>
|
||||||
|
<input type="text" wire:model.defer="physicalArchiveMoveForm.ubicazione_dettaglio" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. lato finestra, mobile sinistro, archivio storico" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
<button type="button" wire:click="movePhysicalArchiveItem" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition hover:border-slate-400 hover:bg-slate-50">Registra movimentazione</button>
|
<button type="button" wire:click="movePhysicalArchiveItem" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition hover:border-slate-400 hover:bg-slate-50">Registra movimentazione</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -601,7 +515,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
||||||
|
|
||||||
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
|
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
|
||||||
<div class="text-sm font-semibold text-amber-900">Catena QR prevista</div>
|
<div class="text-sm font-semibold text-amber-900">Catena QR prevista</div>
|
||||||
<div class="mt-2 text-sm text-amber-800">QR Fattura -> QR Cartella trasparente -> QR Dock -> QR Scatola -> QR Magazzino -> QR Scaffale. Questa tab separa il movimento dal censimento del contenuto.</div>
|
<div class="mt-2 text-sm text-amber-800">QR Foglio -> QR Cartellina -> QR Busta ad anelli -> QR Faldone -> QR Faldone con lacci -> QR Scatola -> QR Scaffale -> QR Posto. Questa tab separa il movimento dal censimento del contenuto.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -621,6 +535,229 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
@elseif($this->hubTab === 'lettore-codici')
|
||||||
|
<x-filament::section>
|
||||||
|
<x-slot name="heading">Lettore codici archivio</x-slot>
|
||||||
|
<x-slot name="description">Scansiona un QR, passa un tag NFC o inserisci un codice archivio per vedere subito il contenuto di una cartella, di un faldone o di una scatola.</x-slot>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$selectedLookupItem = $this->archiveLookupSelectedItem;
|
||||||
|
$selectedLookupChain = $this->archiveLookupSelectedChain;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="grid gap-4 xl:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="rounded-xl border bg-white p-4"
|
||||||
|
x-data="{
|
||||||
|
scannerActive: false,
|
||||||
|
scannerError: '',
|
||||||
|
stream: null,
|
||||||
|
detector: null,
|
||||||
|
rafId: null,
|
||||||
|
async startScanner() {
|
||||||
|
this.scannerError = '';
|
||||||
|
if (!('BarcodeDetector' in window)) {
|
||||||
|
this.scannerError = 'Questo browser non espone BarcodeDetector. Usa il lettore esterno, il QR scanner del telefono o l\'NFC come input testo.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.detector = new BarcodeDetector({ formats: ['qr_code'] });
|
||||||
|
this.stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
|
||||||
|
this.$refs.video.srcObject = this.stream;
|
||||||
|
await this.$refs.video.play();
|
||||||
|
this.scannerActive = true;
|
||||||
|
this.scanFrame();
|
||||||
|
} catch (error) {
|
||||||
|
this.scannerError = 'Impossibile avviare la camera: ' + (error?.message || 'permesso negato');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopScanner() {
|
||||||
|
this.scannerActive = false;
|
||||||
|
if (this.rafId) {
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
}
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.getTracks().forEach((track) => track.stop());
|
||||||
|
this.stream = null;
|
||||||
|
}
|
||||||
|
if (this.$refs.video) {
|
||||||
|
this.$refs.video.srcObject = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async scanFrame() {
|
||||||
|
if (!this.scannerActive || !this.detector || !this.$refs.video) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const codes = await this.detector.detect(this.$refs.video);
|
||||||
|
if (codes.length > 0) {
|
||||||
|
const rawValue = codes[0]?.rawValue || '';
|
||||||
|
if (rawValue !== '') {
|
||||||
|
this.$refs.codeInput.value = rawValue;
|
||||||
|
this.$refs.codeInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
this.stopScanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.scannerError = 'Errore lettura QR: ' + (error?.message || 'lettura non disponibile');
|
||||||
|
this.stopScanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rafId = requestAnimationFrame(() => this.scanFrame());
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Leggi QR / NFC / codice archivio</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Il sistema estrae il codice archivio dal payload QR e ti mostra subito padre, contenuto interno e documento digitale collegato.</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">QR e NFC ready</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-[minmax(0,1fr)_auto_auto]">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice o payload letto</label>
|
||||||
|
<input x-ref="codeInput" type="text" wire:model.live.debounce.150ms="archiveLookupCode" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. AF-20-0001 oppure NGDOC|AF:AF-20-0001|TIPO:SCATOLA|..." />
|
||||||
|
</div>
|
||||||
|
<button type="button" wire:click="openArchiveLookupFromCode" class="inline-flex items-center self-end rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Apri contenuto</button>
|
||||||
|
<button type="button" x-on:click="scannerActive ? stopScanner() : startScanner()" class="inline-flex items-center self-end rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-slate-800">
|
||||||
|
<span x-text="scannerActive ? 'Ferma camera' : 'Leggi QR con camera'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 rounded-lg border border-dashed border-slate-300 bg-slate-50 p-3" x-show="scannerActive || scannerError !== ''" x-cloak>
|
||||||
|
<template x-if="scannerActive">
|
||||||
|
<video x-ref="video" class="aspect-video w-full rounded-lg border border-slate-300 bg-slate-900" playsinline muted></video>
|
||||||
|
</template>
|
||||||
|
<template x-if="scannerError !== ''">
|
||||||
|
<div class="text-xs text-amber-800" x-text="scannerError"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Ricerca archivio</label>
|
||||||
|
<input type="text" wire:model.live.debounce.250ms="archiveLookupSearch" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Codice, titolo, nota, scaffale, payload QR..." />
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-xs text-slate-600">
|
||||||
|
Usa questa pagina anche per i contenitori di trasferimento amministratore: i protocolli si muovono nel contenitore generale senza cambiare i QR già stampati.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Risultati archivio</div>
|
||||||
|
<div class="text-xs text-slate-500">{{ count($this->archiveLookupResults) }} risultati</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
@forelse($this->archiveLookupResults as $row)
|
||||||
|
<button type="button" wire:click="selectArchiveLookupItem({{ $row['id'] }})" class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-left hover:border-sky-300 hover:bg-sky-50/40">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="rounded-full bg-slate-900 px-2.5 py-1 text-[11px] font-semibold text-white">{{ $row['codice_univoco'] }}</span>
|
||||||
|
<span class="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-slate-700">{{ $row['tipo'] }}</span>
|
||||||
|
<span class="text-[11px] text-slate-500">{{ $row['children_count'] }} elementi interni</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm font-semibold text-slate-900">{{ $row['titolo'] }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Padre: {{ $row['parent'] }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Percorso: {{ $row['percorso'] }}</div>
|
||||||
|
@if($row['ubicazione'] !== '')
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Ubicazione: {{ $row['ubicazione'] }}</div>
|
||||||
|
@endif
|
||||||
|
@if($row['documento'])
|
||||||
|
<div class="mt-1 text-xs text-sky-700">Documento digitale: {{ $row['documento'] }}</div>
|
||||||
|
@endif
|
||||||
|
</button>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-lg border border-dashed border-slate-300 bg-white px-4 py-6 text-sm text-slate-500">Nessun nodo archivio trovato con i filtri correnti.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Contenuto del nodo selezionato</div>
|
||||||
|
@if($selectedLookupItem)
|
||||||
|
<div class="mt-4 space-y-4">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="rounded-full bg-slate-900 px-2.5 py-1 text-[11px] font-semibold text-white">{{ $selectedLookupItem->codice_univoco }}</span>
|
||||||
|
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium text-slate-700">{{ $selectedLookupItem->tipo_label }}</span>
|
||||||
|
@if($selectedLookupItem->nfc_reference)
|
||||||
|
<span class="rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-medium text-emerald-700">NFC {{ $selectedLookupItem->nfc_reference }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-lg font-semibold text-slate-900">{{ $selectedLookupItem->titolo }}</div>
|
||||||
|
<div class="mt-1 text-sm text-slate-500">{{ $selectedLookupItem->percorso_fisico }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-xs text-slate-600">
|
||||||
|
Payload QR: {{ $selectedLookupItem->qr_code_data }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($selectedLookupChain !== [])
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Catena contenitori</div>
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
@foreach($selectedLookupChain as $node)
|
||||||
|
<button type="button" wire:click="selectArchiveLookupItem({{ $node['id'] }})" class="rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 hover:border-sky-300 hover:text-sky-700">
|
||||||
|
{{ $node['codice_univoco'] }} · {{ $node['titolo'] }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-sm text-slate-700">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Ubicazione</div>
|
||||||
|
<div class="mt-2 font-medium">{{ collect([$selectedLookupItem->magazzino, $selectedLookupItem->scaffale, $selectedLookupItem->ripiano, $selectedLookupItem->ubicazione_dettaglio])->filter()->implode(' · ') ?: 'Non definita' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-sm text-slate-700">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Data archiviazione</div>
|
||||||
|
<div class="mt-2 font-medium">{{ $selectedLookupItem->data_archiviazione?->format('d-m-Y') ?: '—' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($selectedLookupItem->documento)
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-white px-3 py-3">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Documento digitale collegato</div>
|
||||||
|
<div class="mt-2 text-sm font-medium text-slate-900">{{ $selectedLookupItem->documento->nome_file ?: $selectedLookupItem->documento->nome }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">{{ $selectedLookupItem->documento->resolveArchiveDisplayReference() }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Contenuto interno</div>
|
||||||
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||||
|
@forelse($selectedLookupItem->children as $child)
|
||||||
|
<button type="button" wire:click="selectArchiveLookupItem({{ $child->id }})" class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-left hover:border-sky-300 hover:bg-sky-50/40">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">{{ $child->codice_univoco }}</div>
|
||||||
|
<div class="mt-1 text-sm text-slate-700">{{ $child->titolo }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">{{ $child->tipo_label }}</div>
|
||||||
|
@if($child->documento)
|
||||||
|
<div class="mt-1 text-xs text-sky-700">{{ $child->documento->nome_file ?: $child->documento->nome }}</div>
|
||||||
|
@endif
|
||||||
|
</button>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-lg border border-dashed border-slate-300 bg-white px-4 py-6 text-sm text-slate-500 md:col-span-2">Questo nodo non contiene ancora altri elementi interni.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="mt-4 rounded-lg border border-dashed border-slate-300 bg-slate-50 px-4 py-8 text-sm text-slate-500">Scansiona un QR, passa un tag NFC o seleziona un risultato a sinistra per vedere il contenuto del contenitore.</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
@elseif($this->hubTab === 'template-drive')
|
@elseif($this->hubTab === 'template-drive')
|
||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<x-slot name="heading">Template cartelle Drive</x-slot>
|
<x-slot name="heading">Template cartelle Drive</x-slot>
|
||||||
|
|
@ -756,21 +893,8 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Nota</label>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Nota</label>
|
||||||
<textarea wire:model.defer="physicalArchiveForm.note" rows="3" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Elenco fogli, riferimento verbale, contenuto della busta..."></textarea>
|
<textarea wire:model.defer="physicalArchiveForm.note" rows="3" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Elenco fogli, riferimento verbale, contenuto della busta..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="md:col-span-2 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Magazzino</label>
|
Dopo il primo censimento usa la tab <span class="font-semibold text-slate-900">Movimentazione per codice</span> per assegnare supporto operativo, magazzino, scaffale, ripiano e dettaglio ubicazione ai passaggi successivi.
|
||||||
<input type="text" wire:model.defer="physicalArchiveForm.magazzino" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Sede EUR" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Scaffale</label>
|
|
||||||
<input type="text" wire:model.defer="physicalArchiveForm.scaffale" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. A3" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Ripiano</label>
|
|
||||||
<input type="text" wire:model.defer="physicalArchiveForm.ripiano" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. 2" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Dettaglio ubicazione</label>
|
|
||||||
<input type="text" wire:model.defer="physicalArchiveForm.ubicazione_dettaglio" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. lato finestra" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@
|
||||||
'sky' => 'from-sky-600 to-sky-500',
|
'sky' => 'from-sky-600 to-sky-500',
|
||||||
default => 'from-slate-700 to-slate-600',
|
default => 'from-slate-700 to-slate-600',
|
||||||
};
|
};
|
||||||
|
$fontSizeMap = ['xs' => 10, 'sm' => 12, 'md' => 14, 'lg' => 18, 'xl' => 24];
|
||||||
|
$fontSizes = is_array($preview['font_sizes'] ?? null) ? $preview['font_sizes'] : [];
|
||||||
|
$mnemonicFontPx = $fontSizeMap[$fontSizes['mnemonic'] ?? 'xl'] ?? 24;
|
||||||
|
$titleFontPx = $fontSizeMap[$fontSizes['title'] ?? 'xl'] ?? 24;
|
||||||
|
$subtitleFontPx = $fontSizeMap[$fontSizes['subtitle'] ?? 'md'] ?? 14;
|
||||||
|
$linesFontPx = $fontSizeMap[$fontSizes['lines'] ?? 'md'] ?? 14;
|
||||||
|
$noteFontPx = $fontSizeMap[$fontSizes['note'] ?? 'sm'] ?? 12;
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
|
@ -67,18 +74,48 @@
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="grid gap-3 md:grid-cols-[minmax(0,1fr)_180px]">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice mnemonico</label>
|
<div>
|
||||||
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.mnemonic_code" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. GER00079" />
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice mnemonico</label>
|
||||||
<div class="mt-1 text-[11px] text-slate-500">Suggerito: {{ $this->mnemonicCodeHint ?? 'GER00079' }}</div>
|
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.mnemonic_code" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. GER00079" />
|
||||||
|
<div class="mt-1 text-[11px] text-slate-500">Suggerito: {{ $this->mnemonicCodeHint ?? 'GER00079' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Grandezza codice</label>
|
||||||
|
<select wire:model.live="genericLabelForm.mnemonic_font_size" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||||
|
@foreach($this->genericLabelFontSizeOptions as $sizeKey => $sizeLabel)
|
||||||
|
<option value="{{ $sizeKey }}">{{ $sizeLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2 grid gap-3 md:grid-cols-[minmax(0,1fr)_180px]">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Titolo etichetta</label>
|
<div>
|
||||||
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.titolo" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. SCATOLA CONTRATTI ASCENSORI" />
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Titolo etichetta</label>
|
||||||
|
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.titolo" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. SCATOLA CONTRATTI ASCENSORI" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Grandezza titolo</label>
|
||||||
|
<select wire:model.live="genericLabelForm.title_font_size" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||||
|
@foreach($this->genericLabelFontSizeOptions as $sizeKey => $sizeLabel)
|
||||||
|
<option value="{{ $sizeKey }}">{{ $sizeLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2 grid gap-3 md:grid-cols-[minmax(0,1fr)_180px]">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Seconda riga</label>
|
<div>
|
||||||
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.linea_secondaria" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Stabile Roma 12 · 2024/2026" />
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Seconda riga</label>
|
||||||
|
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.linea_secondaria" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Stabile Roma 12 · 2024/2026" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Grandezza seconda riga</label>
|
||||||
|
<select wire:model.live="genericLabelForm.subtitle_font_size" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||||
|
@foreach($this->genericLabelFontSizeOptions as $sizeKey => $sizeLabel)
|
||||||
|
<option value="{{ $sizeKey }}">{{ $sizeLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Tipologia</label>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Tipologia</label>
|
||||||
|
|
@ -111,31 +148,53 @@
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Supporto</label>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Supporto</label>
|
||||||
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Scatola o raccoglitore" />
|
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Scatola o raccoglitore" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="md:col-span-2 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Riga stabile 1</label>
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<select wire:model.live="genericLabelForm.stable_line_one_source" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
<div>
|
||||||
@foreach($this->genericLabelStableFieldOptions as $fieldKey => $fieldLabel)
|
<div class="text-sm font-semibold text-slate-900">Quattro righe configurabili</div>
|
||||||
<option value="{{ $fieldKey }}">{{ $fieldLabel }}</option>
|
<div class="mt-1 text-xs text-slate-500">Per ogni riga scegli se leggere un campo, scrivere testo libero, lasciare la riga bianca o disattivarla.</div>
|
||||||
@endforeach
|
</div>
|
||||||
</select>
|
<div class="w-full lg:max-w-[180px]">
|
||||||
</div>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Grandezza righe</label>
|
||||||
<div>
|
<select wire:model.live="genericLabelForm.lines_font_size" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Riga stabile 2</label>
|
@foreach($this->genericLabelFontSizeOptions as $sizeKey => $sizeLabel)
|
||||||
<select wire:model.live="genericLabelForm.stable_line_two_source" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
<option value="{{ $sizeKey }}">{{ $sizeLabel }}</option>
|
||||||
@foreach($this->genericLabelStableFieldOptions as $fieldKey => $fieldLabel)
|
@endforeach
|
||||||
<option value="{{ $fieldKey }}">{{ $fieldLabel }}</option>
|
</select>
|
||||||
@endforeach
|
</div>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
<div class="mt-4 grid gap-3">
|
||||||
<div>
|
@for($index = 1; $index <= 4; $index++)
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Righe bianche scrivibili</label>
|
@php
|
||||||
<select wire:model.live="genericLabelForm.blank_lines_count" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
$modeKey = 'line_' . $index . '_mode';
|
||||||
<option value="0">Nessuna</option>
|
$sourceKey = 'line_' . $index . '_source';
|
||||||
<option value="1">1 riga</option>
|
$textKey = 'line_' . $index . '_text';
|
||||||
<option value="2">2 righe</option>
|
$currentMode = data_get($this->genericLabelForm, $modeKey, 'none');
|
||||||
<option value="3">3 righe</option>
|
@endphp
|
||||||
<option value="4">4 righe</option>
|
<div class="grid gap-3 rounded-lg border border-slate-200 bg-white p-3 xl:grid-cols-[160px_minmax(0,1fr)_minmax(0,1fr)]">
|
||||||
</select>
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Riga {{ $index }}</label>
|
||||||
|
<select wire:model.live="genericLabelForm.{{ $modeKey }}" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||||
|
@foreach($this->genericLabelLineModeOptions as $modeValue => $modeLabel)
|
||||||
|
<option value="{{ $modeValue }}">{{ $modeLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Campo archivio</label>
|
||||||
|
<select wire:model.live="genericLabelForm.{{ $sourceKey }}" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" {{ $currentMode !== 'source' ? 'disabled' : '' }}>
|
||||||
|
@foreach($this->genericLabelStableFieldOptions as $fieldKey => $fieldLabel)
|
||||||
|
<option value="{{ $fieldKey }}">{{ $fieldLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Testo libero</label>
|
||||||
|
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.{{ $textKey }}" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Scrivi il testo della riga" {{ $currentMode !== 'text' ? 'disabled' : '' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Prodotto Amazon</label>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Prodotto Amazon</label>
|
||||||
|
|
@ -211,9 +270,19 @@
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Dettaglio ubicazione</label>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Dettaglio ubicazione</label>
|
||||||
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.ubicazione_dettaglio" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. corridoio destro" />
|
<input type="text" wire:model.live.debounce.150ms="genericLabelForm.ubicazione_dettaglio" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. corridoio destro" />
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2 grid gap-3 md:grid-cols-[minmax(0,1fr)_180px]">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Nota breve</label>
|
<div>
|
||||||
<textarea wire:model.live.debounce.200ms="genericLabelForm.note" rows="3" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Contenuto, periodo, riferimenti operativi..."></textarea>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Nota breve</label>
|
||||||
|
<textarea wire:model.live.debounce.200ms="genericLabelForm.note" rows="3" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Contenuto, periodo, riferimenti operativi..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Grandezza note</label>
|
||||||
|
<select wire:model.live="genericLabelForm.note_font_size" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
|
||||||
|
@foreach($this->genericLabelFontSizeOptions as $sizeKey => $sizeLabel)
|
||||||
|
<option value="{{ $sizeKey }}">{{ $sizeLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -240,11 +309,11 @@
|
||||||
<div class="mx-auto w-full max-w-[430px] overflow-hidden rounded-xl border-2 border-slate-900 bg-white shadow-sm">
|
<div class="mx-auto w-full max-w-[430px] overflow-hidden rounded-xl border-2 border-slate-900 bg-white shadow-sm">
|
||||||
<div class="grid min-h-[260px] grid-cols-[1fr_82px] gap-4 p-4">
|
<div class="grid min-h-[260px] grid-cols-[1fr_82px] gap-4 p-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="border-b-2 border-slate-900 pb-2 text-[22px] font-black uppercase tracking-[0.16em] text-slate-900">{{ $preview['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079') }}</div>
|
<div class="border-b-2 border-slate-900 pb-2 font-black uppercase tracking-[0.16em] text-slate-900" style="font-size: {{ $mnemonicFontPx }}px;">{{ $preview['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079') }}</div>
|
||||||
<div class="mt-3 text-[19px] font-bold uppercase leading-tight text-slate-900">{{ data_get($preview, 'slots.title') !== '' ? data_get($preview, 'slots.title') : 'Titolo etichetta' }}</div>
|
<div class="mt-3 font-bold uppercase leading-tight text-slate-900" style="font-size: {{ $titleFontPx }}px;">{{ data_get($preview, 'slots.title') !== '' ? data_get($preview, 'slots.title') : 'Titolo etichetta' }}</div>
|
||||||
<div class="mt-2 text-[13px] font-medium leading-snug text-slate-700">{{ data_get($preview, 'slots.subtitle') !== '' ? data_get($preview, 'slots.subtitle') : 'Seconda riga descrittiva' }}</div>
|
<div class="mt-2 font-medium leading-snug text-slate-700" style="font-size: {{ $subtitleFontPx }}px;">{{ data_get($preview, 'slots.subtitle') !== '' ? data_get($preview, 'slots.subtitle') : 'Seconda riga descrittiva' }}</div>
|
||||||
|
|
||||||
<div class="mt-4 space-y-2 text-[12px] leading-snug text-slate-700">
|
<div class="mt-4 space-y-2 leading-snug text-slate-700" style="font-size: {{ $linesFontPx }}px;">
|
||||||
@foreach(($preview['stable_lines'] ?? []) as $stableLine)
|
@foreach(($preview['stable_lines'] ?? []) as $stableLine)
|
||||||
@if($stableLine === '__BLANK_LINE__')
|
@if($stableLine === '__BLANK_LINE__')
|
||||||
<div class="h-5 border-b border-slate-400"></div>
|
<div class="h-5 border-b border-slate-400"></div>
|
||||||
|
|
@ -261,7 +330,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(data_get($preview, 'slots.note') !== '')
|
@if(data_get($preview, 'slots.note') !== '')
|
||||||
<div class="mt-4 rounded-lg border border-dashed border-slate-300 px-3 py-2 text-[11px] leading-relaxed text-slate-600">{{ data_get($preview, 'slots.note') }}</div>
|
<div class="mt-4 rounded-lg border border-dashed border-slate-300 px-3 py-2 leading-relaxed text-slate-600" style="font-size: {{ $noteFontPx }}px;">{{ data_get($preview, 'slots.note') }}</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -278,16 +347,27 @@
|
||||||
@endif
|
@endif
|
||||||
<div class="relative flex h-full {{ $isBottomQr ? 'flex-col' : 'flex-row' }} gap-3 p-4 {{ $isWideFormat ? 'min-h-[220px]' : 'min-h-[150px]' }}">
|
<div class="relative flex h-full {{ $isBottomQr ? 'flex-col' : 'flex-row' }} gap-3 p-4 {{ $isWideFormat ? 'min-h-[220px]' : 'min-h-[150px]' }}">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="text-[10px] font-semibold uppercase tracking-[0.25em] text-slate-500">{{ $preview['payload']['codice_univoco'] ?? 'AF-QR-PREVIEW' }}</div>
|
<div class="font-semibold uppercase tracking-[0.25em] text-slate-500" style="font-size: {{ max(10, $mnemonicFontPx - 10) }}px;">{{ $preview['payload']['codice_univoco'] ?? 'AF-QR-PREVIEW' }}</div>
|
||||||
<div class="mt-2 text-lg font-bold leading-tight text-slate-900">{{ data_get($preview, 'slots.title') !== '' ? data_get($preview, 'slots.title') : 'Titolo etichetta' }}</div>
|
<div class="mt-2 font-bold leading-tight text-slate-900" style="font-size: {{ $titleFontPx }}px;">{{ data_get($preview, 'slots.title') !== '' ? data_get($preview, 'slots.title') : 'Titolo etichetta' }}</div>
|
||||||
@if(data_get($preview, 'slots.subtitle') !== '')
|
@if(data_get($preview, 'slots.subtitle') !== '')
|
||||||
<div class="mt-2 text-sm font-medium text-slate-600">{{ data_get($preview, 'slots.subtitle') }}</div>
|
<div class="mt-2 font-medium text-slate-600" style="font-size: {{ $subtitleFontPx }}px;">{{ data_get($preview, 'slots.subtitle') }}</div>
|
||||||
@endif
|
@endif
|
||||||
@if(data_get($preview, 'slots.meta') !== '')
|
@if(data_get($preview, 'slots.meta') !== '')
|
||||||
<div class="mt-3 text-xs leading-relaxed text-slate-500">{{ data_get($preview, 'slots.meta') }}</div>
|
<div class="mt-3 leading-relaxed text-slate-500" style="font-size: {{ $linesFontPx }}px;">{{ data_get($preview, 'slots.meta') }}</div>
|
||||||
|
@endif
|
||||||
|
@if(($preview['stable_lines'] ?? []) !== [])
|
||||||
|
<div class="mt-3 space-y-1 leading-relaxed text-slate-600" style="font-size: {{ $linesFontPx }}px;">
|
||||||
|
@foreach(($preview['stable_lines'] ?? []) as $stableLine)
|
||||||
|
@if($stableLine === '__BLANK_LINE__')
|
||||||
|
<div class="h-5 border-b border-slate-300"></div>
|
||||||
|
@elseif($stableLine !== '')
|
||||||
|
<div>{{ $stableLine }}</div>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if(data_get($preview, 'slots.note') !== '')
|
@if(data_get($preview, 'slots.note') !== '')
|
||||||
<div class="mt-3 rounded-lg border border-dashed border-slate-200 bg-slate-50 px-2 py-2 text-[11px] text-slate-500">{{ data_get($preview, 'slots.note') }}</div>
|
<div class="mt-3 rounded-lg border border-dashed border-slate-200 bg-slate-50 px-2 py-2 text-slate-500" style="font-size: {{ $noteFontPx }}px;">{{ data_get($preview, 'slots.note') }}</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="mt-3 rounded-lg border border-slate-200 bg-white px-2 py-2 text-[11px] text-slate-500">
|
<div class="mt-3 rounded-lg border border-slate-200 bg-white px-2 py-2 text-[11px] text-slate-500">
|
||||||
QR payload: {{ $preview['payload']['qr_payload'] ?? 'NGDOC|AF:AF-QR-PREVIEW|TIPO:SCATOLA|STB:0|PARENT:RADICE' }}
|
QR payload: {{ $preview['payload']['qr_payload'] ?? 'NGDOC|AF:AF-QR-PREVIEW|TIPO:SCATOLA|STB:0|PARENT:RADICE' }}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-semibold text-sky-900">Launcher esterno per update e sync Git</div>
|
<div class="text-sm font-semibold text-sky-900">Launcher esterno per update e sync Git</div>
|
||||||
<div class="mt-1 text-[11px] text-sky-800">
|
<div class="mt-1 text-[11px] text-sky-800">
|
||||||
Usa questa pagina per lanciare aggiornamenti senza restare dentro la pagina completa Modifiche mentre Livewire e i file vengono riallineati.
|
Usa questa pagina per lanciare aggiornamenti senza restare dentro la pagina completa Modifiche mentre Livewire e i file vengono riallineati. Il flusso principale da Gitea ora blocca nuove richieste, crea il backup pre-update, aggiorna DB e ricostruisce l'app prima di riaprire il nodo.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
|
|
@ -31,8 +31,8 @@
|
||||||
|
|
||||||
<div class="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
|
<div class="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="text-sm font-semibold text-slate-900">Sync Git da Gitea</div>
|
<div class="text-sm font-semibold text-slate-900">Refresh nodo da Gitea</div>
|
||||||
<div class="mt-1 text-[11px] text-slate-500">Flusso consigliato per staging e nodi interni.</div>
|
<div class="mt-1 text-[11px] text-slate-500">Flusso consigliato per staging e nodi interni: backup, finestra protetta, sync Git, dipendenze, migrate e cache rebuild.</div>
|
||||||
|
|
||||||
@if(! $this->ticketAttachmentMetadataReady)
|
@if(! $this->ticketAttachmentMetadataReady)
|
||||||
<div class="mt-3 rounded-lg border border-amber-300 bg-amber-50 p-3 text-[11px] text-amber-900">
|
<div class="mt-3 rounded-lg border border-amber-300 bg-amber-50 p-3 text-[11px] text-amber-900">
|
||||||
|
|
@ -59,29 +59,36 @@
|
||||||
<x-filament::button size="sm" color="success" wire:click="runNodeRefresh" :disabled="!$supportCanRunUpdate">
|
<x-filament::button size="sm" color="success" wire:click="runNodeRefresh" :disabled="!$supportCanRunUpdate">
|
||||||
Aggiorna nodo completo da Gitea
|
Aggiorna nodo completo da Gitea
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
<x-filament::button size="sm" wire:click="runGitSync" :disabled="!$supportCanRunUpdate">
|
<x-filament::button size="sm" color="warning" wire:click="runNodeRefresh" :disabled="!$supportCanRunUpdate">
|
||||||
Aggiorna staging da Gitea
|
Aggiorna staging da Gitea
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
<x-filament::button size="sm" color="info" wire:click="refreshGitSyncProgress" :disabled="!$this->gitSyncInProgress">
|
<x-filament::button size="sm" wire:click="runGitSync" :disabled="!$supportCanRunUpdate">
|
||||||
Aggiorna avanzamento Git
|
Solo sync Git avanzata
|
||||||
|
</x-filament::button>
|
||||||
|
<x-filament::button size="sm" color="info" wire:click="refreshUpdateProgress" :disabled="!$this->updateInProgress">
|
||||||
|
Aggiorna avanzamento refresh
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($this->gitSyncInProgress)
|
<div class="mt-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-[11px] text-amber-900">
|
||||||
<div class="mt-3 rounded-lg border bg-slate-50 p-3" wire:poll.2s="refreshGitSyncProgress">
|
Il pulsante <span class="font-semibold">Aggiorna staging da Gitea</span> esegue il refresh completo del nodo: backup pre-update, maintenance mode per bloccare nuove operazioni utente, sync da Gitea, <span class="font-mono">composer install</span>, tentativo di build frontend se disponibile, <span class="font-mono">migrate --force</span>, pulizia cache, rebuild Blade e riapertura automatica del sistema.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($this->updateInProgress)
|
||||||
|
<div class="mt-3 rounded-lg border bg-slate-50 p-3" wire:poll.2s="refreshUpdateProgress">
|
||||||
@else
|
@else
|
||||||
<div class="mt-3 rounded-lg border bg-slate-50 p-3">
|
<div class="mt-3 rounded-lg border bg-slate-50 p-3">
|
||||||
@endif
|
@endif
|
||||||
<div class="mb-1 flex items-center justify-between text-[11px] text-gray-600">
|
<div class="mb-1 flex items-center justify-between text-[11px] text-gray-600">
|
||||||
<span class="font-semibold">Avanzamento sync Git</span>
|
<span class="font-semibold">Avanzamento refresh nodo</span>
|
||||||
<span class="font-mono">{{ (int) $this->gitSyncProgressPercent }}%</span>
|
<span class="font-mono">{{ (int) $this->updateProgressPercent }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-2 w-full overflow-hidden rounded bg-gray-200">
|
<div class="h-2 w-full overflow-hidden rounded bg-gray-200">
|
||||||
<div class="h-2 {{ $this->gitSyncProgressStatus === 'failed' ? 'bg-rose-500' : 'bg-emerald-500' }}" style="width: {{ max(0, min(100, (int) $this->gitSyncProgressPercent)) }}%"></div>
|
<div class="h-2 {{ $this->updateProgressStatus === 'failed' ? 'bg-rose-500' : 'bg-emerald-500' }}" style="width: {{ max(0, min(100, (int) $this->updateProgressPercent)) }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-[11px] text-gray-600">{{ $this->gitSyncProgressMessage }}</div>
|
<div class="mt-1 text-[11px] text-gray-600">{{ $this->updateProgressMessage }}</div>
|
||||||
@if(filled($this->lastGitSyncIssue))
|
@if(filled($this->lastUpdateIssue))
|
||||||
<div class="mt-2 rounded border border-amber-300 bg-amber-50 px-2 py-2 text-[11px] text-amber-900">{{ $this->lastGitSyncIssue }}</div>
|
<div class="mt-2 rounded border border-amber-300 bg-amber-50 px-2 py-2 text-[11px] text-amber-900">{{ $this->lastUpdateIssue }}</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -108,7 +115,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-[11px] text-slate-600">
|
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-[11px] text-slate-600">
|
||||||
Il flusso consigliato del nodo e: push su Gitea, poi "Aggiorna nodo completo da Gitea" da questa pagina. Gli archivi caricati manualmente dentro storage o cartelle private non vengono trasferiti da Git, salvo procedure dedicate di backup/sync dati.
|
Il flusso consigliato del nodo e: push su Gitea, poi "Aggiorna staging da Gitea" da questa pagina. I dati utente non vengono distribuiti via Git: per questo il launcher esegue prima il backup pre-update e poi una finestra di maintenance per evitare scritture concorrenti durante il riallineamento del nodo.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(filled($this->pendingGitPreviewIssue))
|
@if(filled($this->pendingGitPreviewIssue))
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,5 @@
|
||||||
<x-filament-panels::page wire:poll.8s="refreshLiveCallBanner">
|
<x-filament-panels::page>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@if($liveIncomingCall)
|
|
||||||
@php
|
|
||||||
$rubricaUrl = $this->getLiveRubricaUrl();
|
|
||||||
$estrattoUrl = $this->getLiveEstrattoContoUrl();
|
|
||||||
@endphp
|
|
||||||
<div class="rounded-xl border border-emerald-300 bg-emerald-50 p-4">
|
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<div class="text-sm font-semibold text-emerald-900">Chiamata in arrivo in tempo reale</div>
|
|
||||||
<div class="mt-1 text-xs text-emerald-800">
|
|
||||||
Numero: <span class="font-semibold">{{ $liveIncomingCall['phone'] ?? '-' }}</span>
|
|
||||||
@if(!empty($liveIncomingCall['target_extension']))
|
|
||||||
· Interno: {{ $liveIncomingCall['target_extension'] }}
|
|
||||||
@endif
|
|
||||||
@if(!empty($liveIncomingCall['received_at']))
|
|
||||||
· Ricevuta: {{ $liveIncomingCall['received_at'] }}
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@if(!empty($liveIncomingCall['rubrica_nome']))
|
|
||||||
<div class="mt-1 text-xs text-emerald-800">Contatto riconosciuto: {{ $liveIncomingCall['rubrica_nome'] }}</div>
|
|
||||||
@endif
|
|
||||||
@if(!empty($liveIncomingCall['line']))
|
|
||||||
<div class="mt-1 text-[11px] text-emerald-700">{{ $this->excerpt((string) $liveIncomingCall['line'], 180) }}</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<x-filament::button size="sm" color="success" wire:click="creaPostItDaChiamataInIngresso">
|
|
||||||
Crea Post-it (prima valutazione)
|
|
||||||
</x-filament::button>
|
|
||||||
|
|
||||||
<x-filament::button size="sm" color="gray" wire:click="usaChiamataInIngresso">
|
|
||||||
Precompila ticket (opzionale)
|
|
||||||
</x-filament::button>
|
|
||||||
|
|
||||||
@if($liveCallCanClickToCall)
|
|
||||||
<x-filament::button size="sm" color="warning" wire:click="richiediClickToCallDaInterno">
|
|
||||||
Chiama da interno assegnato
|
|
||||||
</x-filament::button>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($rubricaUrl)
|
|
||||||
<a href="{{ $rubricaUrl }}" 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">
|
|
||||||
Apri scheda rubrica
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($estrattoUrl)
|
|
||||||
<a href="{{ $estrattoUrl }}" class="inline-flex items-center rounded-md bg-slate-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-700" target="_blank" rel="noopener noreferrer">
|
|
||||||
Apri estratto conto
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm text-slate-700">
|
<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.
|
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>
|
||||||
|
|
@ -124,70 +68,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@php
|
|
||||||
$ticketCallContext = $this->ticketCallContext;
|
|
||||||
@endphp
|
|
||||||
@if(!empty($ticketCallContext['phone']) || !empty($ticketCallContext['caller_name']) || !empty($ticketCallContext['target_extension']))
|
|
||||||
<div class="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-900">
|
|
||||||
<div class="font-semibold uppercase tracking-wide">Contesto chiamata</div>
|
|
||||||
<div class="mt-2 grid gap-2 md:grid-cols-2">
|
|
||||||
@if(!empty($ticketCallContext['phone']))
|
|
||||||
<div>
|
|
||||||
<div class="text-[11px] uppercase tracking-wide text-emerald-700">Numero chiamante</div>
|
|
||||||
<div class="text-sm font-semibold">{{ $ticketCallContext['phone'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(!empty($ticketCallContext['target_extension']))
|
|
||||||
<div>
|
|
||||||
<div class="text-[11px] uppercase tracking-wide text-emerald-700">Interno o gruppo chiamato</div>
|
|
||||||
<div class="text-sm font-semibold">{{ $ticketCallContext['target_extension'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(!empty($ticketCallContext['received_at']))
|
|
||||||
<div>
|
|
||||||
<div class="text-[11px] uppercase tracking-wide text-emerald-700">Ricevuta alle</div>
|
|
||||||
<div>{{ $ticketCallContext['received_at'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(!empty($ticketCallContext['caller_name']))
|
|
||||||
<div>
|
|
||||||
<div class="text-[11px] uppercase tracking-wide text-emerald-700">Contatto associato</div>
|
|
||||||
<div>{{ $ticketCallContext['caller_name'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(!empty($ticketCallContext['caller_profile']))
|
|
||||||
<div>
|
|
||||||
<div class="text-[11px] uppercase tracking-wide text-emerald-700">Profilo</div>
|
|
||||||
<div>{{ $ticketCallContext['caller_profile'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(!empty($ticketCallContext['riferimento_stabile']))
|
|
||||||
<div>
|
|
||||||
<div class="text-[11px] uppercase tracking-wide text-emerald-700">Riferimento stabile</div>
|
|
||||||
<div>{{ $ticketCallContext['riferimento_stabile'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(!empty($ticketCallContext['riferimento_unita']))
|
|
||||||
<div>
|
|
||||||
<div class="text-[11px] uppercase tracking-wide text-emerald-700">Riferimento unità</div>
|
|
||||||
<div>{{ $ticketCallContext['riferimento_unita'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(!empty($ticketCallContext['stabile_label']))
|
|
||||||
<div>
|
|
||||||
<div class="text-[11px] uppercase tracking-wide text-emerald-700">Stabile collegato</div>
|
|
||||||
<div>{{ $ticketCallContext['stabile_label'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@if(!empty($ticketCallContext['call_note']))
|
|
||||||
<div class="mt-2 rounded-md bg-white/70 px-3 py-2 text-[11px] text-emerald-800">
|
|
||||||
<div class="font-semibold">Nota operativa importata</div>
|
|
||||||
<div class="mt-1 whitespace-pre-wrap">{{ $ticketCallContext['call_note'] }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||||
<label class="block text-sm md:col-span-2">
|
<label class="block text-sm md:col-span-2">
|
||||||
<span class="mb-1 block font-medium">Titolo operativo</span>
|
<span class="mb-1 block font-medium">Titolo operativo</span>
|
||||||
|
|
|
||||||
500
scripts/install/laravel-site-bootstrap.sh
Normal file
500
scripts/install/laravel-site-bootstrap.sh
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
LOG_FILE="${LOG_FILE:-/tmp/laravel-site-bootstrap.log}"
|
||||||
|
|
||||||
|
SITE_KEY="${SITE_KEY:-}"
|
||||||
|
PROJECT_NAME="${PROJECT_NAME:-Laravel Site}"
|
||||||
|
APP_DIR="${APP_DIR:-}"
|
||||||
|
BRANCH="${BRANCH:-main}"
|
||||||
|
REPO_URL="${REPO_URL:-}"
|
||||||
|
APP_URL="${APP_URL:-http://127.0.0.1}"
|
||||||
|
SERVER_NAME="${SERVER_NAME:-_}"
|
||||||
|
DB_NAME="${DB_NAME:-}"
|
||||||
|
DB_USER="${DB_USER:-}"
|
||||||
|
DB_PASSWORD="${DB_PASSWORD:-}"
|
||||||
|
DB_HOST="${DB_HOST:-127.0.0.1}"
|
||||||
|
DB_PORT="${DB_PORT:-3306}"
|
||||||
|
PHP_VERSION="${PHP_VERSION:-8.3}"
|
||||||
|
RUN_NPM_BUILD=1
|
||||||
|
RUN_MIGRATIONS=1
|
||||||
|
CONFIGURE_SCHEDULER=1
|
||||||
|
CONFIGURE_QUEUE_WORKER=0
|
||||||
|
ALLOW_EXISTING_APP_DIR=0
|
||||||
|
ALLOW_EXISTING_DATABASE=0
|
||||||
|
REPLACE_NGINX_SITE=0
|
||||||
|
QUEUE_COMMAND="${QUEUE_COMMAND:-php artisan queue:work --sleep=3 --tries=3 --max-time=3600}"
|
||||||
|
|
||||||
|
OWNER_USER="${SUDO_USER:-$USER}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: laravel-site-bootstrap.sh [options]
|
||||||
|
|
||||||
|
Required options:
|
||||||
|
--repo-url <url> Git repository URL to clone
|
||||||
|
--db-password <password> MySQL password for the application user
|
||||||
|
|
||||||
|
Optional options:
|
||||||
|
--site-key <slug> Unique site key used for nginx/cron/supervisor names
|
||||||
|
--project-name <name> APP_NAME value (default: Laravel Site)
|
||||||
|
--branch <name> Git branch to checkout (default: main)
|
||||||
|
--app-dir <path> Laravel application directory (default: /var/www/<site-key>)
|
||||||
|
--app-url <url> Public APP_URL value (default: http://127.0.0.1)
|
||||||
|
--server-name <name> Nginx server_name value (default: _)
|
||||||
|
--db-name <name> MySQL database name (default derived from site key)
|
||||||
|
--db-user <user> MySQL app user (default derived from site key)
|
||||||
|
--db-host <host> MySQL host (default: 127.0.0.1)
|
||||||
|
--db-port <port> MySQL port (default: 3306)
|
||||||
|
--php-version <version> PHP FPM version for nginx upstream (default: 8.3)
|
||||||
|
--skip-build Skip npm install/build
|
||||||
|
--skip-migrations Skip artisan migrate --force
|
||||||
|
--without-scheduler Do not create the cron scheduler entry
|
||||||
|
--with-queue-worker Create a supervisor queue worker service
|
||||||
|
--queue-command <cmd> Queue worker command when --with-queue-worker is used
|
||||||
|
--allow-existing-app-dir Reuse an existing git worktree in the target app dir
|
||||||
|
--allow-existing-database Reuse an existing database/user instead of failing
|
||||||
|
--replace-nginx-site Replace an existing nginx site file for this site key
|
||||||
|
--help Show this message
|
||||||
|
|
||||||
|
Example:
|
||||||
|
sudo bash scripts/install/laravel-site-bootstrap.sh \
|
||||||
|
--site-key demo-netgescon \
|
||||||
|
--project-name "Demo NetGescon" \
|
||||||
|
--repo-url git@your-gitea:studio/demo-netgescon.git \
|
||||||
|
--branch main \
|
||||||
|
--app-url https://demo.example.it \
|
||||||
|
--server-name demo.example.it \
|
||||||
|
--db-password 'StrongDbPass123!'
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local level="$1"
|
||||||
|
shift
|
||||||
|
printf '[%s] %s\n' "$level" "$*" >&2
|
||||||
|
printf '[%s] %s\n' "$level" "$*" >> "$LOG_FILE" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
info() { log INFO "$@"; }
|
||||||
|
warn() { log WARN "$@"; }
|
||||||
|
fail() { log ERROR "$@"; exit 1; }
|
||||||
|
|
||||||
|
on_error() {
|
||||||
|
local exit_code="$1"
|
||||||
|
local line_no="$2"
|
||||||
|
local command_text="$3"
|
||||||
|
|
||||||
|
log ERROR "Installer failed at line ${line_no}: ${command_text} (exit ${exit_code})"
|
||||||
|
log ERROR "See log file: ${LOG_FILE}"
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR
|
||||||
|
|
||||||
|
run_root() {
|
||||||
|
if [[ ${EUID} -eq 0 ]]; then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_owner() {
|
||||||
|
local target_user="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [[ ${EUID} -eq 0 ]]; then
|
||||||
|
runuser -u "$target_user" -- "$@"
|
||||||
|
else
|
||||||
|
sudo -u "$target_user" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
sql_escape() {
|
||||||
|
printf '%s' "$1" | sed "s/'/''/g"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
local file="$3"
|
||||||
|
local escaped
|
||||||
|
|
||||||
|
escaped=$(printf '%s' "$value" | sed -e 's/[\/&]/\\&/g')
|
||||||
|
|
||||||
|
if grep -q "^${key}=" "$file"; then
|
||||||
|
sed -i "s/^${key}=.*/${key}=${escaped}/" "$file"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
slugify() {
|
||||||
|
local value="$1"
|
||||||
|
|
||||||
|
value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')
|
||||||
|
value=$(printf '%s' "$value" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')
|
||||||
|
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
value='laravel-site'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_site_key() {
|
||||||
|
if [[ -z "$SITE_KEY" ]]; then
|
||||||
|
if [[ -n "$APP_DIR" ]]; then
|
||||||
|
SITE_KEY="$(slugify "$(basename "$APP_DIR")")"
|
||||||
|
elif [[ "$SERVER_NAME" != '_' ]]; then
|
||||||
|
SITE_KEY="$(slugify "$SERVER_NAME")"
|
||||||
|
elif [[ -n "$DB_NAME" ]]; then
|
||||||
|
SITE_KEY="$(slugify "$DB_NAME")"
|
||||||
|
else
|
||||||
|
SITE_KEY='laravel-site'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
SITE_KEY="$(slugify "$SITE_KEY")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$APP_DIR" ]]; then
|
||||||
|
APP_DIR="/var/www/${SITE_KEY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$DB_NAME" ]]; then
|
||||||
|
DB_NAME="$(printf 'laravel_%s' "$SITE_KEY" | tr '-' '_' | cut -c1-64)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$DB_USER" ]]; then
|
||||||
|
DB_USER="$(printf '%s_user' "$DB_NAME" | cut -c1-32)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
mysql_database_exists() {
|
||||||
|
run_root mysql -Nse "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$1'" | grep -qx "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
mysql_user_exists() {
|
||||||
|
run_root mysql -Nse "SELECT User FROM mysql.user WHERE User = '$1' AND Host = 'localhost'" | grep -qx "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_safe_targets() {
|
||||||
|
if [[ -d "$APP_DIR" ]] && [[ -n "$(find "$APP_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then
|
||||||
|
if [[ $ALLOW_EXISTING_APP_DIR -ne 1 ]]; then
|
||||||
|
fail "App directory already exists and is not empty: $APP_DIR. Re-run with --allow-existing-app-dir only if you really want to reuse it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$APP_DIR/.git" || ! -f "$APP_DIR/artisan" ]]; then
|
||||||
|
fail "Existing app directory must already be a Laravel git worktree: $APP_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "$NGINX_SITE_FILE" || -L "$NGINX_SITE_ENABLED" ]]; then
|
||||||
|
if [[ $REPLACE_NGINX_SITE -ne 1 ]]; then
|
||||||
|
fail "Nginx site already exists for ${SITE_KEY}. Re-run with --replace-nginx-site only if replacement is intentional."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mysql_database_exists "$DB_NAME"; then
|
||||||
|
if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then
|
||||||
|
fail "Database already exists: $DB_NAME. Re-run with --allow-existing-database only if reuse is intentional."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mysql_user_exists "$DB_USER"; then
|
||||||
|
if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then
|
||||||
|
fail "Database user already exists: $DB_USER. Re-run with --allow-existing-database only if reuse is intentional."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--site-key)
|
||||||
|
SITE_KEY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--project-name)
|
||||||
|
PROJECT_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--repo-url)
|
||||||
|
REPO_URL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--branch)
|
||||||
|
BRANCH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--app-dir)
|
||||||
|
APP_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--app-url)
|
||||||
|
APP_URL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--server-name)
|
||||||
|
SERVER_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db-name)
|
||||||
|
DB_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db-user)
|
||||||
|
DB_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db-password)
|
||||||
|
DB_PASSWORD="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db-host)
|
||||||
|
DB_HOST="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db-port)
|
||||||
|
DB_PORT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--php-version)
|
||||||
|
PHP_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--skip-build)
|
||||||
|
RUN_NPM_BUILD=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-migrations)
|
||||||
|
RUN_MIGRATIONS=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--without-scheduler)
|
||||||
|
CONFIGURE_SCHEDULER=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--with-queue-worker)
|
||||||
|
CONFIGURE_QUEUE_WORKER=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--queue-command)
|
||||||
|
QUEUE_COMMAND="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--allow-existing-app-dir)
|
||||||
|
ALLOW_EXISTING_APP_DIR=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--allow-existing-database)
|
||||||
|
ALLOW_EXISTING_DATABASE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--replace-nginx-site)
|
||||||
|
REPLACE_NGINX_SITE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown option: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
info "Starting Laravel site bootstrap"
|
||||||
|
info "Execution user: $(id -un)"
|
||||||
|
info "Log file: $LOG_FILE"
|
||||||
|
|
||||||
|
if [[ -z "$REPO_URL" ]]; then
|
||||||
|
fail "--repo-url is required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$DB_PASSWORD" ]]; then
|
||||||
|
fail "--db-password is required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for command in git php composer mysql nginx; do
|
||||||
|
if ! command -v "$command" >/dev/null 2>&1; then
|
||||||
|
fail "Missing required command: $command"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
ensure_site_key
|
||||||
|
|
||||||
|
APP_PARENT_DIR="$(dirname "$APP_DIR")"
|
||||||
|
NGINX_SITE_FILE="/etc/nginx/sites-available/${SITE_KEY}.conf"
|
||||||
|
NGINX_SITE_ENABLED="/etc/nginx/sites-enabled/${SITE_KEY}.conf"
|
||||||
|
CRON_FILE="/etc/cron.d/${SITE_KEY}-scheduler"
|
||||||
|
SUPERVISOR_FILE="/etc/supervisor/conf.d/${SITE_KEY}-worker.conf"
|
||||||
|
TEMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||||
|
|
||||||
|
assert_safe_targets
|
||||||
|
|
||||||
|
info "Preparing directories"
|
||||||
|
run_root install -d -o "$OWNER_USER" -g www-data -m 2775 "$APP_PARENT_DIR"
|
||||||
|
|
||||||
|
if [[ -d "$APP_DIR/.git" ]]; then
|
||||||
|
info "Refreshing application worktree"
|
||||||
|
run_owner "$OWNER_USER" git -C "$APP_DIR" remote set-url origin "$REPO_URL"
|
||||||
|
run_owner "$OWNER_USER" git -C "$APP_DIR" fetch --all --prune
|
||||||
|
run_owner "$OWNER_USER" git -C "$APP_DIR" checkout "$BRANCH"
|
||||||
|
run_owner "$OWNER_USER" git -C "$APP_DIR" reset --hard "origin/$BRANCH"
|
||||||
|
else
|
||||||
|
info "Cloning application worktree"
|
||||||
|
run_owner "$OWNER_USER" git clone --branch "$BRANCH" "$REPO_URL" "$APP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$APP_DIR/artisan" ]]; then
|
||||||
|
fail "Laravel artisan file not found in $APP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Configuring MySQL database and user"
|
||||||
|
DB_PASSWORD_ESCAPED="$(sql_escape "$DB_PASSWORD")"
|
||||||
|
run_root mysql <<SQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
|
||||||
|
ALTER USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
|
||||||
|
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
info "Installing PHP dependencies"
|
||||||
|
run_owner "$OWNER_USER" composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
if [[ $RUN_NPM_BUILD -eq 1 && -f package.json ]]; then
|
||||||
|
info "Installing frontend dependencies"
|
||||||
|
if [[ -f package-lock.json ]]; then
|
||||||
|
run_owner "$OWNER_USER" npm ci
|
||||||
|
else
|
||||||
|
run_owner "$OWNER_USER" npm install
|
||||||
|
fi
|
||||||
|
run_owner "$OWNER_USER" npm run build
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f .env ]]; then
|
||||||
|
if [[ -f .env.example ]]; then
|
||||||
|
cp .env.example .env
|
||||||
|
else
|
||||||
|
fail ".env and .env.example are both missing"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Configuring Laravel environment"
|
||||||
|
set_env_value APP_NAME "$PROJECT_NAME" .env
|
||||||
|
set_env_value APP_ENV production .env
|
||||||
|
set_env_value APP_DEBUG false .env
|
||||||
|
set_env_value APP_URL "$APP_URL" .env
|
||||||
|
set_env_value DB_CONNECTION mysql .env
|
||||||
|
set_env_value DB_HOST "$DB_HOST" .env
|
||||||
|
set_env_value DB_PORT "$DB_PORT" .env
|
||||||
|
set_env_value DB_DATABASE "$DB_NAME" .env
|
||||||
|
set_env_value DB_USERNAME "$DB_USER" .env
|
||||||
|
set_env_value DB_PASSWORD "$DB_PASSWORD" .env
|
||||||
|
|
||||||
|
php artisan key:generate --force
|
||||||
|
php artisan storage:link || true
|
||||||
|
php artisan optimize:clear
|
||||||
|
|
||||||
|
if [[ $RUN_MIGRATIONS -eq 1 ]]; then
|
||||||
|
info "Running migrations"
|
||||||
|
php artisan migrate --force
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Caching Laravel configuration"
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache
|
||||||
|
|
||||||
|
info "Setting permissions"
|
||||||
|
run_root chown -R www-data:www-data "$APP_DIR"
|
||||||
|
run_root chmod -R 775 "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
|
||||||
|
|
||||||
|
info "Writing nginx virtual host"
|
||||||
|
cat > "$TEMP_DIR/${SITE_KEY}.conf" <<EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name $SERVER_NAME;
|
||||||
|
root $APP_DIR/public;
|
||||||
|
|
||||||
|
index index.php index.html;
|
||||||
|
client_max_body_size 64m;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/${SITE_KEY}-access.log;
|
||||||
|
error_log /var/log/nginx/${SITE_KEY}-error.log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass unix:/run/php/php${PHP_VERSION}-fpm.sock;
|
||||||
|
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.(?!well-known).* {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}.conf" "$NGINX_SITE_FILE"
|
||||||
|
run_root ln -sfn "$NGINX_SITE_FILE" "$NGINX_SITE_ENABLED"
|
||||||
|
run_root nginx -t
|
||||||
|
run_root systemctl reload nginx
|
||||||
|
|
||||||
|
if [[ $CONFIGURE_SCHEDULER -eq 1 ]]; then
|
||||||
|
info "Configuring scheduler cron"
|
||||||
|
cat > "$TEMP_DIR/${SITE_KEY}-scheduler" <<EOF
|
||||||
|
* * * * * www-data cd $APP_DIR && php artisan schedule:run >> /var/log/${SITE_KEY}-scheduler.log 2>&1
|
||||||
|
EOF
|
||||||
|
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-scheduler" "$CRON_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $CONFIGURE_QUEUE_WORKER -eq 1 ]]; then
|
||||||
|
info "Configuring supervisor worker"
|
||||||
|
cat > "$TEMP_DIR/${SITE_KEY}-worker.conf" <<EOF
|
||||||
|
[program:${SITE_KEY}-worker]
|
||||||
|
command=$QUEUE_COMMAND
|
||||||
|
directory=$APP_DIR
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
user=www-data
|
||||||
|
redirect_stderr=true
|
||||||
|
stdout_logfile=/var/log/${SITE_KEY}-worker.log
|
||||||
|
stopwaitsecs=3600
|
||||||
|
EOF
|
||||||
|
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-worker.conf" "$SUPERVISOR_FILE"
|
||||||
|
run_root supervisorctl reread
|
||||||
|
run_root supervisorctl update
|
||||||
|
run_root supervisorctl restart "${SITE_KEY}-worker" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUMMARY_FILE="$APP_DIR/install-site-summary.txt"
|
||||||
|
cat > "$TEMP_DIR/install-site-summary.txt" <<EOF
|
||||||
|
Laravel site bootstrap completed
|
||||||
|
Date: $(date -Is)
|
||||||
|
Site key: $SITE_KEY
|
||||||
|
Project name: $PROJECT_NAME
|
||||||
|
Application dir: $APP_DIR
|
||||||
|
Branch: $BRANCH
|
||||||
|
APP_URL: $APP_URL
|
||||||
|
Server name: $SERVER_NAME
|
||||||
|
Database: $DB_NAME
|
||||||
|
Database user: $DB_USER
|
||||||
|
Scheduler configured: $CONFIGURE_SCHEDULER
|
||||||
|
Queue worker configured: $CONFIGURE_QUEUE_WORKER
|
||||||
|
EOF
|
||||||
|
run_root install -o www-data -g www-data -m 0644 "$TEMP_DIR/install-site-summary.txt" "$SUMMARY_FILE"
|
||||||
|
|
||||||
|
info "Laravel site bootstrap completed"
|
||||||
|
printf '\nSummary file:\n'
|
||||||
|
printf ' %s\n' "$SUMMARY_FILE"
|
||||||
466
scripts/install/netgescon-node-bootstrap.sh
Executable file
466
scripts/install/netgescon-node-bootstrap.sh
Executable file
|
|
@ -0,0 +1,466 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/netgescon}"
|
||||||
|
GIT_SOURCE_DIR="${GIT_SOURCE_DIR:-/var/www/netgescon-git-source}"
|
||||||
|
SITE_KEY="${SITE_KEY:-}"
|
||||||
|
BRANCH="${BRANCH:-main}"
|
||||||
|
REPO_URL="${REPO_URL:-}"
|
||||||
|
APP_URL="${APP_URL:-http://127.0.0.1}"
|
||||||
|
SERVER_NAME="${SERVER_NAME:-_}"
|
||||||
|
DB_NAME="${DB_NAME:-netgescon}"
|
||||||
|
DB_USER="${DB_USER:-netgescon_user}"
|
||||||
|
DB_PASSWORD="${DB_PASSWORD:-}"
|
||||||
|
ADMIN_NAME="${ADMIN_NAME:-NetGescon Super Admin}"
|
||||||
|
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@netgescon.local}"
|
||||||
|
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
||||||
|
PHP_VERSION="${PHP_VERSION:-8.3}"
|
||||||
|
RUN_NPM_BUILD=1
|
||||||
|
RUN_MIGRATIONS=1
|
||||||
|
ALLOW_EXISTING_APP_DIR=0
|
||||||
|
ALLOW_EXISTING_DATABASE=0
|
||||||
|
REPLACE_NGINX_SITE=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: netgescon-node-bootstrap.sh [options]
|
||||||
|
|
||||||
|
Required options:
|
||||||
|
--repo-url <url> Git repository URL to clone as node source
|
||||||
|
--db-password <password> MySQL password for the application user
|
||||||
|
--admin-password <pwd> Password for the initial super-admin user
|
||||||
|
|
||||||
|
Optional options:
|
||||||
|
--site-key <slug> Unique site key used for nginx/cron/supervisor names
|
||||||
|
--branch <name> Git branch to checkout (default: main)
|
||||||
|
--app-dir <path> Laravel application directory (default: /var/www/netgescon)
|
||||||
|
--git-source-dir <path> Local Git source directory for browser sync (default: /var/www/netgescon-git-source)
|
||||||
|
--app-url <url> Public APP_URL value (default: http://127.0.0.1)
|
||||||
|
--server-name <name> Nginx server_name value (default: _)
|
||||||
|
--db-name <name> MySQL database name (default: netgescon)
|
||||||
|
--db-user <user> MySQL app user (default: netgescon_user)
|
||||||
|
--admin-email <email> Initial super-admin email (default: admin@netgescon.local)
|
||||||
|
--admin-name <name> Initial super-admin display name
|
||||||
|
--php-version <version> PHP FPM version for nginx upstream (default: 8.3)
|
||||||
|
--skip-build Skip npm install/build
|
||||||
|
--skip-migrations Skip artisan migrate and admin bootstrap
|
||||||
|
--allow-existing-app-dir Reuse an existing git worktree in the target app dir
|
||||||
|
--allow-existing-database Reuse an existing database/user instead of failing
|
||||||
|
--replace-nginx-site Replace an existing nginx site file for this site key
|
||||||
|
--help Show this message
|
||||||
|
|
||||||
|
Example:
|
||||||
|
sudo bash scripts/install/netgescon-node-bootstrap.sh \
|
||||||
|
--repo-url git@your-gitea:studio/netgescon.git \
|
||||||
|
--app-url https://netgescon.example.it \
|
||||||
|
--server-name netgescon.example.it \
|
||||||
|
--db-password 'StrongDbPass123!' \
|
||||||
|
--admin-password 'StrongAdminPass123!'
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local level="$1"
|
||||||
|
shift
|
||||||
|
printf '[%s] %s\n' "$level" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() { log INFO "$@"; }
|
||||||
|
warn() { log WARN "$@"; }
|
||||||
|
fail() { log ERROR "$@"; exit 1; }
|
||||||
|
|
||||||
|
slugify() {
|
||||||
|
local value="$1"
|
||||||
|
|
||||||
|
value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')
|
||||||
|
value=$(printf '%s' "$value" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')
|
||||||
|
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
value='netgescon'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_root() {
|
||||||
|
if [[ ${EUID} -eq 0 ]]; then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
sql_escape() {
|
||||||
|
printf '%s' "$1" | sed "s/'/''/g"
|
||||||
|
}
|
||||||
|
|
||||||
|
php_escape() {
|
||||||
|
printf '%s' "$1" | sed "s/'/\\'/g"
|
||||||
|
}
|
||||||
|
|
||||||
|
mysql_database_exists() {
|
||||||
|
run_root mysql -Nse "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$1'" | grep -qx "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
mysql_user_exists() {
|
||||||
|
run_root mysql -Nse "SELECT User FROM mysql.user WHERE User = '$1' AND Host = 'localhost'" | grep -qx "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
local file="$3"
|
||||||
|
local escaped
|
||||||
|
escaped=$(printf '%s' "$value" | sed -e 's/[\/&]/\\&/g')
|
||||||
|
|
||||||
|
if grep -q "^${key}=" "$file"; then
|
||||||
|
sed -i "s/^${key}=.*/${key}=${escaped}/" "$file"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--site-key)
|
||||||
|
SITE_KEY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--repo-url)
|
||||||
|
REPO_URL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--branch)
|
||||||
|
BRANCH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--app-dir)
|
||||||
|
APP_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--git-source-dir)
|
||||||
|
GIT_SOURCE_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--app-url)
|
||||||
|
APP_URL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--server-name)
|
||||||
|
SERVER_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db-name)
|
||||||
|
DB_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db-user)
|
||||||
|
DB_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--db-password)
|
||||||
|
DB_PASSWORD="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--admin-email)
|
||||||
|
ADMIN_EMAIL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--admin-name)
|
||||||
|
ADMIN_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--admin-password)
|
||||||
|
ADMIN_PASSWORD="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--php-version)
|
||||||
|
PHP_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--skip-build)
|
||||||
|
RUN_NPM_BUILD=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-migrations)
|
||||||
|
RUN_MIGRATIONS=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--allow-existing-app-dir)
|
||||||
|
ALLOW_EXISTING_APP_DIR=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--allow-existing-database)
|
||||||
|
ALLOW_EXISTING_DATABASE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--replace-nginx-site)
|
||||||
|
REPLACE_NGINX_SITE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown option: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$REPO_URL" ]]; then
|
||||||
|
fail "--repo-url is required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$DB_PASSWORD" ]]; then
|
||||||
|
fail "--db-password is required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$ADMIN_PASSWORD" && $RUN_MIGRATIONS -eq 1 ]]; then
|
||||||
|
fail "--admin-password is required unless --skip-migrations is used"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for command in git php composer mysql nginx; do
|
||||||
|
if ! command -v "$command" >/dev/null 2>&1; then
|
||||||
|
fail "Missing required command: $command"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$SITE_KEY" ]]; then
|
||||||
|
SITE_KEY="$(slugify "$(basename "$APP_DIR")")"
|
||||||
|
else
|
||||||
|
SITE_KEY="$(slugify "$SITE_KEY")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
APP_PARENT_DIR="$(dirname "$APP_DIR")"
|
||||||
|
NGINX_SITE_FILE="/etc/nginx/sites-available/${SITE_KEY}.conf"
|
||||||
|
NGINX_SITE_ENABLED="/etc/nginx/sites-enabled/${SITE_KEY}.conf"
|
||||||
|
SUPERVISOR_FILE="/etc/supervisor/conf.d/${SITE_KEY}-worker.conf"
|
||||||
|
CRON_FILE="/etc/cron.d/${SITE_KEY}-scheduler"
|
||||||
|
TEMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||||
|
|
||||||
|
if [[ -d "$APP_DIR" ]] && [[ -n "$(find "$APP_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then
|
||||||
|
if [[ $ALLOW_EXISTING_APP_DIR -ne 1 ]]; then
|
||||||
|
fail "App directory already exists and is not empty: $APP_DIR. Re-run with --allow-existing-app-dir only if you really want to reuse it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$APP_DIR/.git" || ! -f "$APP_DIR/artisan" ]]; then
|
||||||
|
fail "Existing app directory must already be a Laravel git worktree: $APP_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$GIT_SOURCE_DIR" ]] && [[ ! -d "$GIT_SOURCE_DIR/.git" ]] && [[ -n "$(find "$GIT_SOURCE_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then
|
||||||
|
fail "Git source directory exists and is not a git repository: $GIT_SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "$NGINX_SITE_FILE" || -L "$NGINX_SITE_ENABLED" ]]; then
|
||||||
|
if [[ $REPLACE_NGINX_SITE -ne 1 ]]; then
|
||||||
|
fail "Nginx site already exists for ${SITE_KEY}. Re-run with --replace-nginx-site only if replacement is intentional."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mysql_database_exists "$DB_NAME"; then
|
||||||
|
if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then
|
||||||
|
fail "Database already exists: $DB_NAME. Re-run with --allow-existing-database only if reuse is intentional."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mysql_user_exists "$DB_USER"; then
|
||||||
|
if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then
|
||||||
|
fail "Database user already exists: $DB_USER. Re-run with --allow-existing-database only if reuse is intentional."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Preparing directories"
|
||||||
|
run_root install -d -o "${SUDO_USER:-$USER}" -g www-data -m 2775 "$APP_PARENT_DIR"
|
||||||
|
run_root install -d -o "${SUDO_USER:-$USER}" -g www-data -m 2775 "$(dirname "$GIT_SOURCE_DIR")"
|
||||||
|
|
||||||
|
if [[ -d "$GIT_SOURCE_DIR/.git" ]]; then
|
||||||
|
info "Refreshing local source repository"
|
||||||
|
git -C "$GIT_SOURCE_DIR" remote set-url origin "$REPO_URL"
|
||||||
|
git -C "$GIT_SOURCE_DIR" fetch --all --prune
|
||||||
|
git -C "$GIT_SOURCE_DIR" checkout "$BRANCH"
|
||||||
|
git -C "$GIT_SOURCE_DIR" reset --hard "origin/$BRANCH"
|
||||||
|
else
|
||||||
|
info "Cloning local source repository"
|
||||||
|
git clone --branch "$BRANCH" "$REPO_URL" "$GIT_SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$APP_DIR/.git" ]]; then
|
||||||
|
info "Refreshing application worktree"
|
||||||
|
git -C "$APP_DIR" remote set-url origin "$REPO_URL"
|
||||||
|
git -C "$APP_DIR" fetch --all --prune
|
||||||
|
git -C "$APP_DIR" checkout "$BRANCH"
|
||||||
|
git -C "$APP_DIR" reset --hard "origin/$BRANCH"
|
||||||
|
else
|
||||||
|
info "Cloning application worktree"
|
||||||
|
git clone --branch "$BRANCH" "$GIT_SOURCE_DIR" "$APP_DIR"
|
||||||
|
git -C "$APP_DIR" remote set-url origin "$REPO_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_root git config --global --add safe.directory "$APP_DIR"
|
||||||
|
run_root git config --global --add safe.directory "$GIT_SOURCE_DIR"
|
||||||
|
|
||||||
|
if [[ ! -f "$APP_DIR/artisan" ]]; then
|
||||||
|
fail "Laravel artisan file not found in $APP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Configuring MySQL database and user"
|
||||||
|
DB_PASSWORD_ESCAPED="$(sql_escape "$DB_PASSWORD")"
|
||||||
|
run_root mysql <<SQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS \
|
||||||
|
\\`$DB_NAME\\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
|
||||||
|
ALTER USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
|
||||||
|
GRANT ALL PRIVILEGES ON \\`$DB_NAME\\`.* TO '$DB_USER'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
info "Installing PHP dependencies"
|
||||||
|
cd "$APP_DIR"
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
if [[ $RUN_NPM_BUILD -eq 1 && -f package.json ]]; then
|
||||||
|
info "Installing frontend dependencies"
|
||||||
|
if [[ -f package-lock.json ]]; then
|
||||||
|
npm ci
|
||||||
|
else
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
npm run build
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f .env ]]; then
|
||||||
|
if [[ -f .env.example ]]; then
|
||||||
|
cp .env.example .env
|
||||||
|
else
|
||||||
|
fail ".env and .env.example are both missing"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Configuring Laravel environment"
|
||||||
|
set_env_value APP_NAME NetGescon .env
|
||||||
|
set_env_value APP_ENV production .env
|
||||||
|
set_env_value APP_DEBUG false .env
|
||||||
|
set_env_value APP_URL "$APP_URL" .env
|
||||||
|
set_env_value DB_CONNECTION mysql .env
|
||||||
|
set_env_value DB_HOST 127.0.0.1 .env
|
||||||
|
set_env_value DB_PORT 3306 .env
|
||||||
|
set_env_value DB_DATABASE "$DB_NAME" .env
|
||||||
|
set_env_value DB_USERNAME "$DB_USER" .env
|
||||||
|
set_env_value DB_PASSWORD "$DB_PASSWORD" .env
|
||||||
|
set_env_value CACHE_STORE file .env
|
||||||
|
set_env_value SESSION_DRIVER file .env
|
||||||
|
set_env_value QUEUE_CONNECTION database .env
|
||||||
|
set_env_value FILESYSTEM_DISK public .env
|
||||||
|
set_env_value NETGESCON_GIT_SYNC_SOURCE_REPO "$GIT_SOURCE_DIR" .env
|
||||||
|
|
||||||
|
mkdir -p storage/app/support
|
||||||
|
ln -sfn "$GIT_SOURCE_DIR" storage/app/support/git-source
|
||||||
|
|
||||||
|
php artisan key:generate --force
|
||||||
|
php artisan storage:link || true
|
||||||
|
php artisan optimize:clear
|
||||||
|
|
||||||
|
if [[ $RUN_MIGRATIONS -eq 1 ]]; then
|
||||||
|
info "Running migrations"
|
||||||
|
php artisan migrate --force
|
||||||
|
|
||||||
|
info "Creating initial super-admin user"
|
||||||
|
ADMIN_NAME_ESCAPED="$(php_escape "$ADMIN_NAME")"
|
||||||
|
ADMIN_EMAIL_ESCAPED="$(php_escape "$ADMIN_EMAIL")"
|
||||||
|
ADMIN_PASSWORD_ESCAPED="$(php_escape "$ADMIN_PASSWORD")"
|
||||||
|
|
||||||
|
php artisan tinker --execute="use App\\Models\\User; use Illuminate\\Support\\Facades\\Hash; use Spatie\\Permission\\Models\\Role; \\$role = Role::firstOrCreate(['name' => 'super-admin', 'guard_name' => 'web']); \\$adminRole = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']); \\$user = User::updateOrCreate(['email' => '$ADMIN_EMAIL_ESCAPED'], ['name' => '$ADMIN_NAME_ESCAPED', 'email_verified_at' => now(), 'password' => Hash::make('$ADMIN_PASSWORD_ESCAPED'), 'is_active' => true, 'registration_status' => 'approved']); \\$user->syncRoles(['super-admin', 'admin']);"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Caching Laravel configuration"
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache
|
||||||
|
|
||||||
|
info "Setting permissions"
|
||||||
|
run_root chown -R www-data:www-data "$APP_DIR"
|
||||||
|
run_root chmod -R 775 "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
|
||||||
|
|
||||||
|
info "Writing nginx virtual host"
|
||||||
|
cat > "$TEMP_DIR/${SITE_KEY}.conf" <<EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name $SERVER_NAME;
|
||||||
|
root $APP_DIR/public;
|
||||||
|
|
||||||
|
index index.php index.html;
|
||||||
|
client_max_body_size 64m;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/${SITE_KEY}-access.log;
|
||||||
|
error_log /var/log/nginx/${SITE_KEY}-error.log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \\$uri \\$uri/ /index.php?\$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \\.php$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass unix:/run/php/php${PHP_VERSION}-fpm.sock;
|
||||||
|
fastcgi_param SCRIPT_FILENAME \\$realpath_root\$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\\.(?!well-known).* {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}.conf" "$NGINX_SITE_FILE"
|
||||||
|
run_root ln -sfn "$NGINX_SITE_FILE" "$NGINX_SITE_ENABLED"
|
||||||
|
run_root nginx -t
|
||||||
|
run_root systemctl reload nginx
|
||||||
|
|
||||||
|
info "Configuring scheduler cron"
|
||||||
|
cat > "$TEMP_DIR/${SITE_KEY}-scheduler" <<EOF
|
||||||
|
* * * * * www-data cd $APP_DIR && php artisan schedule:run >> /var/log/${SITE_KEY}-scheduler.log 2>&1
|
||||||
|
EOF
|
||||||
|
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-scheduler" "$CRON_FILE"
|
||||||
|
|
||||||
|
info "Configuring supervisor worker"
|
||||||
|
cat > "$TEMP_DIR/${SITE_KEY}-worker.conf" <<EOF
|
||||||
|
[program:${SITE_KEY}-worker]
|
||||||
|
command=php $APP_DIR/artisan queue:work --sleep=3 --tries=3 --max-time=3600
|
||||||
|
directory=$APP_DIR
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
user=www-data
|
||||||
|
redirect_stderr=true
|
||||||
|
stdout_logfile=/var/log/${SITE_KEY}-worker.log
|
||||||
|
stopwaitsecs=3600
|
||||||
|
EOF
|
||||||
|
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-worker.conf" "$SUPERVISOR_FILE"
|
||||||
|
run_root supervisorctl reread
|
||||||
|
run_root supervisorctl update
|
||||||
|
run_root supervisorctl restart "${SITE_KEY}-worker" || true
|
||||||
|
|
||||||
|
SUMMARY_FILE="$APP_DIR/install-node-summary.txt"
|
||||||
|
cat > "$TEMP_DIR/install-node-summary.txt" <<EOF
|
||||||
|
NetGescon node bootstrap completed
|
||||||
|
Date: $(date -Is)
|
||||||
|
Site key: $SITE_KEY
|
||||||
|
Application dir: $APP_DIR
|
||||||
|
Git source dir: $GIT_SOURCE_DIR
|
||||||
|
Branch: $BRANCH
|
||||||
|
APP_URL: $APP_URL
|
||||||
|
Server name: $SERVER_NAME
|
||||||
|
Database: $DB_NAME
|
||||||
|
Database user: $DB_USER
|
||||||
|
Admin email: $ADMIN_EMAIL
|
||||||
|
Update launcher: $APP_URL/admin-filament/supporto/aggiornamento-nodo
|
||||||
|
EOF
|
||||||
|
run_root install -o www-data -g www-data -m 0644 "$TEMP_DIR/install-node-summary.txt" "$SUMMARY_FILE"
|
||||||
|
|
||||||
|
info "NetGescon node bootstrap completed"
|
||||||
|
printf '\nOpen after first login:\n'
|
||||||
|
printf ' %s/admin-filament/supporto/aggiornamento-nodo\n' "$APP_URL"
|
||||||
|
printf '\nSummary file:\n'
|
||||||
|
printf ' %s\n' "$SUMMARY_FILE"
|
||||||
629
scripts/install/ubuntu2404-laravel-base.sh
Executable file
629
scripts/install/ubuntu2404-laravel-base.sh
Executable file
|
|
@ -0,0 +1,629 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
LOG_FILE="${LOG_FILE:-/tmp/ubuntu2404-laravel-base.log}"
|
||||||
|
|
||||||
|
PHP_VERSION="${PHP_VERSION:-8.3}"
|
||||||
|
NODE_MAJOR="${NODE_MAJOR:-20}"
|
||||||
|
LARAVEL_PROJECTS_DIR="${LARAVEL_PROJECTS_DIR:-/srv/laravel-projects}"
|
||||||
|
INSTALL_CORE=1
|
||||||
|
INSTALL_PHP=1
|
||||||
|
INSTALL_COMPOSER=1
|
||||||
|
INSTALL_NODE=1
|
||||||
|
INSTALL_XFCE=1
|
||||||
|
INSTALL_VSCODE=1
|
||||||
|
INSTALL_XRDP=1
|
||||||
|
CONFIGURE_FIREWALL=1
|
||||||
|
INTERACTIVE=1
|
||||||
|
|
||||||
|
declare -a INSTALLED_COMPONENTS=()
|
||||||
|
declare -a SKIPPED_COMPONENTS=()
|
||||||
|
|
||||||
|
if [[ -n "${SUDO_USER:-}" ]]; then
|
||||||
|
WORKSTATION_USER="$SUDO_USER"
|
||||||
|
else
|
||||||
|
WORKSTATION_USER="${USER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: ubuntu2404-laravel-base.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--php-version <version> PHP major.minor to install (default: 8.3)
|
||||||
|
--node-major <version> Node.js major release (default: 20)
|
||||||
|
--projects-dir <path> Root directory for Laravel projects (default: /srv/laravel-projects)
|
||||||
|
--without-core Skip core packages (nginx, mysql, redis, supervisor, tools)
|
||||||
|
--without-php Skip PHP installation and PHP tuning
|
||||||
|
--without-composer Skip Composer installation
|
||||||
|
--without-node Skip Node.js installation
|
||||||
|
--without-xfce Skip XFCE desktop packages
|
||||||
|
--without-vscode Skip Visual Studio Code installation
|
||||||
|
--without-xrdp Skip XRDP installation
|
||||||
|
--without-firewall Skip UFW configuration
|
||||||
|
--non-interactive Do not ask questions before installation
|
||||||
|
--workstation-user <user> User that will own desktop/workspace directories
|
||||||
|
--help Show this message
|
||||||
|
|
||||||
|
Example:
|
||||||
|
sudo bash scripts/install/ubuntu2404-laravel-base.sh --projects-dir /srv/laravel-projects
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local level="$1"
|
||||||
|
shift
|
||||||
|
printf '[%s] %s\n' "$level" "$*" >&2
|
||||||
|
|
||||||
|
if [[ -n "${LOG_FILE:-}" ]]; then
|
||||||
|
printf '[%s] %s\n' "$level" "$*" >> "$LOG_FILE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
info() { log INFO "$@"; }
|
||||||
|
warn() { log WARN "$@"; }
|
||||||
|
fail() { log ERROR "$@"; exit 1; }
|
||||||
|
|
||||||
|
on_error() {
|
||||||
|
local exit_code="$1"
|
||||||
|
local line_no="$2"
|
||||||
|
local command_text="$3"
|
||||||
|
|
||||||
|
log ERROR "Installer failed at line ${line_no}: ${command_text} (exit ${exit_code})"
|
||||||
|
log ERROR "See log file: ${LOG_FILE}"
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR
|
||||||
|
|
||||||
|
tty_available() {
|
||||||
|
[[ -r /dev/tty ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
command_version() {
|
||||||
|
local command_name="$1"
|
||||||
|
shift || true
|
||||||
|
local output=''
|
||||||
|
|
||||||
|
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||||
|
printf 'missing'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
output="$({ "$@" 2>&1 || true; } | head -n 1)"
|
||||||
|
else
|
||||||
|
output="$({ "$command_name" --version 2>&1 || true; } | head -n 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$output" ]]; then
|
||||||
|
printf '%s' "$output"
|
||||||
|
else
|
||||||
|
printf 'installed'
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
component_state() {
|
||||||
|
local value="$1"
|
||||||
|
|
||||||
|
if [[ "$value" == "missing" || -z "$value" ]]; then
|
||||||
|
printf 'missing'
|
||||||
|
else
|
||||||
|
printf 'present'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_component_line() {
|
||||||
|
local label="$1"
|
||||||
|
local state="$2"
|
||||||
|
local detail="$3"
|
||||||
|
|
||||||
|
printf ' - %-18s : %-8s %s\n' "$label" "$state" "$detail"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_yes_no() {
|
||||||
|
local prompt_text="$1"
|
||||||
|
local default_value="$2"
|
||||||
|
local answer=''
|
||||||
|
local suffix='[Y/n]'
|
||||||
|
|
||||||
|
if [[ "$default_value" == 'n' ]]; then
|
||||||
|
suffix='[y/N]'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! tty_available; then
|
||||||
|
[[ "$default_value" == 'y' ]]
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
printf '%s %s ' "$prompt_text" "$suffix" > /dev/tty
|
||||||
|
IFS= read -r answer < /dev/tty || answer=''
|
||||||
|
answer="${answer,,}"
|
||||||
|
|
||||||
|
if [[ -z "$answer" ]]; then
|
||||||
|
[[ "$default_value" == 'y' ]]
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$answer" in
|
||||||
|
y|yes)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
n|no)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
record_component() {
|
||||||
|
local label="$1"
|
||||||
|
local status="$2"
|
||||||
|
|
||||||
|
if [[ "$status" == 'installed' ]]; then
|
||||||
|
INSTALLED_COMPONENTS+=("$label")
|
||||||
|
else
|
||||||
|
SKIPPED_COMPONENTS+=("$label")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_vscode_apt_source_conflicts() {
|
||||||
|
local source_file=''
|
||||||
|
local removed_any=0
|
||||||
|
|
||||||
|
while IFS= read -r source_file; do
|
||||||
|
[[ -n "$source_file" ]] || continue
|
||||||
|
run_root rm -f "$source_file"
|
||||||
|
removed_any=1
|
||||||
|
done < <(grep -l "packages.microsoft.com/repos/code" /etc/apt/sources.list.d/*.list 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ $removed_any -eq 1 ]]; then
|
||||||
|
info 'Removed conflicting VS Code apt source entries'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
should_run_apt_work() {
|
||||||
|
[[ $INSTALL_CORE -eq 1 || \
|
||||||
|
$INSTALL_PHP -eq 1 || \
|
||||||
|
$INSTALL_COMPOSER -eq 1 || \
|
||||||
|
$INSTALL_NODE -eq 1 || \
|
||||||
|
$INSTALL_VSCODE -eq 1 || \
|
||||||
|
$INSTALL_XFCE -eq 1 || \
|
||||||
|
$INSTALL_XRDP -eq 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
run_root() {
|
||||||
|
if [[ ${EUID} -eq 0 ]]; then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_root_env() {
|
||||||
|
if [[ ${EUID} -eq 0 ]]; then
|
||||||
|
env "$@"
|
||||||
|
else
|
||||||
|
sudo env "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_as_user() {
|
||||||
|
local target_user="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [[ ${EUID} -eq 0 ]]; then
|
||||||
|
runuser -u "$target_user" -- "$@"
|
||||||
|
else
|
||||||
|
sudo -u "$target_user" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--php-version)
|
||||||
|
PHP_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--node-major)
|
||||||
|
NODE_MAJOR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--projects-dir)
|
||||||
|
LARAVEL_PROJECTS_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--without-core)
|
||||||
|
INSTALL_CORE=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--without-php)
|
||||||
|
INSTALL_PHP=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--without-composer)
|
||||||
|
INSTALL_COMPOSER=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--without-node)
|
||||||
|
INSTALL_NODE=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--without-xfce)
|
||||||
|
INSTALL_XFCE=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--without-vscode)
|
||||||
|
INSTALL_VSCODE=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--without-xrdp)
|
||||||
|
INSTALL_XRDP=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--without-firewall)
|
||||||
|
CONFIGURE_FIREWALL=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--non-interactive)
|
||||||
|
INTERACTIVE=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--workstation-user)
|
||||||
|
WORKSTATION_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown option: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
info "Starting Ubuntu 24.04 Laravel base installer"
|
||||||
|
info "Execution user: $(id -un)"
|
||||||
|
info "Log file: $LOG_FILE"
|
||||||
|
|
||||||
|
if [[ ! -f /etc/os-release ]]; then
|
||||||
|
fail "Cannot detect operating system"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /etc/os-release
|
||||||
|
if [[ "${ID:-}" != "ubuntu" || "${VERSION_ID:-}" != "24.04" ]]; then
|
||||||
|
fail "This script supports Ubuntu 24.04 only"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! id -u "$WORKSTATION_USER" >/dev/null 2>&1; then
|
||||||
|
fail "User not found: $WORKSTATION_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PHP_PACKAGES=(
|
||||||
|
"php${PHP_VERSION}"
|
||||||
|
"php${PHP_VERSION}-fpm"
|
||||||
|
"php${PHP_VERSION}-cli"
|
||||||
|
"php${PHP_VERSION}-common"
|
||||||
|
"php${PHP_VERSION}-curl"
|
||||||
|
"php${PHP_VERSION}-mbstring"
|
||||||
|
"php${PHP_VERSION}-xml"
|
||||||
|
"php${PHP_VERSION}-zip"
|
||||||
|
"php${PHP_VERSION}-mysql"
|
||||||
|
"php${PHP_VERSION}-sqlite3"
|
||||||
|
"php${PHP_VERSION}-bcmath"
|
||||||
|
"php${PHP_VERSION}-intl"
|
||||||
|
"php${PHP_VERSION}-gd"
|
||||||
|
"php${PHP_VERSION}-imagick"
|
||||||
|
"php${PHP_VERSION}-redis"
|
||||||
|
"php${PHP_VERSION}-opcache"
|
||||||
|
)
|
||||||
|
|
||||||
|
BASE_PACKAGES=(
|
||||||
|
acl
|
||||||
|
apt-transport-https
|
||||||
|
ca-certificates
|
||||||
|
curl
|
||||||
|
ffmpeg
|
||||||
|
git
|
||||||
|
gnupg
|
||||||
|
htop
|
||||||
|
imagemagick
|
||||||
|
jq
|
||||||
|
lsb-release
|
||||||
|
net-tools
|
||||||
|
nginx
|
||||||
|
redis-server
|
||||||
|
software-properties-common
|
||||||
|
supervisor
|
||||||
|
tree
|
||||||
|
unzip
|
||||||
|
ufw
|
||||||
|
vim
|
||||||
|
wget
|
||||||
|
mysql-server
|
||||||
|
)
|
||||||
|
|
||||||
|
show_preflight_screen() {
|
||||||
|
local os_label="${PRETTY_NAME:-Ubuntu 24.04}"
|
||||||
|
local php_detail
|
||||||
|
local composer_detail
|
||||||
|
local node_detail
|
||||||
|
local npm_detail
|
||||||
|
local mysql_detail
|
||||||
|
local nginx_detail
|
||||||
|
local redis_detail
|
||||||
|
local code_detail
|
||||||
|
local xfce_detail
|
||||||
|
local xrdp_detail
|
||||||
|
|
||||||
|
php_detail=$(command_version php php -v)
|
||||||
|
composer_detail=$(command_version composer composer --version)
|
||||||
|
node_detail=$(command_version node node --version)
|
||||||
|
npm_detail=$(command_version npm npm --version)
|
||||||
|
mysql_detail=$(command_version mysql mysql --version)
|
||||||
|
nginx_detail=$(command_version nginx nginx -v)
|
||||||
|
redis_detail=$(command_version redis-server redis-server --version)
|
||||||
|
code_detail=$(if dpkg -s code >/dev/null 2>&1; then dpkg-query -W -f='VS Code package ${Version}' code 2>/dev/null || printf 'VS Code installed'; else printf 'missing'; fi)
|
||||||
|
xfce_detail=$(if dpkg -s xfce4 >/dev/null 2>&1; then printf 'xfce4 installed'; else printf 'missing'; fi)
|
||||||
|
xrdp_detail=$(if dpkg -s xrdp >/dev/null 2>&1; then printf 'xrdp installed'; else printf 'missing'; fi)
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
printf '============================================================\n'
|
||||||
|
printf ' Laravel Base Installer - Preflight\n'
|
||||||
|
printf '============================================================\n'
|
||||||
|
printf ' Machine : %s\n' "$(hostname)"
|
||||||
|
printf ' System : %s\n' "$os_label"
|
||||||
|
printf ' User : %s\n' "$WORKSTATION_USER"
|
||||||
|
printf ' Projects dir : %s\n' "$LARAVEL_PROJECTS_DIR"
|
||||||
|
printf '\n'
|
||||||
|
printf 'Current status:\n'
|
||||||
|
print_component_line 'PHP' "$(component_state "$php_detail")" "$php_detail"
|
||||||
|
print_component_line 'Composer' "$(component_state "$composer_detail")" "$composer_detail"
|
||||||
|
print_component_line 'Node.js' "$(component_state "$node_detail")" "$node_detail"
|
||||||
|
print_component_line 'npm' "$(component_state "$npm_detail")" "$npm_detail"
|
||||||
|
print_component_line 'MySQL' "$(component_state "$mysql_detail")" "$mysql_detail"
|
||||||
|
print_component_line 'Nginx' "$(component_state "$nginx_detail")" "$nginx_detail"
|
||||||
|
print_component_line 'Redis' "$(component_state "$redis_detail")" "$redis_detail"
|
||||||
|
print_component_line 'VS Code' "$(component_state "$code_detail")" "$code_detail"
|
||||||
|
print_component_line 'XFCE' "$(component_state "$xfce_detail")" "$xfce_detail"
|
||||||
|
print_component_line 'XRDP' "$(component_state "$xrdp_detail")" "$xrdp_detail"
|
||||||
|
printf '\n'
|
||||||
|
printf 'Planned install set:\n'
|
||||||
|
print_component_line 'Core stack' "$([[ $INSTALL_CORE -eq 1 ]] && printf enabled || printf skipped)" 'nginx, mysql, redis, supervisor, common tools'
|
||||||
|
print_component_line 'PHP stack' "$([[ $INSTALL_PHP -eq 1 ]] && printf enabled || printf skipped)" "php${PHP_VERSION} and extensions"
|
||||||
|
print_component_line 'Composer' "$([[ $INSTALL_COMPOSER -eq 1 ]] && printf enabled || printf skipped)" 'global composer binary'
|
||||||
|
print_component_line 'Node.js' "$([[ $INSTALL_NODE -eq 1 ]] && printf enabled || printf skipped)" "Node ${NODE_MAJOR}.x"
|
||||||
|
print_component_line 'VS Code' "$([[ $INSTALL_VSCODE -eq 1 ]] && printf enabled || printf skipped)" 'desktop IDE'
|
||||||
|
print_component_line 'XFCE' "$([[ $INSTALL_XFCE -eq 1 ]] && printf enabled || printf skipped)" 'light desktop environment'
|
||||||
|
print_component_line 'XRDP' "$([[ $INSTALL_XRDP -eq 1 ]] && printf enabled || printf skipped)" 'remote desktop service'
|
||||||
|
print_component_line 'Firewall' "$([[ $CONFIGURE_FIREWALL -eq 1 ]] && printf enabled || printf skipped)" 'OpenSSH, HTTP, HTTPS, XRDP'
|
||||||
|
printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
run_interactive_preflight() {
|
||||||
|
if [[ $INTERACTIVE -ne 1 ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! tty_available; then
|
||||||
|
warn 'No tty available: proceeding with current flags without prompts.'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
show_preflight_screen
|
||||||
|
|
||||||
|
prompt_yes_no 'Install core stack and common tools?' 'y' && INSTALL_CORE=1 || INSTALL_CORE=0
|
||||||
|
prompt_yes_no "Install PHP ${PHP_VERSION} stack?" 'y' && INSTALL_PHP=1 || INSTALL_PHP=0
|
||||||
|
prompt_yes_no 'Install Composer?' 'y' && INSTALL_COMPOSER=1 || INSTALL_COMPOSER=0
|
||||||
|
prompt_yes_no "Install Node.js ${NODE_MAJOR}.x?" 'y' && INSTALL_NODE=1 || INSTALL_NODE=0
|
||||||
|
prompt_yes_no 'Install Visual Studio Code?' "$([[ $INSTALL_VSCODE -eq 1 ]] && printf y || printf n)" && INSTALL_VSCODE=1 || INSTALL_VSCODE=0
|
||||||
|
prompt_yes_no 'Install XFCE desktop?' "$([[ $INSTALL_XFCE -eq 1 ]] && printf y || printf n)" && INSTALL_XFCE=1 || INSTALL_XFCE=0
|
||||||
|
prompt_yes_no 'Install XRDP remote desktop?' "$([[ $INSTALL_XRDP -eq 1 ]] && printf y || printf n)" && INSTALL_XRDP=1 || INSTALL_XRDP=0
|
||||||
|
prompt_yes_no 'Configure firewall rules?' "$([[ $CONFIGURE_FIREWALL -eq 1 ]] && printf y || printf n)" && CONFIGURE_FIREWALL=1 || CONFIGURE_FIREWALL=0
|
||||||
|
|
||||||
|
printf '\nSelection summary:\n'
|
||||||
|
print_component_line 'Core stack' "$([[ $INSTALL_CORE -eq 1 ]] && printf install || printf skip)" ''
|
||||||
|
print_component_line 'PHP stack' "$([[ $INSTALL_PHP -eq 1 ]] && printf install || printf skip)" ''
|
||||||
|
print_component_line 'Composer' "$([[ $INSTALL_COMPOSER -eq 1 ]] && printf install || printf skip)" ''
|
||||||
|
print_component_line 'Node.js' "$([[ $INSTALL_NODE -eq 1 ]] && printf install || printf skip)" ''
|
||||||
|
print_component_line 'VS Code' "$([[ $INSTALL_VSCODE -eq 1 ]] && printf install || printf skip)" ''
|
||||||
|
print_component_line 'XFCE' "$([[ $INSTALL_XFCE -eq 1 ]] && printf install || printf skip)" ''
|
||||||
|
print_component_line 'XRDP' "$([[ $INSTALL_XRDP -eq 1 ]] && printf install || printf skip)" ''
|
||||||
|
print_component_line 'Firewall' "$([[ $CONFIGURE_FIREWALL -eq 1 ]] && printf configure || printf skip)" ''
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if ! prompt_yes_no 'Proceed with installation?' 'y'; then
|
||||||
|
fail 'Installation aborted by user'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_interactive_preflight
|
||||||
|
|
||||||
|
if should_run_apt_work; then
|
||||||
|
normalize_vscode_apt_source_conflicts
|
||||||
|
info "Updating apt metadata"
|
||||||
|
run_root apt-get update
|
||||||
|
run_root_env DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
|
||||||
|
else
|
||||||
|
info "No package installation selected: skipping apt metadata refresh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $INSTALL_CORE -eq 1 ]]; then
|
||||||
|
info "Installing base packages"
|
||||||
|
run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y "${BASE_PACKAGES[@]}"
|
||||||
|
record_component 'Core stack' 'installed'
|
||||||
|
else
|
||||||
|
record_component 'Core stack' 'skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $INSTALL_PHP -eq 1 ]]; then
|
||||||
|
info "Installing PHP ${PHP_VERSION} stack"
|
||||||
|
run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y "${PHP_PACKAGES[@]}"
|
||||||
|
record_component "PHP ${PHP_VERSION} stack" 'installed'
|
||||||
|
else
|
||||||
|
record_component "PHP ${PHP_VERSION} stack" 'skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $INSTALL_PHP -eq 1 ]]; then
|
||||||
|
info "Configuring PHP"
|
||||||
|
run_root mkdir -p "/etc/php/${PHP_VERSION}/fpm/conf.d" "/etc/php/${PHP_VERSION}/cli/conf.d"
|
||||||
|
run_root tee "/etc/php/${PHP_VERSION}/fpm/conf.d/99-laravel-base.ini" >/dev/null <<EOF
|
||||||
|
memory_limit=512M
|
||||||
|
upload_max_filesize=64M
|
||||||
|
post_max_size=64M
|
||||||
|
max_execution_time=300
|
||||||
|
max_input_vars=3000
|
||||||
|
opcache.enable=1
|
||||||
|
opcache.memory_consumption=192
|
||||||
|
opcache.interned_strings_buffer=16
|
||||||
|
opcache.max_accelerated_files=20000
|
||||||
|
opcache.validate_timestamps=1
|
||||||
|
opcache.revalidate_freq=2
|
||||||
|
realpath_cache_size=4096K
|
||||||
|
realpath_cache_ttl=600
|
||||||
|
EOF
|
||||||
|
run_root cp "/etc/php/${PHP_VERSION}/fpm/conf.d/99-laravel-base.ini" "/etc/php/${PHP_VERSION}/cli/conf.d/99-laravel-base.ini"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_COMPOSER_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP_COMPOSER_DIR"' EXIT
|
||||||
|
|
||||||
|
if [[ $INSTALL_COMPOSER -eq 1 ]]; then
|
||||||
|
info "Installing Composer"
|
||||||
|
curl -fsSL https://getcomposer.org/installer -o "$TMP_COMPOSER_DIR/composer-setup.php"
|
||||||
|
php "$TMP_COMPOSER_DIR/composer-setup.php" --install-dir="$TMP_COMPOSER_DIR" --filename=composer >/dev/null
|
||||||
|
run_root install -m 0755 "$TMP_COMPOSER_DIR/composer" /usr/local/bin/composer
|
||||||
|
record_component 'Composer' 'installed'
|
||||||
|
else
|
||||||
|
record_component 'Composer' 'skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $INSTALL_NODE -eq 1 ]]; then
|
||||||
|
info "Installing Node.js ${NODE_MAJOR}.x"
|
||||||
|
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | run_root bash
|
||||||
|
run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs
|
||||||
|
record_component "Node.js ${NODE_MAJOR}.x" 'installed'
|
||||||
|
else
|
||||||
|
record_component "Node.js ${NODE_MAJOR}.x" 'skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $INSTALL_VSCODE -eq 1 ]]; then
|
||||||
|
info "Installing Visual Studio Code"
|
||||||
|
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > "$TMP_COMPOSER_DIR/packages.microsoft.gpg"
|
||||||
|
run_root install -D -o root -g root -m 0644 "$TMP_COMPOSER_DIR/packages.microsoft.gpg" /usr/share/keyrings/packages.microsoft.gpg
|
||||||
|
|
||||||
|
normalize_vscode_apt_source_conflicts
|
||||||
|
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" \
|
||||||
|
| run_root tee /etc/apt/sources.list.d/vscode.list >/dev/null
|
||||||
|
run_root apt-get update
|
||||||
|
run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y code
|
||||||
|
record_component 'VS Code' 'installed'
|
||||||
|
else
|
||||||
|
record_component 'VS Code' 'skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $INSTALL_XFCE -eq 1 ]]; then
|
||||||
|
info "Installing XFCE desktop"
|
||||||
|
run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y xfce4 xfce4-goodies xorg dbus-x11
|
||||||
|
run_as_user "$WORKSTATION_USER" mkdir -p "/home/$WORKSTATION_USER"
|
||||||
|
run_as_user "$WORKSTATION_USER" bash -lc "printf 'xfce4-session\n' > ~/.xsession"
|
||||||
|
record_component 'XFCE desktop' 'installed'
|
||||||
|
else
|
||||||
|
record_component 'XFCE desktop' 'skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $INSTALL_XRDP -eq 1 ]]; then
|
||||||
|
info "Installing XRDP"
|
||||||
|
run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y xrdp
|
||||||
|
run_root systemctl enable xrdp
|
||||||
|
run_root systemctl restart xrdp
|
||||||
|
record_component 'XRDP' 'installed'
|
||||||
|
else
|
||||||
|
record_component 'XRDP' 'skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Creating Laravel workspace directories"
|
||||||
|
run_root mkdir -p "$LARAVEL_PROJECTS_DIR" "$LARAVEL_PROJECTS_DIR/shared" "$LARAVEL_PROJECTS_DIR/backups"
|
||||||
|
run_root chown -R "$WORKSTATION_USER":www-data "$LARAVEL_PROJECTS_DIR"
|
||||||
|
run_root chmod -R 2775 "$LARAVEL_PROJECTS_DIR"
|
||||||
|
|
||||||
|
info "Configuring services"
|
||||||
|
SERVICE_NAMES=()
|
||||||
|
if [[ $INSTALL_PHP -eq 1 ]]; then
|
||||||
|
SERVICE_NAMES+=("php${PHP_VERSION}-fpm")
|
||||||
|
fi
|
||||||
|
if [[ $INSTALL_CORE -eq 1 ]]; then
|
||||||
|
SERVICE_NAMES+=(nginx mysql redis-server supervisor)
|
||||||
|
fi
|
||||||
|
if [[ ${#SERVICE_NAMES[@]} -gt 0 ]]; then
|
||||||
|
run_root systemctl enable "${SERVICE_NAMES[@]}"
|
||||||
|
run_root systemctl restart "${SERVICE_NAMES[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $CONFIGURE_FIREWALL -eq 1 ]]; then
|
||||||
|
info "Configuring firewall"
|
||||||
|
run_root ufw allow OpenSSH
|
||||||
|
run_root ufw allow 80/tcp
|
||||||
|
run_root ufw allow 443/tcp
|
||||||
|
if [[ $INSTALL_XRDP -eq 1 ]]; then
|
||||||
|
run_root ufw allow 3389/tcp
|
||||||
|
fi
|
||||||
|
run_root ufw --force enable
|
||||||
|
record_component 'Firewall rules' 'installed'
|
||||||
|
else
|
||||||
|
record_component 'Firewall rules' 'skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUMMARY_FILE="/home/$WORKSTATION_USER/laravel-base-setup-summary.txt"
|
||||||
|
info "Writing summary to $SUMMARY_FILE"
|
||||||
|
SUMMARY_PHP="$(command_version php php -v)"
|
||||||
|
SUMMARY_COMPOSER="$(command_version composer composer --version)"
|
||||||
|
SUMMARY_NODE="$(command_version node node --version)"
|
||||||
|
SUMMARY_NPM="$(command_version npm npm --version)"
|
||||||
|
SUMMARY_MYSQL="$(command_version mysql mysql --version)"
|
||||||
|
SUMMARY_NGINX="$(command_version nginx nginx -v)"
|
||||||
|
SUMMARY_REDIS="$(command_version redis-server redis-server --version)"
|
||||||
|
cat > "$TMP_COMPOSER_DIR/summary.txt" <<EOF
|
||||||
|
Laravel Base Setup Summary
|
||||||
|
Date: $(date -Is)
|
||||||
|
Host: $(hostname)
|
||||||
|
Ubuntu: ${VERSION:-Ubuntu 24.04}
|
||||||
|
PHP: $SUMMARY_PHP
|
||||||
|
Composer: $SUMMARY_COMPOSER
|
||||||
|
Node: $SUMMARY_NODE
|
||||||
|
NPM: $SUMMARY_NPM
|
||||||
|
MySQL: $SUMMARY_MYSQL
|
||||||
|
Nginx: $SUMMARY_NGINX
|
||||||
|
Redis: $SUMMARY_REDIS
|
||||||
|
Projects dir: $LARAVEL_PROJECTS_DIR
|
||||||
|
Workstation user: $WORKSTATION_USER
|
||||||
|
XFCE installed: $INSTALL_XFCE
|
||||||
|
VS Code installed: $INSTALL_VSCODE
|
||||||
|
XRDP installed: $INSTALL_XRDP
|
||||||
|
Firewall configured: $CONFIGURE_FIREWALL
|
||||||
|
Installed components: ${INSTALLED_COMPONENTS[*]:-none}
|
||||||
|
Skipped components: ${SKIPPED_COMPONENTS[*]:-none}
|
||||||
|
EOF
|
||||||
|
run_root install -o "$WORKSTATION_USER" -g "$WORKSTATION_USER" -m 0644 "$TMP_COMPOSER_DIR/summary.txt" "$SUMMARY_FILE"
|
||||||
|
|
||||||
|
info "Base machine ready"
|
||||||
|
printf '\nInstalled components:\n'
|
||||||
|
for component in "${INSTALLED_COMPONENTS[@]}"; do
|
||||||
|
printf ' - %s\n' "$component"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\nSkipped components:\n'
|
||||||
|
for component in "${SKIPPED_COMPONENTS[@]}"; do
|
||||||
|
printf ' - %s\n' "$component"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\nNext steps:\n'
|
||||||
|
printf ' 1. Copy or clone your Laravel project under %s\n' "$LARAVEL_PROJECTS_DIR"
|
||||||
|
printf ' 2. Configure Nginx site and database for the specific project\n'
|
||||||
|
printf ' 3. For NetGescon, run scripts/install/netgescon-node-bootstrap.sh\n'
|
||||||
Loading…
Reference in New Issue
Block a user