Compare commits
2 Commits
119f486bb8
...
1c6611addb
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c6611addb | |||
| b37171a438 |
93
Dockerfile
93
Dockerfile
|
|
@ -1,44 +1,81 @@
|
|||
FROM php:8.2-fpm
|
||||
FROM php:8.3-cli AS vendor
|
||||
|
||||
# Installa dipendenze di sistema
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
libfreetype6-dev \
|
||||
libicu-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
zip \
|
||||
libzip-dev \
|
||||
unzip \
|
||||
nodejs \
|
||||
npm
|
||||
zip \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j"$(nproc)" gd intl zip \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Pulisce cache
|
||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Installa estensioni PHP
|
||||
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
|
||||
WORKDIR /app
|
||||
COPY composer.json composer.lock ./
|
||||
RUN composer install \
|
||||
--no-dev \
|
||||
--prefer-dist \
|
||||
--no-interaction \
|
||||
--no-progress \
|
||||
--no-scripts \
|
||||
--optimize-autoloader
|
||||
|
||||
# Installa Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
FROM node:20-bookworm-slim AS frontend
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
||||
COPY --from=vendor /app/vendor ./vendor
|
||||
COPY resources ./resources
|
||||
COPY public ./public
|
||||
COPY vite.config.js package.json postcss.config.js tailwind.config.js ./
|
||||
RUN npm run build
|
||||
|
||||
FROM php:8.3-fpm AS runtime
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
default-mysql-client \
|
||||
git \
|
||||
libfreetype6-dev \
|
||||
libicu-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libonig-dev \
|
||||
libpng-dev \
|
||||
libxml2-dev \
|
||||
libzip-dev \
|
||||
unzip \
|
||||
zip \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j"$(nproc)" bcmath exif gd intl mbstring opcache pcntl pdo_mysql zip \
|
||||
&& pecl install redis \
|
||||
&& docker-php-ext-enable redis \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Imposta directory di lavoro
|
||||
WORKDIR /var/www
|
||||
|
||||
# Copia file applicazione
|
||||
COPY . /var/www
|
||||
COPY --chown=www-data:www-data . .
|
||||
COPY --chown=www-data:www-data --from=vendor /app/vendor ./vendor
|
||||
COPY --chown=www-data:www-data --from=frontend /app/public/build ./public/build
|
||||
COPY --chown=www-data:www-data docker/entrypoint.sh /entrypoint.sh
|
||||
COPY --chown=www-data:www-data docker/php/local.ini /usr/local/etc/php/conf.d/99-netgescon.ini
|
||||
|
||||
# Installa dipendenze PHP
|
||||
RUN composer install --optimize-autoloader --no-dev
|
||||
RUN chmod +x /entrypoint.sh \
|
||||
&& mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views bootstrap/cache /opt/netgescon \
|
||||
&& cp -a public /opt/netgescon/public \
|
||||
&& composer dump-autoload --no-dev --optimize \
|
||||
&& chmod -R 775 storage bootstrap/cache
|
||||
|
||||
# Installa dipendenze Node.js
|
||||
RUN npm install && npm run build
|
||||
|
||||
# Imposta permessi
|
||||
RUN chown -R www-data:www-data /var/www \
|
||||
&& chmod -R 755 /var/www/storage \
|
||||
&& chmod -R 755 /var/www/bootstrap/cache
|
||||
|
||||
# Espone porta 9000
|
||||
EXPOSE 9000
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["php-fpm"]
|
||||
|
|
@ -76,11 +76,17 @@ class TabelleMillesimaliArchivio extends Page implements HasTable
|
|||
/** @var array<int, array<string, mixed>> */
|
||||
public array $tabelle = [];
|
||||
|
||||
/** @var array<int, int> */
|
||||
public array $selectedArchiveIds = [];
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $righe = [];
|
||||
|
||||
public ?array $tabellaInfo = null;
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
protected array $archiveDeletionAnalysisCache = [];
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
|
@ -321,7 +327,22 @@ public function table(Table $table): Table
|
|||
}
|
||||
|
||||
TabellaMillesimale::query()->create($payload);
|
||||
$this->resetArchiveDeletionAnalysisCache();
|
||||
$this->loadTabelle();
|
||||
$this->tabellaId = $this->pickTabellaId($this->tabellaId);
|
||||
$this->loadDettaglioTabella();
|
||||
$this->loadVociSpesa();
|
||||
}),
|
||||
|
||||
Action::make('delete_selected_archives')
|
||||
->label(fn (): string => 'Elimina selezionate (' . count($this->normalizeSelectedArchiveIds()) . ')')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Elimina archivi millesimali selezionati')
|
||||
->modalDescription('Saranno eliminate solo le tabelle senza collegamenti attivi. Le tabelle ancora usate da dettagli, voci o altri archivi verranno saltate.')
|
||||
->disabled(fn (): bool => $this->normalizeSelectedArchiveIds() === [])
|
||||
->action(fn (): mixed => $this->deleteSelectedArchives()),
|
||||
])
|
||||
->columns([
|
||||
TextColumn::make(
|
||||
|
|
@ -433,6 +454,14 @@ public function table(Table $table): Table
|
|||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('cleanup_status')
|
||||
->label('Integrità')
|
||||
->getStateUsing(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupBadge($record))
|
||||
->tooltip(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupTooltip($record))
|
||||
->badge()
|
||||
->color(fn(TabellaMillesimale $record): string => $this->analyzeArchiveDeletion($record)['deletable'] ? 'success' : 'warning')
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('preventivo')
|
||||
->label('Prev.')
|
||||
->getStateUsing(function (TabellaMillesimale $record) {
|
||||
|
|
@ -476,6 +505,13 @@ public function table(Table $table): Table
|
|||
->boolean(),
|
||||
])
|
||||
->actions([
|
||||
Action::make('select_cleanup')
|
||||
->label(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'Desel.' : 'Sel.')
|
||||
->icon(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'heroicon-o-check-circle' : 'heroicon-o-plus-circle')
|
||||
->color(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'primary' : 'gray')
|
||||
->tooltip(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupTooltip($record))
|
||||
->action(fn(TabellaMillesimale $record): mixed => $this->toggleArchiveSelection((int) $record->id)),
|
||||
|
||||
Action::make('edit')
|
||||
->label('Modifica')
|
||||
->icon('heroicon-o-pencil-square')
|
||||
|
|
@ -588,6 +624,7 @@ public function table(Table $table): Table
|
|||
}
|
||||
|
||||
$record->update($payload);
|
||||
$this->resetArchiveDeletionAnalysisCache();
|
||||
}),
|
||||
|
||||
Action::make('delete')
|
||||
|
|
@ -595,7 +632,7 @@ public function table(Table $table): Table
|
|||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(fn(TabellaMillesimale $record) => $record->delete()),
|
||||
->action(fn(TabellaMillesimale $record): mixed => $this->deleteSingleArchive($record)),
|
||||
|
||||
Action::make('prospetto')
|
||||
->label('Prospetto')
|
||||
|
|
@ -604,6 +641,194 @@ public function table(Table $table): Table
|
|||
]);
|
||||
}
|
||||
|
||||
protected function normalizeSelectedArchiveIds(): array
|
||||
{
|
||||
$selected = array_values(array_unique(array_map(
|
||||
static fn ($id): int => (int) $id,
|
||||
array_filter($this->selectedArchiveIds, static fn ($id): bool => is_numeric($id) && (int) $id > 0)
|
||||
)));
|
||||
|
||||
$this->selectedArchiveIds = $selected;
|
||||
|
||||
return $selected;
|
||||
}
|
||||
|
||||
public function toggleArchiveSelection(int $tabellaId): void
|
||||
{
|
||||
$selected = $this->normalizeSelectedArchiveIds();
|
||||
|
||||
if (in_array($tabellaId, $selected, true)) {
|
||||
$this->selectedArchiveIds = array_values(array_filter($selected, static fn (int $id): bool => $id !== $tabellaId));
|
||||
return;
|
||||
}
|
||||
|
||||
$selected[] = $tabellaId;
|
||||
$this->selectedArchiveIds = array_values(array_unique($selected));
|
||||
}
|
||||
|
||||
protected function deleteSelectedArchives(): void
|
||||
{
|
||||
$selected = $this->normalizeSelectedArchiveIds();
|
||||
if ($selected === []) {
|
||||
Notification::make()
|
||||
->title('Nessuna tabella selezionata')
|
||||
->warning()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$records = TabellaMillesimale::query()
|
||||
->where('stabile_id', $this->stabileId)
|
||||
->whereIn('id', $selected)
|
||||
->get();
|
||||
|
||||
$deleted = 0;
|
||||
$blocked = [];
|
||||
|
||||
foreach ($records as $record) {
|
||||
$analysis = $this->analyzeArchiveDeletion($record);
|
||||
if (! $analysis['deletable']) {
|
||||
$blocked[] = sprintf('%s (%s)', $record->codice_tabella ?: ('Tabella #' . $record->id), $analysis['summary']);
|
||||
continue;
|
||||
}
|
||||
|
||||
$record->delete();
|
||||
$deleted++;
|
||||
}
|
||||
|
||||
$this->selectedArchiveIds = array_values(array_diff($selected, $records->pluck('id')->map(fn ($id): int => (int) $id)->all()));
|
||||
$this->resetArchiveDeletionAnalysisCache();
|
||||
$this->loadTabelle();
|
||||
$this->tabellaId = $this->pickTabellaId($this->tabellaId && ! in_array($this->tabellaId, $selected, true) ? $this->tabellaId : null);
|
||||
$this->loadDettaglioTabella();
|
||||
$this->loadVociSpesa();
|
||||
|
||||
$notification = Notification::make();
|
||||
|
||||
if ($deleted > 0) {
|
||||
$notification->title('Pulizia archivi completata')->success();
|
||||
} else {
|
||||
$notification->title('Nessun archivio eliminato')->warning();
|
||||
}
|
||||
|
||||
$body = [];
|
||||
if ($deleted > 0) {
|
||||
$body[] = $deleted . ' tabella/e eliminata/e.';
|
||||
}
|
||||
if ($blocked !== []) {
|
||||
$body[] = 'Saltate per collegamenti attivi: ' . implode('; ', array_slice($blocked, 0, 5));
|
||||
if (count($blocked) > 5) {
|
||||
$body[] = 'Altre tabelle bloccate: ' . (count($blocked) - 5) . '.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($body !== []) {
|
||||
$notification->body(implode(' ', $body));
|
||||
}
|
||||
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
protected function deleteSingleArchive(TabellaMillesimale $record): void
|
||||
{
|
||||
$analysis = $this->analyzeArchiveDeletion($record);
|
||||
if (! $analysis['deletable']) {
|
||||
Notification::make()
|
||||
->title('Eliminazione bloccata')
|
||||
->body('La tabella e ancora collegata: ' . $analysis['summary'])
|
||||
->warning()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$record->delete();
|
||||
$this->selectedArchiveIds = array_values(array_filter($this->normalizeSelectedArchiveIds(), static fn (int $id): bool => $id !== (int) $record->id));
|
||||
$this->resetArchiveDeletionAnalysisCache();
|
||||
$this->loadTabelle();
|
||||
$this->tabellaId = $this->pickTabellaId($this->tabellaId === (int) $record->id ? null : $this->tabellaId);
|
||||
$this->loadDettaglioTabella();
|
||||
$this->loadVociSpesa();
|
||||
|
||||
Notification::make()
|
||||
->title('Tabella eliminata')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
protected function analyzeArchiveDeletion(TabellaMillesimale $record): array
|
||||
{
|
||||
$recordId = (int) $record->id;
|
||||
|
||||
if (isset($this->archiveDeletionAnalysisCache[$recordId])) {
|
||||
return $this->archiveDeletionAnalysisCache[$recordId];
|
||||
}
|
||||
|
||||
$sources = [
|
||||
'dettaglio_millesimi' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli millesimi'],
|
||||
'dettagli_tabelle_millesimali' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli tabelle'],
|
||||
'dettagli_millesimali' => ['fk' => 'tabella_id', 'label' => 'dettagli legacy'],
|
||||
'voci_spesa' => ['fk' => 'tabella_millesimale_default_id', 'label' => 'voci spesa'],
|
||||
'voci_preventivo' => ['fk' => 'tabella_millesimale_id', 'label' => 'voci preventivo'],
|
||||
'ripartizione_spese' => ['fk' => 'tabella_millesimale_id', 'label' => 'ripartizioni'],
|
||||
'ordine_giorno' => ['fk' => 'tabella_millesimale_id', 'label' => 'ordini del giorno'],
|
||||
'dettaglio_movimenti' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli movimenti'],
|
||||
];
|
||||
|
||||
$counts = [];
|
||||
foreach ($sources as $table => $config) {
|
||||
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $config['fk'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = DB::table($table)
|
||||
->where($config['fk'], $recordId)
|
||||
->count();
|
||||
|
||||
if ($count > 0) {
|
||||
$counts[$config['label']] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
$summaryParts = [];
|
||||
foreach ($counts as $label => $count) {
|
||||
$summaryParts[] = $label . ': ' . $count;
|
||||
}
|
||||
|
||||
return $this->archiveDeletionAnalysisCache[$recordId] = [
|
||||
'deletable' => $counts === [],
|
||||
'total_links' => array_sum($counts),
|
||||
'counts' => $counts,
|
||||
'summary' => $summaryParts === [] ? 'nessun collegamento attivo' : implode(', ', $summaryParts),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getArchiveCleanupBadge(TabellaMillesimale $record): string
|
||||
{
|
||||
$analysis = $this->analyzeArchiveDeletion($record);
|
||||
|
||||
if ($analysis['deletable']) {
|
||||
return 'Orfana';
|
||||
}
|
||||
|
||||
return $analysis['total_links'] . ' colleg.';
|
||||
}
|
||||
|
||||
protected function getArchiveCleanupTooltip(TabellaMillesimale $record): string
|
||||
{
|
||||
$analysis = $this->analyzeArchiveDeletion($record);
|
||||
|
||||
if ($analysis['deletable']) {
|
||||
return 'Tabella non collegata: eliminabile in pulizia.';
|
||||
}
|
||||
|
||||
return 'Tabella collegata, pulizia bloccata: ' . $analysis['summary'];
|
||||
}
|
||||
|
||||
protected function resetArchiveDeletionAnalysisCache(): void
|
||||
{
|
||||
$this->archiveDeletionAnalysisCache = [];
|
||||
}
|
||||
|
||||
protected function loadTabelle(): void
|
||||
{
|
||||
$this->tabelle = [];
|
||||
|
|
@ -1101,6 +1326,8 @@ private function formatCodiceUnita(?string $codice): ?string
|
|||
return null;
|
||||
}
|
||||
|
||||
$prefix = trim((string) ($this->codicePrefix ?? ''));
|
||||
|
||||
if ($prefix !== '') {
|
||||
$pattern = '/^' . preg_quote($prefix, '/') . '[\s\-\/]+/';
|
||||
$codice = preg_replace($pattern, '', $codice) ?? $codice;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
|
|
@ -29,14 +30,14 @@ public function store(LoginRequest $request): RedirectResponse
|
|||
|
||||
$user = $request->user();
|
||||
|
||||
if ($user && ! $user->is_active) {
|
||||
if ($user && $this->userTableHasColumn($user, 'is_active') && ! $user->is_active) {
|
||||
Auth::guard('web')->logout();
|
||||
throw ValidationException::withMessages([
|
||||
'email' => 'Account disattivato. Contatta il super-admin.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($user && $user->registration_status !== 'approved') {
|
||||
if ($user && $this->userTableHasColumn($user, 'registration_status') && $user->registration_status !== 'approved') {
|
||||
Auth::guard('web')->logout();
|
||||
throw ValidationException::withMessages([
|
||||
'email' => 'Registrazione in attesa di approvazione del super-admin.',
|
||||
|
|
@ -44,7 +45,7 @@ public function store(LoginRequest $request): RedirectResponse
|
|||
}
|
||||
|
||||
$requireEmailVerification = SystemSetting::bool('require_email_verification', true);
|
||||
$isSelfRegistered = $user && ! empty($user->requested_role);
|
||||
$isSelfRegistered = $user && $this->userTableHasColumn($user, 'requested_role') && ! empty($user->requested_role);
|
||||
if ($user && $isSelfRegistered && $requireEmailVerification && ! $user->hasVerifiedEmail()) {
|
||||
Auth::guard('web')->logout();
|
||||
throw ValidationException::withMessages([
|
||||
|
|
@ -57,6 +58,11 @@ public function store(LoginRequest $request): RedirectResponse
|
|||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
protected function userTableHasColumn($user, string $column): bool
|
||||
{
|
||||
return Schema::connection($user->getConnectionName())->hasColumn($user->getTable(), $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Throwable;
|
||||
|
||||
class SystemSetting extends Model
|
||||
{
|
||||
|
|
@ -12,7 +13,15 @@ class SystemSetting extends Model
|
|||
|
||||
public static function getValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (! static::settingsTableExists()) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$setting = static::query()->where('key', $key)->value('value');
|
||||
} catch (Throwable) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ($setting === null) {
|
||||
return $default;
|
||||
|
|
@ -25,6 +34,10 @@ public static function getValue(string $key, mixed $default = null): mixed
|
|||
|
||||
public static function setValue(string $key, mixed $value): void
|
||||
{
|
||||
if (! static::settingsTableExists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$storedValue = is_string($value) ? $value : json_encode($value);
|
||||
|
||||
static::query()->updateOrCreate(
|
||||
|
|
@ -37,4 +50,15 @@ public static function bool(string $key, bool $default = false): bool
|
|||
{
|
||||
return filter_var(static::getValue($key, $default), FILTER_VALIDATE_BOOL);
|
||||
}
|
||||
|
||||
protected static function settingsTableExists(): bool
|
||||
{
|
||||
try {
|
||||
$model = new static();
|
||||
|
||||
return $model->getConnection()->getSchemaBuilder()->hasTable($model->getTable());
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
use App\Models\Soggetto;
|
||||
use App\Models\User;
|
||||
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class LiveIncomingCallService
|
||||
{
|
||||
|
|
@ -15,6 +16,10 @@ class LiveIncomingCallService
|
|||
*/
|
||||
public function getForUser(User $user): ?array
|
||||
{
|
||||
if (! Schema::hasTable('communication_messages')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userExtension = trim((string) ($user->pbx_extension ?? ''));
|
||||
$watchedExtensions = app(PbxRoutingService::class)->getWatchedExtensionsForUser($user);
|
||||
$popupRestricted = (bool) ($user->pbx_popup_enabled ?? false) && count($watchedExtensions) > 0;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public function resolveByExtension(?string $extension): array
|
|||
}
|
||||
|
||||
$stabileId = null;
|
||||
if (method_exists($user, 'stabiliAssegnati')) {
|
||||
if ($this->canResolveAssignedStabili() && method_exists($user, 'stabiliAssegnati')) {
|
||||
$stabileId = $user->stabiliAssegnati()->value('stabili.id');
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ public function resolveByIncomingLine(?string $line): array
|
|||
if ($mappedUser instanceof User) {
|
||||
$targetRouting = [
|
||||
'user_id' => (int) $mappedUser->id,
|
||||
'stabile_id' => method_exists($mappedUser, 'stabiliAssegnati') ? ((int) ($mappedUser->stabiliAssegnati()->value('stabili.id') ?? 0) ?: null): null,
|
||||
'stabile_id' => $this->canResolveAssignedStabili() && method_exists($mappedUser, 'stabiliAssegnati') ? ((int) ($mappedUser->stabiliAssegnati()->value('stabili.id') ?? 0) ?: null): null,
|
||||
'user' => $mappedUser,
|
||||
];
|
||||
}
|
||||
|
|
@ -335,13 +335,18 @@ private function resolveAmministratoreForUser(User $user): ?Amministratore
|
|||
}
|
||||
|
||||
$amministratoreId = null;
|
||||
if (method_exists($user, 'stabiliAssegnati')) {
|
||||
if ($this->canResolveAssignedStabili() && method_exists($user, 'stabiliAssegnati')) {
|
||||
$amministratoreId = $user->stabiliAssegnati()->value('stabili.amministratore_id');
|
||||
}
|
||||
|
||||
return $amministratoreId ? Amministratore::query()->find((int) $amministratoreId) : null;
|
||||
}
|
||||
|
||||
private function canResolveAssignedStabili(): bool
|
||||
{
|
||||
return Schema::hasTable('collaboratore_stabile') && Schema::hasTable('stabili');
|
||||
}
|
||||
|
||||
private function resolveAmministratoreIdForUser(User $user): ?int
|
||||
{
|
||||
$amministratore = $this->resolveAmministratoreForUser($user);
|
||||
|
|
|
|||
|
|
@ -78,7 +78,14 @@
|
|||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
"dont-discover": [
|
||||
"laravel/breeze",
|
||||
"laravel/pail",
|
||||
"laravel/sail",
|
||||
"nunomaduro/collision",
|
||||
"nunomaduro/termwind",
|
||||
"pestphp/pest-plugin-laravel"
|
||||
]
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@ set -euo pipefail
|
|||
|
||||
cd /var/www
|
||||
|
||||
if [[ -d /opt/netgescon/public ]]; then
|
||||
mkdir -p /var/www/public
|
||||
find /var/www/public -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
||||
cp -a /opt/netgescon/public/. /var/www/public/
|
||||
fi
|
||||
|
||||
if [[ -n "${MYSQL_SSL_VERIFY_SERVER_CERT:-}" ]]; then
|
||||
cat > /root/.my.cnf <<EOF
|
||||
[client]
|
||||
|
|
@ -19,6 +25,14 @@ if [[ "${FIX_PERMISSIONS:-false}" == "true" ]]; then
|
|||
chmod -R 775 /var/www/storage /var/www/bootstrap/cache || true
|
||||
fi
|
||||
|
||||
rm -f \
|
||||
/var/www/bootstrap/cache/packages.php \
|
||||
/var/www/bootstrap/cache/services.php \
|
||||
/var/www/bootstrap/cache/config.php \
|
||||
/var/www/bootstrap/cache/events.php \
|
||||
/var/www/bootstrap/cache/routes-v7.php \
|
||||
/var/www/bootstrap/cache/routes.php
|
||||
|
||||
if [[ "${WAIT_FOR_DB:-true}" == "true" ]] && [[ -n "${DB_HOST:-}" ]]; then
|
||||
for i in {1..30}; do
|
||||
if mysqladmin ping -h "${DB_HOST}" -P "${DB_PORT:-3306}" -u "${DB_USERNAME:-root}" -p"${DB_PASSWORD:-}" --silent; then
|
||||
|
|
|
|||
|
|
@ -1,27 +1,134 @@
|
|||
# Docker setup (distribuzione)
|
||||
# Docker setup operativo Day0
|
||||
|
||||
## Prerequisiti
|
||||
- Docker + Docker Compose
|
||||
- Accesso ai volumi legacy (read-only)
|
||||
Questo documento descrive lo stack Docker realmente usato nel repository operativo `netgescon-day0`.
|
||||
|
||||
## 1) Configurazione
|
||||
1. Copia `.env.docker.example` → `.env`
|
||||
2. Compila le variabili principali:
|
||||
- `APP_URL`
|
||||
- `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `DB_ROOT_PASSWORD`
|
||||
- `LEGACY_ARCHIVES_PATH`
|
||||
## Obiettivo
|
||||
|
||||
## 2) Avvio
|
||||
```
|
||||
docker compose -f docker/docker-compose.prod.yml up -d
|
||||
Avviare solo l'applicativo NetGescon in container, lasciando fuori:
|
||||
|
||||
- MySQL esistente
|
||||
- archivi gia presenti
|
||||
- storage gia popolato, quando serve lavorare sui dati correnti
|
||||
|
||||
## Stack effettivo
|
||||
|
||||
Il file autorevole e `docker-compose.yml` nella root del repository.
|
||||
|
||||
Servizi inclusi:
|
||||
|
||||
- `app`
|
||||
- `queue`
|
||||
- `scheduler`
|
||||
- `nginx`
|
||||
- `redis`
|
||||
- `mailhog`
|
||||
|
||||
Il wrapper operativo e:
|
||||
|
||||
```bash
|
||||
bash docker/run-app-stack.sh up -d --build
|
||||
```
|
||||
|
||||
## 3) Aggiornamenti
|
||||
```
|
||||
bash scripts/update-docker.sh
|
||||
## 1) Configurazione locale
|
||||
|
||||
Creare il file `.env.docker.local` partendo da `.env.docker.local.example`.
|
||||
|
||||
Variabili minime da verificare:
|
||||
|
||||
- `APP_URL`
|
||||
- `APP_PORT`
|
||||
- `NETGESCON_DB_HOST`
|
||||
- `NETGESCON_DB_PORT`
|
||||
- `NETGESCON_DB_DATABASE`
|
||||
- `NETGESCON_DB_USERNAME`
|
||||
- `NETGESCON_DB_PASSWORD`
|
||||
- `NETGESCON_STORAGE_PATH`
|
||||
- `NETGESCON_CACHE_PATH`
|
||||
- `RUN_MIGRATIONS`
|
||||
|
||||
Valori consigliati per lavorare sui dati esistenti:
|
||||
|
||||
```dotenv
|
||||
RUN_MIGRATIONS=false
|
||||
NETGESCON_STORAGE_PATH=./storage
|
||||
NETGESCON_CACHE_PATH=./bootstrap/cache
|
||||
```
|
||||
|
||||
## 4) Note
|
||||
- Gli archivi legacy non sono inclusi nelle immagini Docker.
|
||||
- Vengono montati in read-only da `LEGACY_ARCHIVES_PATH`.
|
||||
- La migrazione DB viene eseguita all’avvio (entrypoint).
|
||||
Se gli archivi correnti sono gia presenti nella cartella `storage` del workspace, non serve alcun path esterno aggiuntivo: il container usera direttamente quello storage.
|
||||
|
||||
Se invece gli archivi stanno fuori dal repository, puntare `NETGESCON_STORAGE_PATH` al path host reale.
|
||||
|
||||
## 2) Validazione prima dell'avvio
|
||||
|
||||
Controllare che il compose risolva correttamente le variabili:
|
||||
|
||||
```bash
|
||||
bash docker/run-app-stack.sh config
|
||||
```
|
||||
|
||||
Questo e il check piu economico per verificare:
|
||||
|
||||
- file env presente
|
||||
- sostituzione variabili corretta
|
||||
- bind mount storage/cache coerenti
|
||||
- nessuna migrazione automatica indesiderata
|
||||
|
||||
## 3) Avvio stack
|
||||
|
||||
Avvio completo:
|
||||
|
||||
```bash
|
||||
bash docker/run-app-stack.sh up -d --build
|
||||
```
|
||||
|
||||
Verifica container:
|
||||
|
||||
```bash
|
||||
bash docker/run-app-stack.sh ps
|
||||
```
|
||||
|
||||
Log applicazione:
|
||||
|
||||
```bash
|
||||
bash docker/run-app-stack.sh logs app --tail=200
|
||||
```
|
||||
|
||||
Log nginx:
|
||||
|
||||
```bash
|
||||
bash docker/run-app-stack.sh logs nginx --tail=200
|
||||
```
|
||||
|
||||
## 4) Accesso e collaudo
|
||||
|
||||
URL locale/staging base:
|
||||
|
||||
```text
|
||||
http://localhost:${APP_PORT}
|
||||
```
|
||||
|
||||
Collaudi minimi dopo l'avvio:
|
||||
|
||||
1. login Filament
|
||||
2. pagina `Servizi / Utenze`
|
||||
3. verifica righe FE acqua con riferimento unico `CBILL`
|
||||
4. pagina `Acquisizione Documenti`
|
||||
5. lettore Datalogic con visualizzazione del testo etichetta archivio
|
||||
|
||||
## 5) Politica dati attuale
|
||||
|
||||
- DB esterno esistente: consentito
|
||||
- archivi esistenti: consentiti
|
||||
- migrazioni automatiche in avvio: disattivate di default
|
||||
- `mailhog`: usato per test locale, non per invio ufficiale
|
||||
|
||||
## 6) Differenze rispetto ai documenti storici
|
||||
|
||||
I documenti storici che citano `docker/docker-compose.prod.yml`, `.env.docker.example` o mount legacy separati non sono piu il riferimento operativo principale per Day0.
|
||||
|
||||
Per Day0 fanno fede:
|
||||
|
||||
- `docker-compose.yml`
|
||||
- `docker/run-app-stack.sh`
|
||||
- `.env.docker.local.example`
|
||||
- `.env.docker.local` locale non versionato
|
||||
|
|
|
|||
|
|
@ -100,6 +100,19 @@ ### 2. Allineamento stabile/anno nelle pagine gestionali
|
|||
- [U] base piu affidabile per estrazioni acqua e verifiche contabili per gestione
|
||||
- [P] meno logica sparsa con chiavi session legacy non coerenti tra le pagine
|
||||
|
||||
### 2-bis. Pulizia sicura archivi tabelle millesimali
|
||||
|
||||
- In `condomini/tabelle-millesimali`, tab `Archivio`, ogni riga ha ora l azione `Sel.` per marcare archivi sospetti o dati sporchi.
|
||||
- Il pulsante `Elimina selezionate` rimuove solo le tabelle orfane e salta automaticamente quelle ancora referenziate.
|
||||
- Il controllo integrita verifica in modo adattivo le tabelle realmente presenti nell ambiente, tra cui `dettaglio_millesimi`, `dettagli_tabelle_millesimali`, `voci_spesa` e `voci_preventivo`.
|
||||
- Anche l azione singola `Elimina` e ora protetta: se la tabella e ancora usata, la delete viene bloccata con dettaglio dei collegamenti attivi.
|
||||
|
||||
Effetto pratico:
|
||||
|
||||
- [U] si possono ripulire archivi legacy o tabelle non valorizzate senza rompere collegamenti gia attivi
|
||||
- [U] l operatore vede subito in colonna `Integrita` se una tabella e `Orfana` oppure quanti collegamenti ha
|
||||
- [P] la pulizia resta coerente tra ambienti con strutture database leggermente diverse
|
||||
|
||||
### 3. Supporto > Modifiche come pannello unico
|
||||
|
||||
- La pagina ha gia tab per aggiornamenti, bug, commit e migliorie.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,18 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
</div>
|
||||
|
||||
@if(($tab ?? 'archivio') === 'archivio')
|
||||
<x-filament::section class="p-4!">
|
||||
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-semibold text-gray-900">Pulizia archivi millesimali</div>
|
||||
<div class="text-sm text-gray-600">Usa `Sel.` per marcare gli archivi sporchi. La cancellazione elimina solo le tabelle orfane e blocca quelle ancora collegate a dettagli, voci o altri riferimenti attivi.</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
|
||||
Selezionate: {{ count($selectedArchiveIds ?? []) }}
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
{{ $this->table }}
|
||||
@elseif(($tab ?? 'archivio') === 'prospetto')
|
||||
@php
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user